diff --git a/.editorconfig b/.editorconfig index a87e5e06513..fa740a7511d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false resharper_csharp_prefer_qualified_reference = false resharper_csharp_allow_alias = false -[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}] +[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}] indent_size = 2 [nuget.config] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 14f591ec87d..2a4a4582772 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,11 +11,13 @@ /Resources/Prototypes/Maps/** @Emisse @ArtisticRoomba @spanky-spanky /Resources/Maps/** @Emisse @ArtisticRoomba @spanky-spanky -/Resources/Prototypes/Body/ @DrSmugleaf # suffering +/Resources/Prototypes/Body/ @DrSmugleaf @sowelipililimute # suffering /Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf /Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf /Resources/Prototypes/Guidebook/rules.yml @crazybrain23 -/Content.*/Body/ @DrSmugleaf +/Content.*/Body/ @DrSmugleaf @sowelipililimute +/Content.*/Humanoid/ @sowelipililimute +/Content.Client/Lobby/UI/Humanoid* @sowelipililimute /Content.YAMLLinter @DrSmugleaf /Content.Shared/Damage/ @DrSmugleaf @@ -24,10 +26,11 @@ /Content.*/Forensics/ @ficcialfaint -/Content.*/Trigger/ @slarticodefast +/Content.*/HeatContainer/ @slarticodefast /Content.*/Stunnable/ @Princess-Cheeseballs /Content.*/Nutrition/ @Princess-Cheeseballs +/Content.*/EntityEffects @Princess-Cheeseballs @sowelipililimute # SKREEEE /Content.*.Database/ @PJB3005 @DrSmugleaf @@ -35,12 +38,6 @@ /Pow3r/ @PJB3005 /Content.Server/Power/Pow3r/ @PJB3005 - -# Sloth -/Content.*/Audio @metalgearsloth -/Content.*/Movement @metalgearsloth -/Content.*/NPC @metalgearsloth -/Content.*/Shuttles @metalgearsloth -/Content.*/Weapons @metalgearsloth - /Content.Server/Discord/ @Simyon264 + +/Content.*/Atmos/** @ArtisticRoomba diff --git a/.github/ISSUE_TEMPLATE/01_report_issue.yml b/.github/ISSUE_TEMPLATE/01_report_issue.yml new file mode 100644 index 00000000000..0c70d751fc6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_report_issue.yml @@ -0,0 +1,41 @@ +name: Report an issue +description: For issues that are not related to any other issue form. +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill out an issue. This template is intended for any issues that do not fit other issue templates. + For feedback or help running Space Station 14, please join our [Discord](https://discord.gg/rGvu9hKffJ). + - type: input + attributes: + label: What version did the issue occur in? + description: You can find the version by opening the changelog in-game and looking at the bottom right corner of the changelog window. + placeholder: wizards/aa1337b + validations: + required: false + - type: textarea + attributes: + label: Description + description: | + A clear and concise description of what the issue is. + If the issue is visual in nature, consider posting a screenshot. + validations: + required: true + - type: textarea + attributes: + label: Reproduction + description: List the steps required to reproduce the issue. + placeholder: | + 1. + 2. + 3. + ... + validations: + required: true + - type: textarea + attributes: + label: Additional Context + description: Add any other additional context about the issue here. + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/02_mapping_issue.yml b/.github/ISSUE_TEMPLATE/02_mapping_issue.yml new file mode 100644 index 00000000000..36fe96c914b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_mapping_issue.yml @@ -0,0 +1,48 @@ +name: Report an mapping issue +description: For issues regarding mapping. +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill out an issue. This template is intended for any issues related to mapping. + For feedback or help running Space Station 14, please join our [Discord](https://discord.gg/rGvu9hKffJ). + - type: input + attributes: + label: What version did the issue occur in? + description: You can find the version by opening the changelog in-game and looking at the bottom right corner of the changelog window. + placeholder: wizards/aa1337b + validations: + required: false + - type: input + attributes: + label: On what station, shuttle, or grid did the issue occur on? + description: The name of the station, shuttle, or grid. If you do not know the name, try to describe it. + placeholder: Bagel + validations: + required: true + - type: textarea + attributes: + label: Description + description: | + A clear and concise description of what the issue is. + If the issue is visual in nature, consider posting a screenshot. + validations: + required: true + - type: textarea + attributes: + label: Reproduction + description: List the steps required to reproduce the issue. + placeholder: | + 1. + 2. + 3. + ... + validations: + required: true + - type: textarea + attributes: + label: Additional Context + description: Add any other additional context about the issue here. + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 09c9e76b193..501625bd2f3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,5 @@ +blank_issues_enabled: true contact_links: - - name: Report a Security Vulnerability - url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md - about: Please report security vulnerabilities privately so we can fix them before they are publicly disclosed. - name: Request a Feature url: https://discord.gg/rGvu9hKffJ about: Submit feature requests on our Discord server (https://discord.gg/rGvu9hKffJ). diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md deleted file mode 100644 index 9bc805b74e1..00000000000 --- a/.github/ISSUE_TEMPLATE/issue_report.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Report an Issue -about: "..." -title: '' -labels: '' -assignees: '' - ---- - -## Description - - -**Reproduction** - - -**Screenshots** - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/toolshed-feature-request.md b/.github/ISSUE_TEMPLATE/toolshed-feature-request.md deleted file mode 100644 index dae84c3e255..00000000000 --- a/.github/ISSUE_TEMPLATE/toolshed-feature-request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Toolshed feature request -about: Suggest a feature for Toolshed (for game admins/developers) -title: "[TOOLSHED REQUEST]" -labels: Toolshed -assignees: moonheart08 - ---- - -**Is your feature request related to a problem/bug? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the command you'd like** -A clear and concise description of what you want and what it should do. -If you're a technical user (i.e. programmer) including type signatures is helpful. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4a22f9b3e08..39610370d7f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,12 +26,6 @@ --> Изменения: - ## Технические детали diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 84bc8554377..00000000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Benchmarks -on: - workflow_dispatch: - schedule: - - cron: '0 8 * * *' - -concurrency: benchmarks - -jobs: - benchmark: - name: Run Benchmarks - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: 'recursive' - - name: Get Engine version - run: | - cd RobustToolbox - git fetch --depth=1 - echo "::set-output name=out::$(git rev-parse HEAD)" - id: engine_version - - name: Run script on centcomm - uses: appleboy/ssh-action@master - with: - host: centcomm.spacestation14.io - username: robust-benchmark-runner - key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }} - command_timeout: 100000m - script: | - mkdir benchmark_run_content_${{ github.sha }} - cd benchmark_run_content_${{ github.sha }} - git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive - cd repo_dir - git checkout ${{ github.sha }} - cd Content.Benchmarks - dotnet restore - export ROBUST_BENCHMARKS_ENABLE_SQL=1 - export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" - export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}" - export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}" - export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" - export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks" - export GITHUB_SHA="${{ github.sha }}" - dotnet run --filter '*' --configuration Release - cd ../../.. - rm -rf benchmark_run_content_${{ github.sha }} diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml deleted file mode 100644 index 1f010b72910..00000000000 --- a/.github/workflows/build-docfx.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build & Publish Docfx - -on: - schedule: - - cron: "0 0 * * 0" - -jobs: - docfx: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - - name: Setup submodule - run: | - git submodule update --init --recursive - - name: Pull engine updates - uses: space-wizards/submodule-dependency@v0.1.5 - - name: Update Engine Submodules - run: | - cd RobustToolbox/ - git submodule update --init --recursive - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 9.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build Project - run: dotnet build --no-restore - - - name: Build DocFX - uses: nikeee/docfx-action@v1.0.0 - with: - args: Content.Docfx/docfx.json - - - name: Publish Docfx Documentation on GitHub Pages - uses: maxheld83/ghpages@master - env: - BUILD_DIR: Content.Docfx/_content-site - GH_PAT: ${{ secrets.GH_PAT }} diff --git a/.github/workflows/build-map-renderer.yml b/.github/workflows/build-map-renderer.yml deleted file mode 100644 index c05352fd35e..00000000000 --- a/.github/workflows/build-map-renderer.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build & Test Map Renderer - -on: - push: - branches: [ develop ] - merge_group: - pull_request: - types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ develop ] - -jobs: - build: - if: github.event.pull_request.draft == false - strategy: - matrix: - os: [ubuntu-latest] - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout Master - uses: actions/checkout@v4.2.2 - - - name: Setup Submodule - run: | - git submodule update --init --recursive - - - name: Pull engine updates - uses: space-wizards/submodule-dependency@v0.1.5 - - - name: Update Engine Submodules - run: | - cd RobustToolbox/ - git submodule update --init --recursive - - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 9.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build Project - run: dotnet build Content.MapRenderer --configuration Release --no-restore /m - - - name: Run Map Renderer - run: dotnet run --project Content.MapRenderer Dev - - ci-success: - name: Build & Test Debug - needs: - - build - runs-on: ubuntu-latest - steps: - - name: CI succeeded - run: exit 0 diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml index 4e391b2aef9..ea959c755c2 100644 --- a/.github/workflows/build-test-debug.yml +++ b/.github/workflows/build-test-debug.yml @@ -2,11 +2,11 @@ name: Build & Test Debug on: push: - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] jobs: build: @@ -36,7 +36,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore @@ -45,13 +45,23 @@ jobs: run: dotnet build --configuration DebugOpt --no-restore /m - name: Run Content.Tests - run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 + shell: pwsh + run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" - name: Run Content.IntegrationTests 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 test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" + + - name: Archive NUnit3 test results. + if: always() + uses: actions/upload-artifact@v4 + with: + name: nunit3-results-${{ matrix.os }} + path: test_results/* + retention-days: 7 + compression-level: 9 ci-success: name: Build & Test Debug needs: diff --git a/.github/workflows/labeler-needsreview.yml b/.github/workflows/labeler-needsreview.yml deleted file mode 100644 index 8181d076b55..00000000000 --- a/.github/workflows/labeler-needsreview.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Labels: Review" - -on: - pull_request_target: - types: [review_requested, opened] - -jobs: - add_label: - runs-on: ubuntu-latest - steps: - - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: "Review requested" diff --git a/.github/workflows/labeler-review.yml b/.github/workflows/labeler-review.yml deleted file mode 100644 index 0adfee7d0e5..00000000000 --- a/.github/workflows/labeler-review.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "Labels: Approved" -on: - pull_request_review: - types: [submitted] -jobs: - add_label: - # Change the repository name after you've made sure the team name is correct for your fork! - if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }} - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: tspascoal/get-user-teams-membership@v3 - id: checkUserMember - with: - username: ${{ github.actor }} - team: "content-maintainers,junior-maintainers" - GITHUB_TOKEN: ${{ secrets.LABELER_PAT }} - - if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }} - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: "S: Approved" diff --git a/.github/workflows/labeler-stable.yml b/.github/workflows/labeler-stable.yml deleted file mode 100644 index f6fd2033a11..00000000000 --- a/.github/workflows/labeler-stable.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Labels: Branch stable" - -on: - pull_request_target: - types: - - opened - branches: - - 'stable' - -jobs: - add_label: - runs-on: ubuntu-latest - steps: - - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: "Branch: Stable" diff --git a/.github/workflows/labeler-staging.yml b/.github/workflows/labeler-staging.yml deleted file mode 100644 index b46a198aefb..00000000000 --- a/.github/workflows/labeler-staging.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Labels: Branch staging" - -on: - pull_request_target: - types: - - opened - branches: - - 'staging' - -jobs: - add_label: - runs-on: ubuntu-latest - steps: - - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: "Branch: Staging" diff --git a/.github/workflows/no-submodule-update.yml b/.github/workflows/no-submodule-update.yml deleted file mode 100644 index f2a9d72e375..00000000000 --- a/.github/workflows/no-submodule-update.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: No submodule update checker - -on: - pull_request: - paths: - - 'RobustToolbox' - -jobs: - this_aint_right: - name: Submodule update in pr found - runs-on: ubuntu-latest - steps: - - name: Fail - run: exit 1 diff --git a/.github/workflows/publish-testing.yml b/.github/workflows/publish-testing.yml deleted file mode 100644 index 7a792ed2df8..00000000000 --- a/.github/workflows/publish-testing.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Publish Testing - -concurrency: - group: publish-testing - cancel-in-progress: true - -on: - workflow_dispatch: - schedule: - - cron: '0 10 * * *' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3.6.0 - with: - submodules: 'recursive' - - name: Setup .NET Core - uses: actions/setup-dotnet@v3.2.0 - with: - dotnet-version: 9.0.x - - - name: Get Engine Tag - run: | - cd RobustToolbox - git fetch --depth=1 - - - name: Install dependencies - run: dotnet restore - - - name: Build Packaging - run: dotnet build Content.Packaging --configuration Release --no-restore /m - - - name: Package server - run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64 - - - name: Package client - run: dotnet run --project Content.Packaging client --no-wipe-release - - - name: Publish version - run: Tools/publish_multi_request.py --fork-id wizards-testing - env: - PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 3d54fef5300..00000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish - -concurrency: - group: publish - cancel-in-progress: true - -on: - workflow_dispatch: - # schedule: - # - cron: '0 10 * * *' - -jobs: - build: - runs-on: ubuntu-latest - - 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 - - - uses: actions/checkout@v4.2.2 - with: - submodules: 'recursive' - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 9.0.x - - - name: Get Engine Tag - run: | - cd RobustToolbox - git fetch --depth=1 - - - name: Install dependencies - run: dotnet restore - - - name: Build Packaging - run: dotnet build Content.Packaging --configuration Release --no-restore /m - - - name: Package server - run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64 - - - name: Package client - run: dotnet run --project Content.Packaging client --no-wipe-release - - - name: Publish version - run: Tools/publish_multi_request.py - env: - PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }} - - - name: Publish changelog (Discord) - continue-on-error: true - run: Tools/actions_changelogs_since_last_run.py - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }} - - - name: Publish changelog (RSS) - continue-on-error: true - run: Tools/actions_changelog_rss.py - env: - CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }} diff --git a/.github/workflows/submodule-checker.yml b/.github/workflows/submodule-checker.yml new file mode 100644 index 00000000000..27f8c20b033 --- /dev/null +++ b/.github/workflows/submodule-checker.yml @@ -0,0 +1,14 @@ +name: Submodule Update Checker + +on: + pull_request: + paths: + - 'RobustToolbox' + +jobs: + this_aint_right: + name: Please do not try to change the submodule in your PR + runs-on: ubuntu-latest + steps: + - name: Fail + run: exit 1 diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index 325d8d04d9c..4a86fa76083 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -2,7 +2,7 @@ on: push: - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] paths: - '**.cs' - '**.csproj' @@ -16,7 +16,7 @@ on: merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] paths: - '**.cs' - '**.csproj' @@ -29,7 +29,7 @@ on: jobs: build: name: Test Packaging - if: github.actor != 'PJBot' && github.event.pull_request.draft == false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: @@ -48,10 +48,11 @@ jobs: cd RobustToolbox/ git submodule update --init --recursive - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 9.0.x + # ubuntu-latest has .NET 10 + # - name: Setup .NET Core + # uses: actions/setup-dotnet@v4.1.0 + # with: + # dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore @@ -60,7 +61,13 @@ jobs: run: dotnet build Content.Packaging --configuration Release --no-restore /m - name: Package server - run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64 + run: dotnet run --project Content.Packaging server --log-build --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64 - name: Package client - run: dotnet run --project Content.Packaging client --no-wipe-release + run: dotnet run --project Content.Packaging client --log-build --no-wipe-release + + - uses: actions/upload-artifact@v4 + with: + name: binlogs + path: release/*.binlog + retention-days: 7 diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml deleted file mode 100644 index eb609556d43..00000000000 --- a/.github/workflows/update-credits.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Update Contrib and Patreons in credits - -on: - workflow_dispatch: - schedule: - - cron: 0 0 * * 0 - -jobs: - get_credits: - runs-on: ubuntu-latest - # Hey there fork dev! If you like to include your own contributors in this then you can probably just change this to your own repo - # Do this in dump_github_contributors.ps1 too into your own repo - if: github.repository == 'space-wizards/space-station-14' - - steps: - - uses: actions/checkout@v4.2.2 - with: - ref: master - - - name: Get this week's Contributors - shell: pwsh - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt - - # TODO - #- name: Get this week's Patreons - # run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml - - # MAKE SURE YOU ENABLED "Allow GitHub Actions to create and approve pull requests" IN YOUR ACTIONS, OTHERWISE IT WILL MOST LIKELY FAIL - - - # For this you can use a pat token of an account with direct push access to the repo if you have protected branches. - # Uncomment this and comment the other line if you do this. - # https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches - - #- name: Commit new credit files - # uses: stefanzweifel/git-auto-commit-action@v4 - # with: - # commit_message: Update Credits - # commit_author: PJBot - - # This will make a PR - - name: Set current date as env variable - run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v5 - with: - commit-message: Update Credits - title: Update Credits - body: This is an automated Pull Request. This PR updates the github contributors in the credits section. - author: PJBot - branch: automated/credits-${{env.NOW}} diff --git a/.github/workflows/validate-rsis.yml b/.github/workflows/validate-rsis.yml index a8100e5506b..15d397525d2 100644 --- a/.github/workflows/validate-rsis.yml +++ b/.github/workflows/validate-rsis.yml @@ -2,7 +2,7 @@ name: RSI Validator on: push: - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] merge_group: pull_request: paths: diff --git a/.github/workflows/validate_mapfiles.yml b/.github/workflows/validate_mapfiles.yml deleted file mode 100644 index 53fa3b4ae45..00000000000 --- a/.github/workflows/validate_mapfiles.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Map file schema validator -on: - push: - branches: [ master, staging, stable ] - merge_group: - pull_request: - types: [ opened, reopened, synchronize, ready_for_review ] - -jobs: - yaml-schema-validation: - name: YAML map schema validator - if: github.actor != 'PJBot' && github.event.pull_request.draft == false - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - - name: Setup Submodule - run: git submodule update --init - - name: Pull engine updates - uses: space-wizards/submodule-dependency@v0.1.5 - - uses: PaulRitter/yaml-schema-validator@v1 - with: - schema: RobustToolbox/Schemas/mapfile.yml - path_pattern: .*Resources/Maps/.* - validators_path: RobustToolbox/Schemas/mapfile_validators.py - validators_requirements: RobustToolbox/Schemas/mapfile_requirements.txt diff --git a/.github/workflows/yaml-linter.yml b/.github/workflows/yaml-linter.yml index c10549bcb46..00a233b28b4 100644 --- a/.github/workflows/yaml-linter.yml +++ b/.github/workflows/yaml-linter.yml @@ -2,7 +2,7 @@ name: YAML Linter on: push: - branches: [ master, staging, stable ] + branches: [ develop, event, aquila, eridani ] merge_group: pull_request: types: [ opened, reopened, synchronize, ready_for_review ] @@ -10,7 +10,7 @@ on: jobs: build: name: YAML Linter - if: github.actor != 'PJBot' && github.event.pull_request.draft == false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 @@ -26,7 +26,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/.gitignore b/.gitignore index a28ee6dbcab..2443fab3802 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# MSbuild binlog files +*.binlog + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/.gitmodules b/.gitmodules index 4e3d6596949..49c2598ba12 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "RobustToolbox"] path = RobustToolbox url = https://github.com/space-wizards/RobustToolbox - branch = 267.1.0 + branch = 277.0.0 diff --git a/BuildChecker/BuildChecker.csproj.lscache b/BuildChecker/BuildChecker.csproj.lscache new file mode 100644 index 00000000000..0f958d272cf --- /dev/null +++ b/BuildChecker/BuildChecker.csproj.lscache @@ -0,0 +1,71 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=BuildChecker +CommandLineArgsForDesignTimeEvaluation=-langversion:7.3 -define:TRACE +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=7.3 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=BuildChecker +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETFramework +TargetPath=bin/Debug/net4.7.2/BuildChecker.dll +TargetRefPath= +TemporaryDependencyNodeTargetIdentifier=net4.7.2 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:4 +/define:TRACE;DEBUG;NETFRAMEWORK;NET472;NET20_OR_GREATER;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER +/highentropyva+ +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\net4.7.2\BuildChecker.dll +/subsystemversion:6.00 +/target:library +/warnaserror- +/utf8output +/deterministic+ +/langversion:7.3 +/warnaserror+:NU1605 + +[sourceFiles] +obj/Debug/net4.7.2/BuildChecker.AssemblyInfo.cs + +[metadataReferences] +/microsoft.netframework.referenceassemblies.net472/1.0.3/build/.NETFramework/v4.7.2/ + mscorlib.dll + System.Core.dll + System.Data.dll + System.dll + System.Drawing.dll + System.IO.Compression.FileSystem.dll + System.Numerics.dll + System.Runtime.Serialization.dll + System.Xml.dll + System.Xml.Linq.dll + +[analyzerConfigFiles] +obj/Debug/net4.7.2/BuildChecker.GeneratedMSBuildEditorConfig.editorconfig diff --git a/BuildChecker/git_helper.py b/BuildChecker/git_helper.py index 66d2463669d..4e42b1e70e3 100644 --- a/BuildChecker/git_helper.py +++ b/BuildChecker/git_helper.py @@ -11,9 +11,9 @@ from pathlib import Path from typing import List -SOLUTION_PATH = Path("..") / "SpaceStation14.sln" +SOLUTION_PATH = Path("..") / "SpaceStation14.slnx" # If this doesn't match the saved version we overwrite them all. -CURRENT_HOOKS_VERSION = "3" +CURRENT_HOOKS_VERSION = "4" QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet" diff --git a/BuildChecker/hooks/post-checkout b/BuildChecker/hooks/post-checkout index ee4309de1da..1b91112ff0d 100755 --- a/BuildChecker/hooks/post-checkout +++ b/BuildChecker/hooks/post-checkout @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash gitroot=$(git rev-parse --show-toplevel) diff --git a/BuildChecker/hooks/post-merge b/BuildChecker/hooks/post-merge index 5cf3d91120d..864a9cff504 100755 --- a/BuildChecker/hooks/post-merge +++ b/BuildChecker/hooks/post-merge @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Just call post-checkout since it does the same thing. gitroot=$(git rev-parse --git-path hooks) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b2aeb6197ac..f8f22fb3f89 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,7 +8,7 @@ This isn’t an exhaustive list of things that you can’t do. Rather, take it i This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here. -If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com). +If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [support@spacestation14.com](mailto:support@spacestation14.com). - **Be friendly and patient.** - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..da081074c4a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Space Station 14 Contributing Guidelines + +Thanks for contributing to Space Station 14. +When contributing, be sure to follow our [codebase conventions](https://docs.spacestation14.com/en/general-development/codebase-info/codebase-organization.html) and [PR guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). + +Following these guidelines helps us increase review turnaround time, so be sure to review the linked documents in full. + +The last major guidelines update was on **December 6th, 2025**. + +### Why is this here? +We put this here so that GitHub will notify you when submitting a pull request that the PR guidelines have changed, if you haven't read the latest version. diff --git a/Content.Benchmarks/ComponentQueryBenchmark.cs b/Content.Benchmarks/ComponentQueryBenchmark.cs index bfe367790a6..c9aebf7ba36 100644 --- a/Content.Benchmarks/ComponentQueryBenchmark.cs +++ b/Content.Benchmarks/ComponentQueryBenchmark.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -44,7 +45,7 @@ public void Setup() ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(QueryBenchSystem).Assembly); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); _itemQuery = _entMan.GetEntityQuery(); diff --git a/Content.Benchmarks/Content.Benchmarks.csproj b/Content.Benchmarks/Content.Benchmarks.csproj index c3b60a1c69d..dd0b760f4ad 100644 --- a/Content.Benchmarks/Content.Benchmarks.csproj +++ b/Content.Benchmarks/Content.Benchmarks.csproj @@ -1,17 +1,21 @@  - - $(TargetFramework) ..\bin\Content.Benchmarks\ - false false Exe true - 12 + false + disable + $(DefineConstants);ALLOW_BAD_PRACTICES + + + + + @@ -19,10 +23,12 @@ - - - - - + + + + + + + diff --git a/Content.Benchmarks/Content.Benchmarks.csproj.lscache b/Content.Benchmarks/Content.Benchmarks.csproj.lscache new file mode 100644 index 00000000000..ed732dae864 --- /dev/null +++ b/Content.Benchmarks/Content.Benchmarks.csproj.lscache @@ -0,0 +1,385 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Benchmarks +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;ALLOW_BAD_PRACTICES +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Benchmarks +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=../bin/Content.Benchmarks/Content.Benchmarks.dll +TargetRefPath=obj/Debug/ref/Content.Benchmarks.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe+ +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;ALLOW_BAD_PRACTICES;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:disable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.Benchmarks.dll +/refout:obj\Debug\refint\Content.Benchmarks.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +ColorInterpolateBenchmark.cs +ComponentFetchBenchmark.cs +ComponentQueryBenchmark.cs +DeltaPressureBenchmark.cs +DependencyInjectBenchmark.cs +DestructibleBenchmark.cs +DeviceNetworkingBenchmark.cs +DynamicTreeBenchmark.cs +EntityFetchBenchmark.cs +EntityManagerGetAllComponents.cs +GasReactionBenchmark.cs +GlobalUsings.cs +HeatCapacityBenchmark.cs +MapLoadBenchmark.cs +NetSerializerIntBenchmark.cs +NetSerializerStringBenchmark.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Benchmarks.AssemblyInfo.cs +Program.cs +PvsBenchmark.cs +RaiseEventBenchmark.cs +SpawnEquipDeleteBenchmark.cs +StereoToMonoBenchmark.cs + +[metadataReferences] +../ + Content.Client/obj/Debug/ref/Content.Client.dll + Content.IntegrationTests/obj/Debug/ref/Content.IntegrationTests.dll + Content.Packaging/obj/Debug/net10.0/ref/Content.Packaging.dll + Content.Server.Database/obj/Debug/ref/Content.Server.Database.dll + Content.Server/obj/Debug/ref/Content.Server.dll + Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll + Content.Shared/obj/Debug/ref/Content.Shared.dll + Content.Tests/obj/Debug/ref/Content.Tests.dll + RobustToolbox/ + Avalonia.Base/obj/Debug/net10.0/ref/Avalonia.Base.dll + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Benchmarks/obj/Debug/ref/Robust.Benchmarks.dll + Robust.Client/obj/Debug/ref/Robust.Client.dll + Robust.LoaderApi/Robust.LoaderApi/obj/Debug/net10.0/ref/Robust.LoaderApi.dll + Robust.Packaging/obj/Debug/net10.0/ref/Robust.Packaging.dll + Robust.Server.Testing/obj/Debug/net10.0/ref/Robust.Server.Testing.dll + Robust.Server/obj/Debug/ref/Robust.Server.dll + Robust.Shared.Maths.Testing/obj/Debug/net10.0/ref/Robust.Shared.Maths.Testing.dll + Robust.Shared.Maths.Tests/obj/Debug/net10.0/ref/Robust.Shared.Maths.Tests.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared.Scripting/obj/Debug/ref/Robust.Shared.Scripting.dll + Robust.Shared.Testing/obj/Debug/net10.0/ref/Robust.Shared.Testing.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll + Robust.UnitTesting/obj/Debug/net10.0/ref/Robust.UnitTesting.dll + Robust.Xaml/bin/Debug/netstandard2.0/Robust.Xaml.dll + XamlX/src/ + XamlX.IL.Cecil/bin/Debug/netstandard2.0/XamlX.IL.Cecil.dll + XamlX/bin/Debug/netstandard2.0/XamlX.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + benchmarkdotnet.annotations/0.15.8/lib/netstandard2.0/BenchmarkDotNet.Annotations.dll + benchmarkdotnet/0.15.8/lib/net8.0/BenchmarkDotNet.dll + castle.core/5.1.1/lib/net6.0/Castle.Core.dll + commandlineparser/2.9.1/lib/netstandard2.0/CommandLine.dll + gee.external.capstone/2.3.0/lib/net7.0/Gee.External.Capstone.dll + iced/1.21.0/lib/netstandard2.1/Iced.dll + microsoft.applicationinsights/2.23.0/lib/netstandard2.0/Microsoft.ApplicationInsights.dll + microsoft.codeanalysis.common/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.dll + microsoft.codeanalysis.csharp/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.CSharp.dll + microsoft.codecoverage/18.0.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll + microsoft.data.sqlite.core/10.0.6/lib/net8.0/Microsoft.Data.Sqlite.dll + microsoft.diagnostics.netcore.client/0.2.510501/lib/net6.0/Microsoft.Diagnostics.NETCore.Client.dll + microsoft.diagnostics.runtime/3.1.512801/lib/net6.0/Microsoft.Diagnostics.Runtime.dll + microsoft.diagnostics.tracing.traceevent/3.1.21/lib/netstandard2.0/ + Dia2Lib.dll + Microsoft.Diagnostics.FastSerialization.dll + Microsoft.Diagnostics.Tracing.TraceEvent.dll + TraceReloggerLib.dll + microsoft.dotnet.platformabstractions/3.1.6/lib/netstandard2.0/Microsoft.DotNet.PlatformAbstractions.dll + microsoft.entityframeworkcore.abstractions/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll + microsoft.entityframeworkcore.relational/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll + microsoft.entityframeworkcore.sqlite.core/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Sqlite.dll + microsoft.entityframeworkcore/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.dll + microsoft.extensions.caching.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + microsoft.extensions.caching.memory/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Memory.dll + microsoft.extensions.configuration.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll + microsoft.extensions.dependencyinjection.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll + microsoft.extensions.dependencyinjection/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll + microsoft.extensions.dependencymodel/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.extensions.http/3.1.0/lib/netcoreapp3.1/Microsoft.Extensions.Http.dll + microsoft.extensions.logging.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll + microsoft.extensions.logging/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.dll + microsoft.extensions.objectpool/10.0.0/lib/net10.0/Microsoft.Extensions.ObjectPool.dll + microsoft.extensions.options/10.0.6/lib/net10.0/Microsoft.Extensions.Options.dll + microsoft.extensions.primitives/10.0.6/lib/net10.0/Microsoft.Extensions.Primitives.dll + microsoft.testing.extensions.telemetry/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.Telemetry.dll + microsoft.testing.extensions.trxreport.abstractions/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.TrxReport.Abstractions.dll + microsoft.testing.extensions.vstestbridge/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.VSTestBridge.dll + microsoft.testing.platform.msbuild/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.MSBuild.dll + microsoft.testing.platform/1.9.0/lib/net9.0/Microsoft.Testing.Platform.dll + microsoft.testplatform.adapterutilities/17.13.0/lib/net9.0/Microsoft.TestPlatform.AdapterUtilities.dll + microsoft.testplatform.testhost/18.0.1/lib/net8.0/ + Microsoft.TestPlatform.CommunicationUtilities.dll + Microsoft.TestPlatform.CoreUtilities.dll + Microsoft.TestPlatform.CrossPlatEngine.dll + Microsoft.TestPlatform.PlatformAbstractions.dll + Microsoft.TestPlatform.Utilities.dll + Microsoft.VisualStudio.TestPlatform.Common.dll + Microsoft.VisualStudio.TestPlatform.ObjectModel.dll + testhost.dll + moq/4.20.72/lib/net6.0/Moq.dll + netcord/1.0.0-alpha.388/lib/net9.0/NetCord.dll + nett/0.15.0/lib/netstandard2.0/Nett.dll + newtonsoft.json/13.0.4/lib/net6.0/Newtonsoft.Json.dll + npgsql.entityframeworkcore.postgresql/10.0.0/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll + npgsql/10.0.0/lib/net10.0/Npgsql.dll + nunit/4.4.0/lib/net8.0/ + nunit.framework.dll + nunit.framework.legacy.dll + perfolizer/0.6.1/lib/net6.0/Perfolizer.dll + pidgin/3.5.1/lib/net7.0/Pidgin.dll + pragmastat/3.2.4/lib/net6.0/Pragmastat.dll + prometheus-net/8.2.1/lib/net7.0/Prometheus.NetStandard.dll + robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + sixlabors.imagesharp/3.1.12/lib/net6.0/SixLabors.ImageSharp.dll + sqlitepclraw.config.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.batteries_v2.dll + sqlitepclraw.core/3.0.2/lib/netstandard2.0/SQLitePCLRaw.core.dll + sqlitepclraw.provider.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll + system.codedom/10.0.0/lib/net10.0/System.CodeDom.dll + system.diagnostics.eventlog/9.0.0/lib/net9.0/System.Diagnostics.EventLog.dll + system.management/10.0.0/lib/net10.0/System.Management.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll +/ + benchmarkdotnet.annotations/0.15.8/analyzers/dotnet/roslyn4.8/cs/BenchmarkDotNet.Analyzers.dll + microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll + microsoft.entityframeworkcore.analyzers/10.0.6/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll + microsoft.extensions.logging.abstractions/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll + microsoft.extensions.options/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll + microsoft.net.illink.tasks/10.0.0/analyzers/dotnet/cs/ + ILLink.CodeFixProvider.dll + ILLink.RoslynAnalyzer.dll + nunit.analyzers/4.11.2/analyzers/dotnet/cs/ + nunit.analyzers.codefixes.dll + nunit.analyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.Benchmarks.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs index b31b3ed1a24..dac79e03760 100644 --- a/Content.Benchmarks/DeltaPressureBenchmark.cs +++ b/Content.Benchmarks/DeltaPressureBenchmark.cs @@ -1,11 +1,11 @@ +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Diagnosers; using Content.IntegrationTests; using Content.IntegrationTests.Pair; -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.CCVar; using Robust.Shared; using Robust.Shared.Analyzers; @@ -69,7 +69,7 @@ public async Task SetupAsync() { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; var mapdata = await _pair.CreateTestMap(); diff --git a/Content.Benchmarks/DestructibleBenchmark.cs b/Content.Benchmarks/DestructibleBenchmark.cs new file mode 100644 index 00000000000..58c834c0aef --- /dev/null +++ b/Content.Benchmarks/DestructibleBenchmark.cs @@ -0,0 +1,192 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Server.Destructible; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Maps; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Benchmarks; + +[Virtual] +[GcServer(true)] +[MemoryDiagnoser] +public class DestructibleBenchmark +{ + /// + /// Number of destructible entities per prototype to spawn with a . + /// + [Params(1, 10, 100, 1000, 5000)] + public int EntityCount; + + /// + /// Amount of blunt damage we do to each entity. + /// + [Params(10000)] + public FixedPoint2 DamageAmount; + + [Params("Blunt")] + public ProtoId DamageType; + + private static readonly EntProtoId WindowProtoId = "Window"; + private static readonly EntProtoId WallProtoId = "WallReinforced"; + private static readonly EntProtoId HumanProtoId = "MobHuman"; + + private static readonly ProtoId TileRef = "Plating"; + + private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId]; + + private readonly List> _damageables = new(); + private readonly List> _destructbiles = new(); + + private TestMapData _currentMapData = default!; + + private DamageSpecifier _damage; + + private TestPair _pair = default!; + private IEntityManager _entMan = default!; + private IPrototypeManager _protoMan = default!; + private IRobustRandom _random = default!; + private ITileDefinitionManager _tileDefMan = default!; + private DamageableSystem _damageable = default!; + private DestructibleSystem _destructible = default!; + private SharedMapSystem _map = default!; + + [GlobalSetup] + public async Task SetupAsync() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); + var server = _pair.Server; + + _entMan = server.ResolveDependency(); + _protoMan = server.ResolveDependency(); + _random = server.ResolveDependency(); + _tileDefMan = server.ResolveDependency(); + _damageable = _entMan.System(); + _destructible = _entMan.System(); + _map = _entMan.System(); + + if (!_protoMan.Resolve(DamageType, out var type)) + return; + + _damage = new DamageSpecifier(type, DamageAmount); + + _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking. + } + + [IterationSetup] + public void IterationSetup() + { + var plating = _tileDefMan[TileRef].TileId; + var server = _pair.Server; + _currentMapData = _pair.CreateTestMap().GetAwaiter().GetResult(); + + // We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system. + // Needed for managing the performance of destructive effects and damage application. + server.WaitPost(() => + { + // Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario... + for (var x = 0; x < EntityCount; x++) + { + for (var y = 0; y < _prototypes.Length; y++) + { + _map.SetTile(_currentMapData.Grid, _currentMapData.Grid, new Vector2i(x, y), new Tile(plating)); + } + } + + for (var x = 0; x < EntityCount; x++) + { + var y = 0; + foreach (var protoId in _prototypes) + { + var coords = new EntityCoordinates(_currentMapData.Grid, x + 0.5f, y + 0.5f); + _entMan.SpawnEntity(protoId, coords); + y++; + } + } + + var query = _entMan.EntityQueryEnumerator(); + + _destructbiles.EnsureCapacity(EntityCount); + _damageables.EnsureCapacity(EntityCount); + + while (query.MoveNext(out var uid, out var damageable, out var destructible)) + { + _damageables.Add((uid, damageable)); + _destructbiles.Add((uid, damageable, destructible)); + } + }) + .GetAwaiter() + .GetResult(); + } + + [Benchmark] + public async Task PerformDealDamage() + { + await _pair.Server.WaitPost(() => + { + _damageable.ApplyDamageToAllEntities(_damageables, _damage); + }); + } + + [Benchmark] + public async Task PerformTestTriggers() + { + await _pair.Server.WaitPost(() => + { + _destructible.TestAllTriggers(_destructbiles); + }); + } + + [Benchmark] + public async Task PerformTestBehaviors() + { + await _pair.Server.WaitPost(() => + { + _destructible.TestAllBehaviors(_destructbiles); + }); + } + + [IterationCleanup] + public void IterationCleanupAsync() + { + // We need to nuke the entire map and respawn everything as some destructible effects + // spawn entities and whatnot. + _pair.Server.WaitPost(() => + { + _map.QueueDeleteMap(_currentMapData.MapId); + }) + .Wait(); + + // Deletion of entities is often queued (QueueDel) which must be processed by running ticks + // or else it will grow infinitely and leak memory. + _pair.Server.WaitRunTicks(2) + .GetAwaiter() + .GetResult(); + + _destructbiles.Clear(); + _damageables.Clear(); + } + + [GlobalCleanup] + public async Task CleanupAsync() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } +} diff --git a/Content.Benchmarks/DeviceNetworkingBenchmark.cs b/Content.Benchmarks/DeviceNetworkingBenchmark.cs index bb2a22312ea..fcde4feb646 100644 --- a/Content.Benchmarks/DeviceNetworkingBenchmark.cs +++ b/Content.Benchmarks/DeviceNetworkingBenchmark.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; @@ -60,7 +61,7 @@ public async Task SetupAsync() { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; await server.WaitPost(() => diff --git a/Content.Benchmarks/GasReactionBenchmark.cs b/Content.Benchmarks/GasReactionBenchmark.cs new file mode 100644 index 00000000000..f2988693663 --- /dev/null +++ b/Content.Benchmarks/GasReactionBenchmark.cs @@ -0,0 +1,254 @@ +using System.IO; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Server.Atmos; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Reactions; +using Content.Shared.Atmos; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Benchmarks; + +/// +/// Benchmarks the performance of different gas reactions. +/// Tests each reaction type with realistic gas mixtures to measure computational cost. +/// +[Virtual] +[GcServer(true)] +[MemoryDiagnoser] +public class GasReactionBenchmark +{ + private const int Iterations = 1000; + private TestPair _pair = default!; + private AtmosphereSystem _atmosphereSystem = default!; + + // Grid and tile for reactions that need a holder + private EntityUid _testGrid = default!; + private TileAtmosphere _testTile = default!; + // Reaction instances + private PlasmaFireReaction _plasmaFireReaction = default!; + private TritiumFireReaction _tritiumFireReaction = default!; + private FrezonProductionReaction _frezonProductionReaction = default!; + private FrezonCoolantReaction _frezonCoolantReaction = default!; + private AmmoniaOxygenReaction _ammoniaOxygenReaction = default!; + private N2ODecompositionReaction _n2oDecompositionReaction = default!; + private WaterVaporReaction _waterVaporReaction = default!; + // Gas mixtures for each reaction type + private GasMixture _plasmaFireMixture = default!; + private GasMixture _tritiumFireMixture = default!; + private GasMixture _frezonProductionMixture = default!; + private GasMixture _frezonCoolantMixture = default!; + private GasMixture _ammoniaOxygenMixture = default!; + private GasMixture _n2oDecompositionMixture = default!; + private GasMixture _waterVaporMixture = default!; + + [GlobalSetup] + public async Task SetupAsync() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); + var server = _pair.Server; + + // Create test map and grid + var mapData = await _pair.CreateTestMap(); + _testGrid = mapData.Grid; + + await server.WaitPost(() => + { + var entMan = server.ResolveDependency(); + _atmosphereSystem = entMan.System(); + + _plasmaFireReaction = new PlasmaFireReaction(); + _tritiumFireReaction = new TritiumFireReaction(); + _frezonProductionReaction = new FrezonProductionReaction(); + _frezonCoolantReaction = new FrezonCoolantReaction(); + _ammoniaOxygenReaction = new AmmoniaOxygenReaction(); + _n2oDecompositionReaction = new N2ODecompositionReaction(); + _waterVaporReaction = new WaterVaporReaction(); + + SetupGasMixtures(); + SetupTile(); + }); + } + + private void SetupGasMixtures() + { + // Plasma Fire: Plasma + Oxygen at high temperature + // Temperature must be > PlasmaMinimumBurnTemperature for reaction to occur + _plasmaFireMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.PlasmaMinimumBurnTemperature + 100f // ~673K + }; + _plasmaFireMixture.AdjustMoles(Gas.Plasma, 20f); + _plasmaFireMixture.AdjustMoles(Gas.Oxygen, 100f); + + // Tritium Fire: Tritium + Oxygen at high temperature + // Temperature must be > FireMinimumTemperatureToExist for reaction to occur + _tritiumFireMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.FireMinimumTemperatureToExist + 100f // ~473K + }; + _tritiumFireMixture.AdjustMoles(Gas.Tritium, 20f); + _tritiumFireMixture.AdjustMoles(Gas.Oxygen, 100f); + + // Frezon Production: Oxygen + Tritium + Nitrogen catalyst + // Optimal temperature for efficiency (80% of max efficiency temp) + _frezonProductionMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.FrezonProductionMaxEfficiencyTemperature * 0.8f // ~48K + }; + _frezonProductionMixture.AdjustMoles(Gas.Oxygen, 50f); + _frezonProductionMixture.AdjustMoles(Gas.Tritium, 50f); + _frezonProductionMixture.AdjustMoles(Gas.Nitrogen, 10f); + + // Frezon Coolant: Frezon + Nitrogen + // Temperature must be > FrezonCoolLowerTemperature (23.15K) for reaction to occur + _frezonCoolantMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.T20C + 50f // ~343K + }; + _frezonCoolantMixture.AdjustMoles(Gas.Frezon, 30f); + _frezonCoolantMixture.AdjustMoles(Gas.Nitrogen, 100f); + + // Ammonia + Oxygen reaction (concentration-dependent, no temp requirement) + _ammoniaOxygenMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.T20C + 100f // ~393K + }; + _ammoniaOxygenMixture.AdjustMoles(Gas.Ammonia, 40f); + _ammoniaOxygenMixture.AdjustMoles(Gas.Oxygen, 40f); + + // N2O Decomposition (no temperature requirement, just needs N2O moles) + _n2oDecompositionMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.T20C + 100f // ~393K + }; + _n2oDecompositionMixture.AdjustMoles(Gas.NitrousOxide, 100f); + + // Water Vapor - needs water vapor to condense + _waterVaporMixture = new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.T20C + }; + _waterVaporMixture.AdjustMoles(Gas.WaterVapor, 50f); + } + + private void SetupTile() + { + // Create a tile atmosphere to use as holder for all reactions + var testIndices = new Vector2i(0, 0); + _testTile = new TileAtmosphere(_testGrid, testIndices, new GasMixture(Atmospherics.CellVolume) + { + Temperature = Atmospherics.T20C + }); + } + + private static GasMixture CloneMixture(GasMixture original) + { + return new GasMixture(original); + } + + [Benchmark] + public async Task PlasmaFireReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_plasmaFireMixture); + _plasmaFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task TritiumFireReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_tritiumFireMixture); + _tritiumFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task FrezonProductionReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_frezonProductionMixture); + _frezonProductionReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task FrezonCoolantReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_frezonCoolantMixture); + _frezonCoolantReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task AmmoniaOxygenReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_ammoniaOxygenMixture); + _ammoniaOxygenReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task N2ODecompositionReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_n2oDecompositionMixture); + _n2oDecompositionReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [Benchmark] + public async Task WaterVaporReaction() + { + await _pair.Server.WaitPost(() => + { + for (var i = 0; i < Iterations; i++) + { + var mixture = CloneMixture(_waterVaporMixture); + _waterVaporReaction.React(mixture, _testTile, _atmosphereSystem, 1f); + } + }); + } + + [GlobalCleanup] + public async Task CleanupAsync() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } +} diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs new file mode 100644 index 00000000000..18366306d7a --- /dev/null +++ b/Content.Benchmarks/HeatCapacityBenchmark.cs @@ -0,0 +1,84 @@ +using System.IO; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Content.IntegrationTests; +using Content.IntegrationTests.Pair; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; + +namespace Content.Benchmarks; + +[Virtual] +[GcServer(true)] +[MemoryDiagnoser] +public class HeatCapacityBenchmark +{ + private TestPair _pair = default!; + private IEntityManager _sEntMan = default!; + private IEntityManager _cEntMan = default!; + private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!; + private AtmosphereSystem _sAtmos = default!; + private GasMixture _mix; + + [GlobalSetup] + public async Task SetupAsync() + { + ProgramShared.PathOffset = "../../../../"; + PoolManager.Startup(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); + await _pair.Connect(); + _cEntMan = _pair.Client.ResolveDependency(); + _sEntMan = _pair.Server.ResolveDependency(); + _cAtmos = _cEntMan.System(); + _sAtmos = _sEntMan.System(); + + const float volume = 2500f; + const float temperature = 293.15f; + + const float o2 = 12.3f; + const float n2 = 45.6f; + const float co2 = 0.42f; + const float plasma = 0.05f; + + _mix = new GasMixture(volume) { Temperature = temperature }; + + _mix.AdjustMoles(Gas.Oxygen, o2); + _mix.AdjustMoles(Gas.Nitrogen, n2); + _mix.AdjustMoles(Gas.CarbonDioxide, co2); + _mix.AdjustMoles(Gas.Plasma, plasma); + } + + [Benchmark] + public async Task ClientHeatCapacityBenchmark() + { + await _pair.Client.WaitPost(delegate + { + for (var i = 0; i < 10000; i++) + { + _cAtmos.GetHeatCapacity(_mix, applyScaling: true); + } + }); + } + + [Benchmark] + public async Task ServerHeatCapacityBenchmark() + { + await _pair.Server.WaitPost(delegate + { + for (var i = 0; i < 10000; i++) + { + _sAtmos.GetHeatCapacity(_mix, applyScaling: true); + } + }); + } + + [GlobalCleanup] + public async Task CleanupAsync() + { + await _pair.DisposeAsync(); + PoolManager.Shutdown(); + } +} diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index 3d527953b83..9ea95c84b64 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; -using Content.Server.Maps; +using Content.Shared.Maps; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.EntitySerialization.Systems; @@ -29,7 +30,7 @@ public void Setup() ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); var server = _pair.Server; Paths = server.ResolveDependency() diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index 51a013539e0..af1ec8d9efc 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -50,7 +51,7 @@ public void Setup() #endif PoolManager.Startup(); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); _pair.Server.CfgMan.SetCVar(CVars.NetPVS, true); _pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0); diff --git a/Content.Benchmarks/RaiseEventBenchmark.cs b/Content.Benchmarks/RaiseEventBenchmark.cs index 4c4e0d9125a..99e032d0b58 100644 --- a/Content.Benchmarks/RaiseEventBenchmark.cs +++ b/Content.Benchmarks/RaiseEventBenchmark.cs @@ -1,4 +1,5 @@ #nullable enable +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -21,8 +22,10 @@ public void Setup() { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(typeof(BenchSystem).Assembly); - _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); + _pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult(); var entMan = _pair.Server.EntMan; + var fact = _pair.Server.ResolveDependency(); + var bus = (EntityEventBus)entMan.EventBus; _sys = entMan.System(); _pair.Server.WaitPost(() => @@ -30,6 +33,8 @@ public void Setup() var uid = entMan.Spawn(); _sys.Ent = new(uid, entMan.GetComponent(uid)); _sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp); + _sys.NetId = fact.GetRegistration().NetID!.Value; + _sys.EvSubs = bus.GetNetCompEventHandlers(); }) .GetAwaiter() .GetResult(); @@ -60,6 +65,12 @@ public int RaiseICompEvent() return _sys.RaiseICompEvent(); } + [Benchmark] + public int RaiseNetEvent() + { + return _sys.RaiseNetIdEvent(); + } + [Benchmark] public int RaiseCSharpEvent() { @@ -74,6 +85,8 @@ public sealed class BenchSystem : EntitySystem public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev); public event EntityEventHandler? OnCSharpEvent; + public ushort NetId; + internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!; public override void Initialize() { @@ -92,7 +105,7 @@ public int RaiseEvent() public int RaiseCompEvent() { var ev = new BenchEv(); - EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev); + RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev); return ev.N; } @@ -100,7 +113,16 @@ public int RaiseICompEvent() { // Raise with an IComponent instead of concrete type var ev = new BenchEv(); - EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev); + RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev); + return ev.N; + } + + public int RaiseNetIdEvent() + { + // Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events) + var ev = new BenchEv(); + ref var unitEv = ref Unsafe.As(ref ev); + EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv); return ev.N; } @@ -118,6 +140,7 @@ private void OnEvent(EntityUid uid, TransformComponent component, ref BenchEv ar } [ByRefEvent] + [ComponentEvent(Exclusive = false)] public struct BenchEv { public int N; diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs index 9b878eac40c..b6d5427480c 100644 --- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs +++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; @@ -36,7 +37,7 @@ public async Task SetupAsync() { ProgramShared.PathOffset = "../../../../"; PoolManager.Startup(); - _pair = await PoolManager.GetServerClient(); + _pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)); var server = _pair.Server; var mapData = await _pair.CreateTestMap(); diff --git a/Content.Client/Access/Systems/JobStatusSystem.cs b/Content.Client/Access/Systems/JobStatusSystem.cs new file mode 100644 index 00000000000..8327a6c198d --- /dev/null +++ b/Content.Client/Access/Systems/JobStatusSystem.cs @@ -0,0 +1,39 @@ +using Content.Client.Overlays; +using Content.Shared.Access.Systems; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Access.Systems; + +public sealed class JobStatusSystem : SharedJobStatusSystem +{ + [Dependency] private readonly ShowJobIconsSystem _showJobIcons = default!; + [Dependency] private readonly ShowCrewIconsSystem _showCrewIcons = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private static readonly ProtoId CrewBorderIcon = "CrewBorderIcon"; + private static readonly ProtoId CrewUncertainBorderIcon = "CrewUncertainBorderIcon"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetStatusIconsEvent); + } + + // show the status icons if the player has the correponding HUDs + private void OnGetStatusIconsEvent(Entity ent, ref GetStatusIconsEvent ev) + { + if (_showJobIcons.IsActive && ent.Comp.JobStatusIcon != null) + ev.StatusIcons.Add(_prototype.Index(ent.Comp.JobStatusIcon)); + + if (_showCrewIcons.IsActive) + { + if (_showCrewIcons.UncertainCrewBorder) + ev.StatusIcons.Add(_prototype.Index(CrewUncertainBorderIcon)); + else if (ent.Comp.IsCrew) + ev.StatusIcons.Add(_prototype.Index(CrewBorderIcon)); + } + } +} diff --git a/Content.Client/Access/UI/AccessLevelControl.xaml b/Content.Client/Access/UI/AccessLevelControl.xaml index 56968d89839..6a3155effa4 100644 --- a/Content.Client/Access/UI/AccessLevelControl.xaml +++ b/Content.Client/Access/UI/AccessLevelControl.xaml @@ -1,4 +1,4 @@ diff --git a/Content.Client/Access/UI/AccessOverriderWindow.xaml b/Content.Client/Access/UI/AccessOverriderWindow.xaml index ae482140bc0..545ebec4941 100644 --- a/Content.Client/Access/UI/AccessOverriderWindow.xaml +++ b/Content.Client/Access/UI/AccessOverriderWindow.xaml @@ -2,7 +2,7 @@ MinSize="650 290"> - + diff --git a/Content.Client/Fax/UI/FaxWindow.xaml.cs b/Content.Client/Fax/UI/FaxWindow.xaml.cs index b809af7f4ae..9b25a2e2c74 100644 --- a/Content.Client/Fax/UI/FaxWindow.xaml.cs +++ b/Content.Client/Fax/UI/FaxWindow.xaml.cs @@ -25,13 +25,13 @@ public FaxWindow() PaperButtonPressed += OnPaperButtonPressed; - FileButton.OnPressed += _ => FileButtonPressed?.Invoke(); - PaperButton.OnPressed += _ => PaperButtonPressed?.Invoke(); + FileButton.OnPressed += _ => FileButtonPressed?.Invoke(); + PaperButton.OnPressed += _ => PaperButtonPressed?.Invoke(); CopyButton.OnPressed += _ => CopyButtonPressed?.Invoke(); SendButton.OnPressed += _ => SendButtonPressed?.Invoke(); RefreshButton.OnPressed += _ => RefreshButtonPressed?.Invoke(); PeerSelector.OnItemSelected += args => - PeerSelected?.Invoke((string) args.Button.GetItemMetadata(args.Id)!); + PeerSelected?.Invoke((string)args.Button.GetItemMetadata(args.Id)!); } public void UpdateState(FaxUiState state) diff --git a/Content.Client/FeedbackPopup/ClientFeedbackManager.cs b/Content.Client/FeedbackPopup/ClientFeedbackManager.cs new file mode 100644 index 00000000000..5b2b52b1b74 --- /dev/null +++ b/Content.Client/FeedbackPopup/ClientFeedbackManager.cs @@ -0,0 +1,67 @@ +using Content.Shared.FeedbackSystem; +using Robust.Shared.Prototypes; + +namespace Content.Client.FeedbackPopup; + +/// +public sealed class ClientFeedbackManager : SharedFeedbackManager +{ + /// + /// A read-only set representing the currently displayed feedback popups. + /// + public override IReadOnlySet> DisplayedPopups => _displayedPopups; + + private readonly HashSet> _displayedPopups = []; + + public override void Initialize() + { + base.Initialize(); + NetManager.RegisterNetMessage(ReceivedPopupMessage); + NetManager.RegisterNetMessage(_ => Open()); + } + + /// + /// Opens the feedback popup window. + /// + public void Open() + { + InvokeDisplayedPopupsChanged(true); + } + + /// + public override void Display(List>? prototypes) + { + if (prototypes == null) + return; + + var count = _displayedPopups.Count; + _displayedPopups.UnionWith(prototypes); + InvokeDisplayedPopupsChanged(_displayedPopups.Count > count); + } + + /// + public override void Remove(List>? prototypes) + { + if (prototypes == null) + { + _displayedPopups.Clear(); + } + else + { + _displayedPopups.ExceptWith(prototypes); + } + + InvokeDisplayedPopupsChanged(false); + } + + private void ReceivedPopupMessage(FeedbackPopupMessage message) + { + if (message.Remove) + { + Remove(message.FeedbackPrototypes); + return; + } + + Display(message.FeedbackPrototypes); + } +} diff --git a/Content.Client/FeedbackPopup/FeedbackEntry.xaml b/Content.Client/FeedbackPopup/FeedbackEntry.xaml new file mode 100644 index 00000000000..9b7e6ceb17d --- /dev/null +++ b/Content.Client/FeedbackPopup/FeedbackEntry.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Humanoid/LayerMarkingItem.xaml.cs b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs new file mode 100644 index 00000000000..76d46a480f7 --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs @@ -0,0 +1,212 @@ +using System.Linq; +using Content.Client.Guidebook.Controls; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Input; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingItem : BoxContainer, ISearchableControl +{ + [Dependency] private readonly IEntityManager _entity = default!; + + private readonly SpriteSystem _sprite; + + private readonly MarkingsViewModel _markingsModel; + private readonly MarkingPrototype _markingPrototype; + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private bool _interactive; + + private List? _colorSliders; + + public event Action? Pressed; + public event Action? Unpressed; + public ProtoId MarkingId => _markingPrototype.ID; + + public LayerMarkingItem(MarkingsViewModel model, ProtoId organ, HumanoidVisualLayers layer, MarkingPrototype prototype, bool interactive) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _sprite = _entity.System(); + + _markingsModel = model; + _markingPrototype = prototype; + _organ = organ; + _layer = layer; + _interactive = interactive; + + UpdateData(); + UpdateSelection(); + + SelectButton.OnPressed += SelectButtonPressed; + ColorsButton.OnPressed += ColorsButtonPressed; + + OnKeyBindDown += OnPressed; + OnKeyBindUp += OnUnpressed; + + if (!interactive) + { + SelectButton.MouseFilter = Control.MouseFilterMode.Ignore; + } + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateSelection; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateSelection; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateSelection(); + } + + private void UpdateData() + { + MarkingTexture.Textures = _markingPrototype.Sprites.Select(layer => _sprite.Frame0(layer)).ToList(); + SelectButton.Text = Loc.GetString($"marking-{_markingPrototype.ID}"); + } + + private void UpdateSelection() + { + var selected = _markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID); + SelectButton.Pressed = selected && _interactive; + ColorsButton.Visible = selected && _interactive && _markingsModel.IsMarkingColorCustomizable(_organ, _layer, _markingPrototype.ID); + + if (!selected || !_interactive) + { + ColorsButton.Pressed = false; + ColorsContainer.Visible = false; + } + + if (_markingsModel.GetMarking(_organ, _layer, _markingPrototype.ID) is { } marking && + _colorSliders is { } sliders) + { + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + sliders[i].Color = marking.MarkingColors[i]; + } + } + } + + private void SelectButtonPressed(BaseButton.ButtonEventArgs args) + { + if (!_interactive) + { + SelectButton.Pressed = false; + return; + } + + if (_markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID)) + { + if (!_markingsModel.TryDeselectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = true; + } + } + else + { + if (!_markingsModel.TrySelectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = false; + } + } + } + + private void ColorsButtonPressed(BaseButton.ButtonEventArgs args) + { + ColorsContainer.Visible = ColorsButton.Pressed; + + if (_colorSliders is not null) + return; + + if (_markingsModel.GetMarking(_organ, _layer, _markingPrototype.ID) is not { } marking) + return; + + _colorSliders = new(); + + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + var container = new BoxContainer() + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + + ColorsContainer.AddChild(container); + + var selector = new ColorSelectorSliders(); + selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; + + var label = _markingPrototype.Sprites[i] switch + { + SpriteSpecifier.Rsi rsi => Loc.GetString($"marking-{_markingPrototype.ID}-{rsi.RsiState}"), + SpriteSpecifier.Texture texture => Loc.GetString($"marking-{_markingPrototype.ID}-{texture.TexturePath.Filename}"), + _ => throw new InvalidOperationException("SpriteSpecifier not of known type"), + }; + + container.AddChild(new Label { Text = label }); + container.AddChild(selector); + + selector.Color = marking.MarkingColors[i]; + + _colorSliders.Add(selector); + + var colorIndex = i; + selector.OnColorChanged += _ => + { + _markingsModel.TrySetMarkingColor(_organ, _layer, _markingPrototype.ID, colorIndex, selector.Color); + }; + } + } + + public bool CheckMatchesSearch(string query) + { + return Loc.GetString($"marking-{_markingPrototype.ID}").Contains(query, StringComparison.OrdinalIgnoreCase); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } + + private void OnPressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Pressed?.Invoke(args, this); + } + + private void OnUnpressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Unpressed?.Invoke(args, this); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml b/Content.Client/Humanoid/LayerMarkingOrderer.xaml new file mode 100644 index 00000000000..c25a29296eb --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml @@ -0,0 +1,3 @@ + + + diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs new file mode 100644 index 00000000000..c3275ca885d --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs @@ -0,0 +1,192 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Interaction; +using Content.Client.Stylesheets; +using Content.Shared.Body; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingOrderer : BoxContainer +{ + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private readonly MarkingsViewModel _markingsModel; + private readonly DragDropHelper _dragDropHelper; + private readonly List _beacons = new(); + private LayerDragDropBeacon? _dragTarget; + + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public LayerMarkingOrderer(MarkingsViewModel markingsModel, ProtoId organ, HumanoidVisualLayers layer) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _markingsModel = markingsModel; + _organ = organ; + _layer = layer; + _dragDropHelper = new(OnBeginDrag, OnContinueDrag, OnEndDrag); + + UpdateItems(); + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateItems; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateItems; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateItems(); + } + + private void UpdateItems() + { + Items.RemoveAllChildren(); + _beacons.Clear(); + + if (_markingsModel.SelectedMarkings(_organ, _layer) is not { } markings) + return; + + for (var idx = 0; idx < markings.Count; idx++) + { + var marking = markings[idx]; + + var container = new LayerMarkingItemContainer(); + container.Margin = new(4); + + var item = new LayerMarkingItem(_markingsModel, _organ, _layer, _prototype.Index(marking.MarkingId), false); + item.DefaultCursorShape = CursorShape.Hand; + item.Pressed += (args, control) => OnItemPressed(args, control, container); + item.Unpressed += OnItemUnpressed; + + container.AddChild(item); + + var before = new LayerDragDropBeacon(CandidatePosition.Before, idx); + var after = new LayerDragDropBeacon(CandidatePosition.After, idx); + _beacons.Add(before); + _beacons.Add(after); + + Items.AddChild(before); + Items.AddChild(container); + Items.AddChild(after); + } + } + + private void OnItemPressed(GUIBoundKeyEventArgs args, LayerMarkingItem control, LayerMarkingItemContainer container) + { + _dragDropHelper.MouseDown(new(control, container)); + } + + private void OnItemUnpressed(GUIBoundKeyEventArgs args, LayerMarkingItem control) + { + _dragDropHelper.EndDrag(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _dragDropHelper.Update(args.DeltaSeconds); + } + + private bool OnBeginDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = false; + item.Orphan(); + item.DefaultCursorShape = CursorShape.Move; + UserInterfaceManager.PopupRoot.AddChild(item); + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + return true; + } + + private bool OnContinueDrag(float frameTime) + { + var (item, container) = _dragDropHelper.Dragged; + + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + + var closestBeacon = + _beacons.MinBy(beacon => + (UserInterfaceManager.MousePositionScaled.Position - beacon.GlobalPosition).LengthSquared()); + + if (closestBeacon != _dragTarget) + { + _dragTarget?.UnbecomeTarget(); + _dragTarget = closestBeacon; + _dragTarget?.BecomeTarget(); + } + + return true; + } + + private void OnEndDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = true; + item.Orphan(); + container.AddChild(item); + _dragTarget?.UnbecomeTarget(); + + if (_dragTarget != null) + { + _markingsModel.ChangeMarkingOrder(_organ, _layer, item.MarkingId, _dragTarget.CandidatePosition, _dragTarget.Index); + } + } +} + +internal readonly record struct LayerMarkingDragged(LayerMarkingItem Item, LayerMarkingItemContainer Container); + +internal sealed class LayerMarkingItemContainer : PanelContainer +{ + public LayerMarkingItemContainer() + { + SetHeight = 64; + HorizontalExpand = true; + } +} + +internal sealed class LayerDragDropBeacon(CandidatePosition position, int index) : PanelContainer +{ + public readonly CandidatePosition CandidatePosition = position; + public readonly int Index = index; + + public void BecomeTarget() + { + SetHeight = 64; + HorizontalExpand = true; + SetOnlyStyleClass(StyleClass.PanelDropTarget); + } + + public void UnbecomeTarget() + { + SetHeight = float.NaN; + RemoveStyleClass(StyleClass.PanelDropTarget); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingPicker.xaml b/Content.Client/Humanoid/LayerMarkingPicker.xaml new file mode 100644 index 00000000000..c5804584ac2 --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingPicker.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Imperial/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/Imperial/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs new file mode 100644 index 00000000000..2fd42731888 --- /dev/null +++ b/Content.Client/Imperial/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs @@ -0,0 +1,672 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Stylesheets; +using Content.Shared.Imperial.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.Imperial.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatUiFragment : BoxContainer +{ + public event Action? OnNotificationSwitchPressed; + public event Action? OnSendMessage; + public event Action? OnChatSelected; + public event Action? OnCreateChat; + public event Action? OnTypingMessage; + public event Action? OnSendLocation; + public event Action>? OnAddMembers; + public event Action>? OnRemoveMembers; + public event Action? OnEditChat; + public event Action? OnPrint; + + public enum NanoChatMode + { + Contacts, + GroupChats, + AddingMembers, + EditingChat, + } + + private NetEntity? _userId; + + private readonly TimeSpan _cooldown = TimeSpan.FromSeconds(5); + private TimeSpan _lastTyping; + private TimeSpan _lastPrint; + private TimeSpan _typingIndicatorTimeout; + + private readonly HashSet _selectedMembers = new(); + private NanoChatBoundUserInterfaceState? _lastState; + private Button? _selectedChat; + + private string _searchQuery = string.Empty; + + private NanoChatMode _currentMode = NanoChatMode.Contacts; + + private readonly Dictionary _sidebarOptions = new() + { + { NanoChatMode.Contacts, "nano-chat-ui-menu-options-contacts" }, + { NanoChatMode.GroupChats, "nano-chat-ui-menu-options-group-chats" }, + }; + + [Dependency] private readonly IEntitySystemManager _entitySystemManager = null!; + [Dependency] private readonly IPrototypeManager _prototypeManager = null!; + [Dependency] private readonly IGameTiming _gameTiming = null!; + private readonly SpriteSystem _spriteSystem; + + public NanoChatUiFragment() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _spriteSystem = _entitySystemManager.GetEntitySystem(); + + foreach (var (id, name) in _sidebarOptions) + OptionsMenu.AddItem(Loc.GetString(name), (int)id); + + OptionsMenu.OnItemSelected += args => + { + OptionsMenu.SelectId(args.Id); + _currentMode = (NanoChatMode)args.Id; + + if (_lastState != null) + UpdateState(_lastState); + }; + + MenuButton.OnPressed += _ => + { + MenuPanel.Visible = !MenuPanel.Visible; + }; + + NewChatNameInput.OnTextChanged += _ => + { + if (string.IsNullOrWhiteSpace(NewChatNameInput.Text)) + { + NewChatButton.Disabled = true; + return; + } + + NewChatButton.Disabled = false; + }; + + NewChatButton.OnPressed += _ => + { + OnCreateChat?.Invoke(NewChatNameInput.Text); + NewChatNameInput.Text = string.Empty; + NewChatButton.Disabled = true; + MenuPanel.Visible = false; + MenuButton.Pressed = false; + }; + + SearchInput.OnTextChanged += args => + { + _searchQuery = args.Text; + if (_lastState != null) + UpdateState(_lastState); + }; + + NotificationSwitch.OnPressed += _ => OnNotificationSwitchPressed?.Invoke(); + + MessageTextInput.OnTextChanged += OnTextInputTextChanged; + + SendLocationButton.OnPressed += _ => OnSendLocation?.Invoke(); + + PrintChatButton.OnPressed += _ => OnPrintButtonPressed(); + + SendButton.OnPressed += _ => SendMessage(); + MessageTextInput.OnTextEntered += _ => SendMessage(); + + AddMembersButton.OnPressed += _ => OnAddMembersButtonPressed(); + + EditChatNameInput.OnTextEntered += _ => OnEditChatButtonPressed(); + EditChatButton.OnPressed += _ => OnEditChatButtonPressed(); + } + + /// + /// Обновление состояния UI. + /// + public void UpdateState(NanoChatBoundUserInterfaceState state) + { + _lastState = state; + _userId = state.CurrentUserId; + + ContactsListContainer.DisposeAllChildren(); + MessageContainer.DisposeAllChildren(); + + UpdateCurrentChatDisplay(state.CurrentChat, state.IsServerOnline, state.Contacts); + + AddMembersButton.Visible = state.CurrentChat != null; + SendLocationButton.Visible = state.CanSendLocation; + EditChatButton.Visible = state.CurrentChat?.Owner == _userId ; + CreateChatContainer.Visible = OptionsMenu.SelectedId == (int)NanoChatMode.GroupChats; + SearchContainer.Visible = OptionsMenu.SelectedId == (int) NanoChatMode.Contacts; + + AddMembersButton.Pressed = _currentMode == NanoChatMode.AddingMembers; + EditChatButton.Pressed = _currentMode == NanoChatMode.EditingChat; + EditChatNameInput.Visible = EditChatButton.Pressed; + CurrentContactLabel.Visible = !EditChatButton.Pressed; + + var canSend = state is { IsServerOnline: true, CurrentChat: not null, IsContactReachable: true }; + SendButton.Disabled = !canSend; + SendLocationButton.Disabled = !canSend; + + var isPrintOnCooldown = _gameTiming.CurTime - _lastPrint <= _cooldown; + PrintChatButton.Disabled = !canSend || !state.CanPrint || isPrintOnCooldown; + + NotificationSwitch.Text = Loc.GetString( + state.NotificationOn + ? "news-read-ui-notification-on" + : "news-read-ui-notification-off"); + + + var sortedChats = state.Chats.OrderByDescending(c => + state.UnreadMessages.TryGetValue(c.Id, out var chatUnreadCount) && chatUnreadCount > 0); + + var allUnreadCount = state.UnreadMessages.Values.Sum(); + MenuButton.Text = allUnreadCount > 0 + ? Loc.GetString("nano-chat-ui-menu-open-button-unread", ("unread", allUnreadCount)) + : Loc.GetString("nano-chat-ui-menu-open-button"); + + var filteredChats = _currentMode switch + { + NanoChatMode.Contacts => sortedChats.Where(c => + (state.UnreadMessages.TryGetValue(c.Id, out var unread) && unread > 0) || + (c.Automated && MatchesSearch(c, state.Contacts, _searchQuery))), + NanoChatMode.GroupChats => sortedChats.Where(c => !c.Automated), + _ => sortedChats, + }; + + if (state is { CurrentChat: not null, TypingUsers.Count: > 0 }) + { + var typingNames = string.Join(", ", state.TypingUsers.Values); + + TypingIndicatorLabel.Text = Loc.GetString("nano-chat-ui-typing-indicator", + ("names", typingNames), + ("count", state.TypingUsers.Count)); + + TypingIndicatorLabel.Visible = true; + _typingIndicatorTimeout = _gameTiming.CurTime + _cooldown; + } + else + TypingIndicatorLabel.Visible = false; + + foreach (var chat in filteredChats) + AddAContact(chat, state); + + HandleNanoChatMode(state); + } + + + /// + /// Отправляет сообщение о печатании в текущем чате. + /// + private void OnTextInputTextChanged(LineEdit.LineEditEventArgs args) + { + var curTime = _gameTiming.CurTime; + if (curTime - _lastTyping <= _cooldown) + return; + + _lastTyping = curTime; + OnTypingMessage?.Invoke(); + } + + /// + /// Отправляет сообщение о печати истории чата. + /// + private void OnPrintButtonPressed() + { + var curTime = _gameTiming.CurTime; + if (curTime - _lastPrint <= _cooldown) + return; + + _lastPrint = curTime; + PrintChatButton.Disabled = true; + OnPrint?.Invoke(); + } + + /// + /// Отправляет сообщение в текущий чат. + /// + private void SendMessage(string? message = null) + { + var text = message ?? MessageTextInput.Text; + if (SendButton.Disabled || string.IsNullOrWhiteSpace(text)) + return; + + ChatScroll.SetScrollValue(new Vector2(0f, float.MaxValue)); + + OnSendMessage?.Invoke(text); + if (message is null) + MessageTextInput.Text = string.Empty; + } + + /// + /// Установка режима добавления пользователей в чат, отправка сообщений о добавлении пользователей. + /// + private void OnAddMembersButtonPressed() + { + if (_lastState?.CurrentChat == null) + return; + + if (_currentMode != NanoChatMode.AddingMembers) + _currentMode = NanoChatMode.AddingMembers; + else + { + if (_selectedMembers.Count > 0) + OnAddMembers?.Invoke(_lastState.CurrentChat.Id, _selectedMembers.ToList()); + + _currentMode = NanoChatMode.GroupChats; + } + + _selectedMembers.Clear(); + + if (_lastState != null) + UpdateState(_lastState); + } + + /// + /// Установка режима изменения чата, отправка сообщений об изменении чата. + /// + private void OnEditChatButtonPressed() + { + if (_lastState?.CurrentChat == null) + return; + + if (_currentMode != NanoChatMode.EditingChat) + { + _currentMode = NanoChatMode.EditingChat; + EditChatNameInput.Text = _lastState.CurrentChat.Name; + } + else + { + if (EditChatNameInput.Text != _lastState.CurrentChat.Name + && !string.IsNullOrWhiteSpace(EditChatNameInput.Text)) + { + OnEditChat?.Invoke(_lastState.CurrentChat.Id, EditChatNameInput.Text); + CurrentContactLabel.Text = EditChatNameInput.Text; + } + + if (_selectedMembers.Count > 0) + OnRemoveMembers?.Invoke(_lastState.CurrentChat.Id, _selectedMembers.ToList()); + + _currentMode = _lastState.CurrentChat.Automated ? NanoChatMode.Contacts : NanoChatMode.GroupChats; + } + + if (_lastState != null) + UpdateState(_lastState); + } + + + /// + /// Обновление имени чата и его состояния. + /// + private void UpdateCurrentChatDisplay(NanoChatChat? chat, bool isServerOnline, List contacts) + { + if (!isServerOnline) + { + CurrentContactIcon.Visible = false; + CurrentContactLabel.Text = Loc.GetString("nano-chat-ui-chat-window-no-chat-selected"); + + var noServerLabel = new Label() + { + Text = Loc.GetString("nano-chat-ui-chat-window-message-server-unreachable"), + Align = Label.AlignMode.Center, + FontColorOverride = Color.Red, + }; + MessageContainer.AddChild(noServerLabel); + return; + } + if (chat is null) + { + CurrentContactIcon.Visible = false; + CurrentContactLabel.Text = Loc.GetString("nano-chat-ui-chat-window-no-chat-selected"); + return; + } + + NanoChatContact? singleContact = null; + if (chat.Automated && _userId != null) + { + var otherId = chat.Members.FirstOrDefault(m => m != _userId.Value); + foreach (var c in contacts.Where(c => c.Id == otherId)) + { + singleContact = c; + break; + } + } + + + if (singleContact is null) + { + CurrentContactLabel.Text = chat.Name; + CurrentContactIcon.Visible = false; + return; + } + + CurrentContactLabel.Text = Loc.GetString("nano-chat-ui-chat-window-contact-name-job", + ("name", singleContact.Value.Name), + ("job", GetJobTitle(singleContact.Value.JobTitle))); + + if (singleContact.Value.JobIconId != null + && _prototypeManager.TryIndex(singleContact.Value.JobIconId, out var jobIconProto)) + { + CurrentContactIcon.Texture = _spriteSystem.Frame0(jobIconProto.Icon); + CurrentContactIcon.Visible = true; + } + else + CurrentContactIcon.Visible = false; + } + + /// + /// Проверка контакта на соответствие запросу. + /// + private bool MatchesSearch(NanoChatChat chat, List contacts, string query) + { + if (string.IsNullOrWhiteSpace(query)) + return true; + + if (!string.IsNullOrEmpty(chat.Name) && chat.Name.Contains(query, StringComparison.OrdinalIgnoreCase)) + return true; + + if (chat.Members.Count > 2 || _userId == null) + return false; + + if (!chat.Members.Contains(_userId.Value)) + { + var member1 = contacts.FirstOrDefault(c => c.Id == chat.Members.ElementAtOrDefault(0)); + var member2 = contacts.FirstOrDefault(c => c.Id == chat.Members.ElementAtOrDefault(1)); + + return (member1.Name?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) || + (GetJobTitle(member1.JobTitle).Contains(query, StringComparison.OrdinalIgnoreCase)) || + (member2.Name?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) || + (GetJobTitle(member2.JobTitle).Contains(query, StringComparison.OrdinalIgnoreCase)) || + chat.Id.ToString().Contains(query); + } + + var otherId = chat.Members.FirstOrDefault(m => m != _userId.Value); + var contact = contacts.FirstOrDefault(c => c.Id == otherId); + + if (contact.Name != null && contact.Name.Contains(query, StringComparison.OrdinalIgnoreCase)) + return true; + + var jobTitle = GetJobTitle(contact.JobTitle); + + + return jobTitle.Contains(query, StringComparison.OrdinalIgnoreCase); // || chat.Messages.Any(m => m.Content.Contains(query)); + } + + /// + /// Добавляет контакт в левую панель. + /// + private void AddAContact(NanoChatChat chat, NanoChatBoundUserInterfaceState state) + { + var isCurrentChat = state.CurrentChat?.Id == chat.Id; + + var btn = new Button + { + Margin = new Thickness(0, 0, 0, 2), + HorizontalExpand = true, + ToggleMode = true, + Pressed = isCurrentChat, + }; + + if (isCurrentChat) + _selectedChat = btn; + + var row = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + NanoChatContact? singleContact = null; + NanoChatContact? adminContact1 = null; + NanoChatContact? adminContact2 = null; + + if (chat.Automated && _userId != null) + { + if (state.FinePrintText && chat.Members.Count >= 2) + { + adminContact1 = state.Contacts.FirstOrDefault(c => c.Id == chat.Members[0]); + adminContact2 = state.Contacts.FirstOrDefault(c => c.Id == chat.Members[1]); + } + else + { + var otherId = chat.Members.FirstOrDefault(m => m != _userId.Value); + singleContact = state.Contacts.FirstOrDefault(c => c.Id == otherId); + } + } + + string text; + if (state.FinePrintText && adminContact1 != null && adminContact2 != null) + { + text = Loc.GetString("nano-chat-ui-contacts-contact-name-job-fine", + ("name1", adminContact1.Value.Name ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")), + ("job1", GetJobTitle(adminContact1.Value.JobTitle)), + ("name2", adminContact2.Value.Name ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")), + ("job2", GetJobTitle(adminContact2.Value.JobTitle))); + } + else if (singleContact is not null) + { + if (singleContact.Value.JobIconId != null && _prototypeManager.TryIndex(singleContact.Value.JobIconId, out var jobIconProto)) + { + var icon = new TextureRect + { + Texture = _spriteSystem.Frame0(jobIconProto.Icon), + TextureScale = new Vector2(2, 2), + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 0, 4, 0), + }; + row.AddChild(icon); + } + text = Loc.GetString("nano-chat-ui-contacts-contact-name-job", + ("name", singleContact.Value.Name ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")), + ("job", GetJobTitle(singleContact.Value.JobTitle))); + } + else + text = chat.Name; + + if (!state.FinePrintText && state.UnreadMessages.TryGetValue(chat.Id, out var unreadCount) && unreadCount > 0) + { + text = Loc.GetString("nano-chat-ui-contacts-contact-name-job-unread", + ("name-job", text), + ("unread", unreadCount)); + } + + var label = new RichTextLabel() { Text = text, HorizontalExpand = true }; + row.AddChild(label); + btn.AddChild(row); + + btn.OnPressed += _ => SelectChat(btn, chat, state); + ContactsListContainer.AddChild(btn); + } + + /// + /// Обработка всех возможных состояний действий. + /// + private void HandleNanoChatMode(NanoChatBoundUserInterfaceState state) + { + switch (_currentMode) + { + case NanoChatMode.EditingChat: + case NanoChatMode.AddingMembers: + { + if (state.CurrentChat == null) + return; + + var hasAvailableContacts = state.Contacts.Exists(contact => + contact.Id != _userId && + (_currentMode == NanoChatMode.AddingMembers + ? !state.CurrentChat.Members.Contains(contact.Id) + : state.CurrentChat.Members.Contains(contact.Id))); + + if (!hasAvailableContacts) + { + MessageContainer.AddChild(new Label { Text = Loc.GetString("nano-chat-ui-chat-window-no-available-contacts") }); + break; + } + + var instruction = new Label { Text = Loc.GetString("nano-chat-ui-chat-window-add-contacts") }; + + if (_currentMode is NanoChatMode.EditingChat) + instruction.Text = Loc.GetString("nano-chat-ui-chat-window-editing-contacts"); + + MessageContainer.AddChild(instruction); + + var relevantContacts = state.Contacts.Where(contact => + contact.Id != _userId && + (_currentMode == NanoChatMode.AddingMembers + ? !state.CurrentChat.Members.Contains(contact.Id) + : state.CurrentChat.Members.Contains(contact.Id))); + + foreach (var contact in relevantContacts) + { + var btnText = Loc.GetString( + "nano-chat-ui-chat-window-contact-name-job", + ("name", contact.Name), + ("job", GetJobTitle(contact.JobTitle))); + + var btn = new Button + { + Text = btnText, + TextAlign = Label.AlignMode.Left, + ToggleMode = true, + Pressed = _selectedMembers.Contains(contact.Id), + }; + + btn.OnPressed += _ => + { + if (!_selectedMembers.Remove(contact.Id)) + _selectedMembers.Add(contact.Id); + + if (_lastState != null) + UpdateState(_lastState); + }; + + MessageContainer.AddChild(btn); + } + break; + } + default: + case NanoChatMode.Contacts: + case NanoChatMode.GroupChats: + if (state.CurrentChat?.Messages == null) + return; + + foreach (var msg in state.CurrentChat.Messages) + AddMessageBubble(msg); + break; + } + } + + /// + /// Добавляет облачко текста в окно с сообщениями. + /// + private void AddMessageBubble(NanoChatMessage msg) + { + var displaySender = msg.SenderName; + + if (msg.SenderId == _userId) + displaySender = Loc.GetString("nano-chat-ui-chat-window-sender-you"); + else if (string.IsNullOrEmpty(displaySender)) + displaySender = Loc.GetString("nano-chat-ui-chat-window-sender-unknown"); + + var wrapper = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + var panel = new PanelContainer + { + StyleClasses = { StyleClass.BackgroundPanelDark }, + HorizontalExpand = false, + MouseFilter = MouseFilterMode.Pass, + ToolTip = Loc.GetString("nano-chat-ui-chat-window-message-tooltip", ("time", msg.SendTime.ToString(@"hh\:mm\:ss"))), + }; + + if (msg.SenderId == _userId) + { + wrapper.AddChild(new Control { HorizontalExpand = true }); + wrapper.AddChild(panel); + } + else + { + wrapper.AddChild(panel); + wrapper.AddChild(new Control { HorizontalExpand = true }); + } + + var label = new RichTextLabel(); + label.SetMessage(FormattedMessage.FromMarkupPermissive( + Loc.GetString("nano-chat-ui-chat-window-message", + ("sender", displaySender), + ("content", msg.Content)))); + + panel.AddChild(label); + MessageContainer.AddChild(wrapper); + } + + /// + /// Выбирает чат в интерфейсе. + /// + private void SelectChat(Button btn, NanoChatChat chat, NanoChatBoundUserInterfaceState state) + { + if (state.CurrentChat?.Id == chat.Id) + { + btn.Pressed = true; + return; + } + + _selectedChat?.Pressed = false; + _selectedChat = btn; + _selectedChat.Pressed = true; + + MenuPanel.Visible = false; + MenuButton.Pressed = false; + AddMembersButton.Pressed = false; + + _currentMode = chat.Automated ? NanoChatMode.Contacts : NanoChatMode.GroupChats; + OptionsMenu.SelectId((int)_currentMode); + _selectedMembers.Clear(); + + UpdateCurrentChatDisplay(chat, true, state.Contacts); + + MessageContainer.DisposeAllChildren(); + MessageContainer.AddChild(new RichTextLabel + { + Margin = new Thickness(0, 0, 0, 5), + Text = Loc.GetString("nano-chat-ui-chat-window-message-loading"), + }); + + OnChatSelected?.Invoke(chat.Id); + } + + /// Имя профессии + /// Или имя профессии, или Неизвестный, если оно 'nano-chat-ui-contact-job-unknown' + private static string GetJobTitle(string? jobTitle) + { + return jobTitle is null or "nano-chat-ui-contact-job-unknown" + ? Loc.GetString("nano-chat-ui-contact-job-unknown") + : jobTitle; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (TypingIndicatorLabel.Visible && _gameTiming.CurTime > _typingIndicatorTimeout) + TypingIndicatorLabel.Visible = false; + + var isPrintOnCooldown = _gameTiming.CurTime - _lastPrint <= _cooldown; + + if (!PrintChatButton.Disabled && isPrintOnCooldown) + PrintChatButton.Disabled = true; + else if (!isPrintOnCooldown) + PrintChatButton.Disabled = false; + } +} diff --git a/Content.Client/Imperial/CrewSkills/CrewSkillsSystem.cs b/Content.Client/Imperial/CrewSkills/CrewSkillsSystem.cs new file mode 100644 index 00000000000..1f49412bf2c --- /dev/null +++ b/Content.Client/Imperial/CrewSkills/CrewSkillsSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Imperial.CrewSkills; + +namespace Content.Client.Imperial.CrewSkills; + + +public sealed partial class CrewSkillsSystem : SharedCrewSkillsSystem; diff --git a/Content.Client/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteVisualsSystem.cs b/Content.Client/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteVisualsSystem.cs new file mode 100644 index 00000000000..01a1b29951e --- /dev/null +++ b/Content.Client/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteVisualsSystem.cs @@ -0,0 +1,123 @@ +using Content.Client.Clothing; +using Content.Client.Items.Systems; +using Content.Client.Weapons.Ranged.Components; +using Content.Shared.Clothing; +using Content.Shared.Hands; +using Content.Shared.Imperial.DeimonFly.BuckshotRoulette; +using Content.Shared.Inventory; +using Content.Shared.Item; +using Robust.Client.GameObjects; + +namespace Content.Client.Imperial.DeimonFly.BuckshotRoulette; + +/// +/// Переключает внешний вид дробовика дилера на клиенте, не вмешиваясь в штатные системы оружия и рук. +/// +public sealed class BuckshotRouletteVisualsSystem : EntitySystem +{ + [Dependency] private readonly SharedItemSystem _item = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnAfterState); + SubscribeLocalEvent(OnGetInhandVisuals, + after: [typeof(ItemSystem)]); + SubscribeLocalEvent(OnGetEquipmentVisuals, + after: [typeof(ClientClothingSystem)]); + } + + private void OnStartup(Entity shotgun, ref ComponentStartup args) + { + ApplyWorldVisuals(shotgun, force: true); + } + + private void OnAfterState(Entity shotgun, ref AfterAutoHandleStateEvent args) + { + ApplyWorldVisuals(shotgun); + } + + private void ApplyWorldVisuals(Entity shotgun, bool force = false) + { + if (!force && + shotgun.Comp.AppliedBarrelVisualState == shotgun.Comp.BarrelVisualState) + { + return; + } + + if (!TryComp(shotgun, out var sprite) || + !_sprite.LayerMapTryGet((shotgun.Owner, sprite), GunVisualLayers.Base, out var layer, false)) + { + return; + } + + var (rsi, state) = shotgun.Comp.BarrelVisualState switch + { + BuckshotRouletteBarrelVisualState.Sawed => (shotgun.Comp.SawedWorldSprite, "icon"), + BuckshotRouletteBarrelVisualState.Restoring => (shotgun.Comp.RestoringWorldSprite, "restoring-icon"), + _ => (shotgun.Comp.IntactWorldSprite, "icon"), + }; + + _sprite.LayerSetRsi((shotgun.Owner, sprite), layer, rsi, state); + shotgun.Comp.AppliedBarrelVisualState = shotgun.Comp.BarrelVisualState; + + // Если предмет сейчас находится в руках или слоте экипировки, просим владельца сразу перерисовать его. + _item.VisualsChanged(shotgun); + } + + private void OnGetInhandVisuals( + Entity shotgun, + ref GetInhandVisualsEvent args) + { + if (shotgun.Comp.BarrelVisualState == BuckshotRouletteBarrelVisualState.Intact) + return; + + // ItemSystem уже выбрала правильное состояние с учётом HeldPrefix и наличия его в RSI. + // Меняем только RSI штатного базового слоя, не удаляя дополнительные визуальные слои предмета. + var regularState = $"inhand-{args.Location.ToString().ToLowerInvariant()}"; + var wieldedState = $"wielded-{regularState}"; + foreach (var (_, layer) in args.Layers) + { + if (layer.State != regularState && layer.State != wieldedState) + continue; + + layer.RsiPath = shotgun.Comp.SawedInhandSprite.ToString(); + return; + } + } + + private void OnGetEquipmentVisuals( + Entity shotgun, + ref GetEquipmentVisualsEvent args) + { + if (shotgun.Comp.BarrelVisualState == BuckshotRouletteBarrelVisualState.Intact) + return; + + if (!_inventory.TryGetSlot(args.Equipee, args.Slot, out var slot)) + return; + + var state = slot.SlotFlags switch + { + SlotFlags.BACK => "equipped-BACKPACK", + SlotFlags.SUITSTORAGE => "equipped-SUITSTORAGE", + _ => null, + }; + + if (state == null) + return; + + // ClothingSystem уже сформировала базовый слой с правильным ключом, масштабом и состоянием. + foreach (var (_, layer) in args.Layers) + { + if (layer.State != state) + continue; + + layer.RsiPath = shotgun.Comp.SawedWorldSprite.ToString(); + return; + } + } +} diff --git a/Content.Client/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunStatusSystem.cs b/Content.Client/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunStatusSystem.cs new file mode 100644 index 00000000000..d895d404cb9 --- /dev/null +++ b/Content.Client/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunStatusSystem.cs @@ -0,0 +1,30 @@ +using Content.Client.Items; +using Content.Client.Weapons.Ranged.Components; +using Content.Client.Weapons.Ranged.Systems; +using Content.Shared.Imperial.DeimonFly.RussianRoulette; + +namespace Content.Client.Imperial.DeimonFly.RussianRoulette; + +/// +/// Скрывает индикатор камер барабана у револьверов для русской рулетки. +/// +public sealed class RussianRouletteGunStatusSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + // Стандартная система оружия сначала добавляет индикатор, после чего мы удаляем только его. + SubscribeLocalEvent( + OnCollectItemStatus, + after: [typeof(GunSystem)]); + } + + private void OnCollectItemStatus(Entity gun, ref ItemStatusCollectMessage args) + { + if (!TryComp(gun, out var ammoCounter) || ammoCounter.Control == null) + return; + + args.Controls.Remove(ammoCounter.Control); + } +} diff --git a/Content.Client/Imperial/EmergencyButton/EmergencyButtonSystem.cs b/Content.Client/Imperial/EmergencyButton/EmergencyButtonSystem.cs deleted file mode 100644 index 3cd18f67441..00000000000 --- a/Content.Client/Imperial/EmergencyButton/EmergencyButtonSystem.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.Imperial.EmergencyButton.Components; -using Robust.Client.GameObjects; - -namespace Content.Client.Imperial.EmergencyButton; - -public sealed class EmergencyButtonSystem : EntitySystem -{ - [Dependency] private readonly SpriteSystem _sprite = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnComponentStartup); - SubscribeLocalEvent(OnComponentShutdown); - - } - - private void OnComponentStartup(EntityUid uid, EmergencyButtonComponent component, ComponentStartup args) - { - UpdateAppearance(uid, component); - } - - private void OnComponentShutdown(EntityUid uid, EmergencyButtonComponent component, ComponentShutdown args) - { - if (TryComp(uid, out var sprite)) - { - _sprite.LayerSetRsiState((uid, sprite), 0, "EmergencyButton"); - } - } - - private void UpdateAppearance(EntityUid uid, EmergencyButtonComponent component) - { - if (!TryComp(uid, out var sprite)) - return; - - _sprite.LayerSetRsiState((uid, sprite), 0, "EmergencyButton"); - } -} - diff --git a/Content.Client/Imperial/EnergyCore/UI/CoreTerminalBoundUserInterface.cs b/Content.Client/Imperial/EnergyCore/UI/CoreTerminalBoundUserInterface.cs new file mode 100644 index 00000000000..ea801af0701 --- /dev/null +++ b/Content.Client/Imperial/EnergyCore/UI/CoreTerminalBoundUserInterface.cs @@ -0,0 +1,37 @@ +// Компоненты консоли ядра +using JetBrains.Annotations; +using Content.Shared.Imperial.EnergyCore; +using Robust.Client.UserInterface; + +namespace Content.Client.Imperial.EnergyCore.UI +{ + [UsedImplicitly] + public sealed class CoreTerminalBoundUserInterface : BoundUserInterface + { + private CoreTerminalWindow? _window; + + public CoreTerminalBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.OnCoreTerminalButton += ButtonPressed; + } + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (CoreTerminalBoundUserInterfaceState)state; + _window?.UpdateState(castState); //Update window state + } + + public void ButtonPressed(UiButton button) + { + SendMessage(new UiButtonPressedMessage(button)); + } + } +} diff --git a/Content.Client/Imperial/EnergyCore/UI/CoreTerminalWindow.xaml b/Content.Client/Imperial/EnergyCore/UI/CoreTerminalWindow.xaml new file mode 100644 index 00000000000..104fa187abc --- /dev/null +++ b/Content.Client/Imperial/EnergyCore/UI/CoreTerminalWindow.xaml @@ -0,0 +1,116 @@ + + + + + + + + + + @@ -18,9 +29,13 @@ diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs index 64d1f78d3ce..66530eba917 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using Content.Client.Decals.UI; using Content.Client.Stylesheets; using Content.Shared.Decals; using Robust.Client.AutoGenerated; @@ -8,6 +8,8 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; namespace Content.Client.SprayPainter.UI; @@ -21,6 +23,9 @@ public sealed partial class SprayPainterDecals : Control public Action? OnColorChanged; public Action? OnAngleChanged; public Action? OnSnapChanged; + public Action? OnColorPickerToggled; + + private PaletteColorPicker? _palette; private SpriteSystem? _sprite; private string _selectedDecal = string.Empty; @@ -30,14 +35,17 @@ public SprayPainterDecals() { RobustXamlLoader.Load(this); - AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90; - SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90; + AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360; + SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360; SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0; AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value); UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed; SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed; ColorSelector.OnColorChanged += OnColorSelected; + + ColorPalette.OnPressed += ColorPaletteOnPressed; + ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed); } private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _) @@ -147,6 +155,7 @@ private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj) public void SetSelectedDecal(string name) { _selectedDecal = name; + SelectedDecalName.Text = name; if (_sprite is null) return; @@ -171,4 +180,35 @@ public void SetSnap(bool snap) { SnapToTileCheckBox.Pressed = snap; } + + private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _) + { + // Code copied from other implementations of `PaletteColorPicker`. + if (_palette is null) + { + _palette = new PaletteColorPicker(); + _palette.OpenCenteredLeft(); + _palette.PaletteList.OnItemSelected += args => + { + var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value; + ColorSelector.Color = color; + OnColorSelected(color); + }; + return; + } + + if (_palette.IsOpen) + { + _palette.Close(); + } + else + { + _palette.Open(); + } + } + + public void SetColorPicker(bool enabled) + { + ColorPicker.Pressed = enabled; + } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs index eb1218ad678..2f727960438 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs @@ -30,6 +30,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow public event Action? OnDecalColorChanged; public event Action? OnDecalAngleChanged; public event Action? OnDecalSnapChanged; + public event Action? OnDecalColorPickerToggled; // Pipe color data private ItemList _colorList = default!; @@ -195,6 +196,7 @@ public void PopulateCategories(Dictionary _sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color); _sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle); _sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap); + _sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle); Tabs.AddChild(_sprayPainterDecals); TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals")); @@ -298,7 +300,12 @@ public void SetDecalSnap(bool snap) if (_sprayPainterDecals != null) _sprayPainterDecals.SetSnap(snap); } - # endregion + + public void SetDecalColorPicker(bool colorPickerEnabled) + { + _sprayPainterDecals?.SetColorPicker(colorPickerEnabled); + } + #endregion } public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData; diff --git a/Content.Client/Sprite/SpriteFadeSystem.cs b/Content.Client/Sprite/SpriteFadeSystem.cs index ab90b8c328d..8a4aa72d13f 100644 --- a/Content.Client/Sprite/SpriteFadeSystem.cs +++ b/Content.Client/Sprite/SpriteFadeSystem.cs @@ -29,16 +29,15 @@ public sealed class SpriteFadeSystem : EntitySystem [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly EntityQuery _spriteQuery = default!; + [Dependency] private readonly EntityQuery _fadeQuery = default!; + [Dependency] private readonly EntityQuery _fadingQuery = default!; + [Dependency] private readonly EntityQuery _fixturesQuery = default!; private List<(MapCoordinates Point, bool ExcludeBoundingBox)> _points = new(); private readonly HashSet _comps = new(); - private EntityQuery _spriteQuery; - private EntityQuery _fadeQuery; - private EntityQuery _fadingQuery; - private EntityQuery _fixturesQuery; - private const float TargetAlpha = 0.4f; private const float ChangeRate = 1f; @@ -46,11 +45,6 @@ public override void Initialize() { base.Initialize(); - _spriteQuery = GetEntityQuery(); - _fadeQuery = GetEntityQuery(); - _fadingQuery = GetEntityQuery(); - _fixturesQuery = GetEntityQuery(); - SubscribeLocalEvent(OnFadingShutdown); } diff --git a/Content.Client/Stack/StackStatusControl.cs b/Content.Client/Stack/StackStatusControl.cs index 80062268322..1031b9ec9de 100644 --- a/Content.Client/Stack/StackStatusControl.cs +++ b/Content.Client/Stack/StackStatusControl.cs @@ -7,6 +7,9 @@ namespace Content.Client.Stack; +/// +/// Used by hands in player UI to display the stack count. +/// public sealed class StackStatusControl : Control { private readonly StackComponent _parent; @@ -15,7 +18,7 @@ public sealed class StackStatusControl : Control public StackStatusControl(StackComponent parent) { _parent = parent; - _label = new RichTextLabel {StyleClasses = {StyleNano.StyleClassItemStatus}}; + _label = new RichTextLabel {StyleClasses = {StyleClass.ItemStatus}}; _label.SetMarkup(Loc.GetString("comp-stack-status", ("count", _parent.Count))); AddChild(_label); } diff --git a/Content.Client/Stack/StackSystem.cs b/Content.Client/Stack/StackSystem.cs index 182daa73a50..9396c76df59 100644 --- a/Content.Client/Stack/StackSystem.cs +++ b/Content.Client/Stack/StackSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Client.Items; using Content.Client.Storage.Systems; using Content.Shared.Stacks; @@ -7,6 +6,7 @@ namespace Content.Client.Stack { + /// [UsedImplicitly] public sealed class StackSystem : SharedStackSystem { @@ -16,33 +16,21 @@ public sealed class StackSystem : SharedStackSystem public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnAppearanceChange); Subs.ItemStatus(ent => new StackStatusControl(ent)); } - public override void SetCount(EntityUid uid, int amount, StackComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - base.SetCount(uid, amount, component); - - // TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call. - if (component.Count <= 0) - { - Xform.DetachEntity(uid, Transform(uid)); - return; - } - - component.UiUpdateNeeded = true; - } + #region Appearance - private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args) + private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent args) { + var (uid, comp) = ent; + if (args.Sprite == null || comp.LayerStates.Count < 1) return; - // Skip processing if no actual + // Skip processing if no elements in the stack if (!_appearanceSystem.TryGetData(uid, StackVisuals.Actual, out var actual, args.Component)) return; @@ -56,9 +44,24 @@ private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref Appearan ApplyLayerFunction((uid, comp), ref actual, ref maxCount); if (comp.IsComposite) - _counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite); + { + _counterSystem.ProcessCompositeSprite(uid, + actual, + maxCount, + comp.LayerStates, + hidden, + sprite: args.Sprite); + } else - _counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite); + { + _counterSystem.ProcessOpaqueSprite(uid, + comp.BaseLayer, + actual, + maxCount, + comp.LayerStates, + hidden, + sprite: args.Sprite); + } } /// @@ -67,7 +70,7 @@ private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref Appearan /// The entity considered. /// The actual number of items in the stack. Altered depending on the function to run. /// The maximum number of items in the stack. Altered depending on the function to run. - /// Whether or not a function was applied. + /// True if a function was applied. private bool ApplyLayerFunction(Entity ent, ref int actual, ref int maxCount) { switch (ent.Comp.LayerFunction) @@ -78,8 +81,10 @@ private bool ApplyLayerFunction(Entity ent, ref int actual, ref ApplyThreshold(threshold, ref actual, ref maxCount); return true; } + break; } + // No function applied. return false; } @@ -105,7 +110,10 @@ private static void ApplyThreshold(StackLayerThresholdComponent comp, ref int ac else break; } + actual = newActual; } + + #endregion } } diff --git a/Content.Client/StationRecords/GeneralRecord.xaml.cs b/Content.Client/StationRecords/GeneralRecord.xaml.cs index e84c2dc0982..b69c6fb8bb6 100644 --- a/Content.Client/StationRecords/GeneralRecord.xaml.cs +++ b/Content.Client/StationRecords/GeneralRecord.xaml.cs @@ -1,8 +1,9 @@ using Content.Shared.StationRecords; +using Content.Shared.Humanoid.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Enums; +using Robust.Shared.Prototypes; namespace Content.Client.StationRecords; @@ -10,14 +11,15 @@ namespace Content.Client.StationRecords; public sealed partial class GeneralRecord : Control { public Action? OnDeletePressed; - public GeneralRecord(GeneralStationRecord record, bool canDelete, uint? id) + public GeneralRecord(GeneralStationRecord record, bool canDelete, uint? id, IPrototypeManager prototypeManager) { RobustXamlLoader.Load(this); RecordName.Text = record.Name; Age.Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString())); Title.Text = Loc.GetString("general-station-record-console-record-title", ("job", Loc.GetString(record.JobTitle))); - Species.Text = Loc.GetString("general-station-record-console-record-species", ("species", record.Species)); + var species = Loc.GetString(prototypeManager.Index(record.Species).Name); + Species.Text = Loc.GetString("general-station-record-console-record-species", ("species", species)); Gender.Text = Loc.GetString("general-station-record-console-record-gender", ("gender", record.Gender.ToString())); Fingerprint.Text = Loc.GetString("general-station-record-console-record-fingerprint", @@ -25,7 +27,7 @@ public GeneralRecord(GeneralStationRecord record, bool canDelete, uint? id) Dna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", record.DNA ?? Loc.GetString("generic-not-available-shorthand"))); - if (canDelete && id != null ) + if (canDelete && id != null) { DeleteButton.Visible = true; DeleteButton.OnPressed += _ => OnDeletePressed?.Invoke(id.Value); diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs index 272e6c3b251..951928325d5 100644 --- a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs +++ b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs @@ -2,12 +2,15 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; namespace Content.Client.StationRecords; [GenerateTypedNameReferences] public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + public Action? OnKeySelected; public Action? OnFiltersChanged; @@ -20,6 +23,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow public GeneralStationRecordConsoleWindow() { RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); _currentFilterType = StationRecordFilterType.Name; @@ -44,7 +48,7 @@ public GeneralStationRecordConsoleWindow() StationRecordsFilterType.OnItemSelected += eventArgs => { - var type = (StationRecordFilterType) eventArgs.Id; + var type = (StationRecordFilterType)eventArgs.Id; if (_currentFilterType != type) { @@ -139,7 +143,7 @@ private void PopulateRecordListing(Dictionary listing, uint? selec private void PopulateRecordContainer(GeneralStationRecord record, bool enableDelete, uint? id) { RecordContainer.RemoveAllChildren(); - var newRecord = new GeneralRecord(record, enableDelete, id); + var newRecord = new GeneralRecord(record, enableDelete, id, _prototypeManager); newRecord.OnDeletePressed = OnDeleted; RecordContainer.AddChild(newRecord); diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs index 4d3be5439cf..7064be5bab2 100644 --- a/Content.Client/StatusIcon/StatusIconOverlay.cs +++ b/Content.Client/StatusIcon/StatusIconOverlay.cs @@ -96,7 +96,7 @@ protected override void Draw(in OverlayDrawArgs args) countL++; } yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetL - proto.Offset) / EyeManager.PixelsPerMeter; - xOffset = -(bounds.Width + sprite.Offset.X) / 2f; + xOffset = -(bounds.Width + sprite.Offset.X) / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter; } else @@ -109,7 +109,7 @@ protected override void Draw(in OverlayDrawArgs args) countR++; } yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter; - xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)texture.Width / EyeManager.PixelsPerMeter; + xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter; } diff --git a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs index 85c6b8e0660..4f58919d780 100644 --- a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs +++ b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs @@ -5,14 +5,12 @@ namespace Content.Client.Sticky.Visualizers; public sealed class StickyVisualizerSystem : VisualizerSystem { - private EntityQuery _spriteQuery; + [Dependency] private readonly EntityQuery _spriteQuery = default!; public override void Initialize() { base.Initialize(); - _spriteQuery = GetEntityQuery(); - SubscribeLocalEvent(OnInit); } diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs index ca2b986667e..96dc353c481 100644 --- a/Content.Client/Storage/Systems/EntityStorageSystem.cs +++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs @@ -1,43 +1,5 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Client.Storage.Components; -using Content.Shared.Destructible; -using Content.Shared.Foldable; -using Content.Shared.Interaction; -using Content.Shared.Lock; -using Content.Shared.Movement.Events; -using Content.Shared.Storage.Components; -using Content.Shared.Storage.EntitySystems; -using Content.Shared.Verbs; -using Robust.Shared.GameStates; +using Content.Shared.Storage.EntitySystems; namespace Content.Client.Storage.Systems; -public sealed class EntityStorageSystem : SharedEntityStorageSystem -{ - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnEntityUnpausedEvent); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnComponentStartup); - SubscribeLocalEvent(OnInteract, after: new[] { typeof(LockSystem) }); - SubscribeLocalEvent(OnLockToggleAttempt); - SubscribeLocalEvent(OnDestruction); - SubscribeLocalEvent>(AddToggleOpenVerb); - SubscribeLocalEvent(OnRelayMovement); - SubscribeLocalEvent(OnFoldAttempt); - - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); - } - - public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component) - { - if (component != null) - return true; - - TryComp(uid, out var storage); - component = storage; - return component != null; - } -} +public sealed class EntityStorageSystem : SharedEntityStorageSystem; diff --git a/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs b/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs index b657414cdf6..301de2bf041 100644 --- a/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs +++ b/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs @@ -8,8 +8,6 @@ namespace Content.Client.Storage.Systems; /// public sealed class StorageContainerVisualsSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, StorageContainerVisualsComponent component, ref AppearanceChangeEvent args) { if (args.Sprite == null) diff --git a/Content.Client/Store/StoreSystem.cs b/Content.Client/Store/StoreSystem.cs new file mode 100644 index 00000000000..cb6e42a0d72 --- /dev/null +++ b/Content.Client/Store/StoreSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Store; + +namespace Content.Client.Store; + +public sealed class StoreSystem : SharedStoreSystem; diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index d8236604bff..36eec4a6713 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -1,7 +1,6 @@ +using System.Linq; using Content.Shared.Store; using JetBrains.Annotations; -using System.Linq; -using Content.Shared.Store.Components; using Robust.Client.UserInterface; using Robust.Shared.Prototypes; @@ -11,6 +10,7 @@ namespace Content.Client.Store.Ui; public sealed class StoreBoundUserInterface : BoundUserInterface { private IPrototypeManager _prototypeManager = default!; + private readonly StoreSystem _storeSystem = default!; [ViewVariables] private StoreMenu? _menu; @@ -23,6 +23,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { + _storeSystem = EntMan.System(); } protected override void Open() @@ -30,12 +31,12 @@ protected override void Open() base.Open(); _menu = this.CreateWindow(); - if (EntMan.TryGetComponent(Owner, out var store)) - _menu.Title = Loc.GetString(store.Name); + if (_storeSystem.TryGetStore(Owner, out var store)) + _menu.Title = Loc.GetString(store.Value.Comp.Name); _menu.OnListingButtonPressed += (_, listing) => { - SendMessage(new StoreBuyListingMessage(listing.ID)); + SendMessage(new StoreBuyListingMessage(listing.ID, EntMan.GetNetEntity(Owner))); }; _menu.OnCategoryButtonPressed += (_, category) => diff --git a/Content.Client/Strip/StrippingMenu.cs b/Content.Client/Strip/StrippingMenu.cs index 124356e0110..e3fa8277366 100644 --- a/Content.Client/Strip/StrippingMenu.cs +++ b/Content.Client/Strip/StrippingMenu.cs @@ -17,7 +17,7 @@ public sealed class StrippingMenu : DefaultWindow public StrippingMenu() { var box = new BoxContainer() { Orientation = LayoutOrientation.Vertical, Margin = new Thickness(0, 8) }; - Contents.AddChild(box); + ContentsContainer.AddChild(box); box.AddChild(SnareContainer); box.AddChild(HandsContainer); box.AddChild(InventoryContainer); diff --git a/Content.Client/Stylesheets/BaseStylesheet.Fonts.cs b/Content.Client/Stylesheets/BaseStylesheet.Fonts.cs new file mode 100644 index 00000000000..0b3e4aef977 --- /dev/null +++ b/Content.Client/Stylesheets/BaseStylesheet.Fonts.cs @@ -0,0 +1,66 @@ +using Content.Client.Stylesheets.Fonts; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Reflection; +using Robust.Shared.Sandboxing; +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client.Stylesheets; + +public abstract partial class BaseStylesheet : IStyleResources +{ + [Dependency] protected readonly ISandboxHelper SandboxHelper = default!; + [Dependency] protected readonly IReflectionManager ReflectionManager = default!; + [Dependency] protected internal readonly IResourceCache ResCache = default!; + + public Stylesheet Stylesheet { get; init; } + + public abstract NotoFontFamilyStack BaseFont { get; } + + /// + /// Get the style rules for the given font stack, with the provided sizes. + /// Does not set the 'default' font, but does create rules for every combination of font kind and font size. + /// This is intended for font sizes you think will be common/generally useful, for less common usecases prefer specifying the font explicitly. + /// + /// The prefix for the style classes, if any. + /// Font stack to use + /// A set of styleclasses and the associated size of font to use. + /// A rules list containing all combinations. + /// Use to get the appropriate styleclass for a font choice. + // god xmldoc refs are long ^^^ + // lmao + protected StyleRule[] GetRulesForFont(string? prefix, NotoFontFamilyStack stack, List<(string?, int)> sizes) // TODO: NotoFontFamilyStack is temporary + { + var rules = new List(); + + foreach (var (name, size) in sizes) + { + foreach (var kind in stack.AvailableKinds) + { + var builder = E().Class(GetFontClass(kind, prefix)); + + if (name is not null) + builder.Class(name); + + builder.Prop(Label.StylePropertyFont, stack.GetFont(size, kind)); + + rules.Add(builder); + } + } + + return rules.ToArray(); + } + + /// + /// Returns the appropriate styleclass for the given font configuration. + /// + /// + /// + /// + public static string GetFontClass(FontKind kind, string? prefix = null) + { + var kindStr = kind.ToString().ToLowerInvariant(); + return prefix is null ? $"font-{kindStr}" : $"{prefix}-{kindStr}"; + } +} diff --git a/Content.Client/Stylesheets/BaseStylesheet.Resources.cs b/Content.Client/Stylesheets/BaseStylesheet.Resources.cs new file mode 100644 index 00000000000..96e260a17a2 --- /dev/null +++ b/Content.Client/Stylesheets/BaseStylesheet.Resources.cs @@ -0,0 +1,108 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Client.ResourceManagement; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets; + +public abstract partial class BaseStylesheet +{ + /// + /// The file roots of the stylesheet, dictates where assets get read from for the given type of resource. + /// Roots will be checked in order for assets, avoid having a significant number of them. + /// + /// + /// Must be a constant, changes to this after construction will not be reflected. + /// + public abstract Dictionary Roots { get; } + + /// + /// Attempts to locate a resource within the stylesheet's roots. + /// + /// The relative path of the target resource. + /// The discovered/cached resource, if any. + /// Type of the resource to read. + /// Whether is null. + public bool TryGetResource(ResPath target, [NotNullWhen(true)] out T? resource) + where T : BaseResource, new() + { + DebugTools.Assert(target.IsRelative, + "Target path must be relative. Use ResCache directly if you need an absolute file location."); + + foreach (var root in Roots[typeof(T)]) + { + if (ResCache.TryGetResource(root / target, out resource)) + return true; + } + + resource = null; + return false; + } + + /// + /// Retrieves a resource, or throws. + /// + /// The relative path of the target resource. + /// Type of the resource to read. + /// The retrieved resource + /// Thrown if the resource does not exist within the stylesheet's roots. + public T GetResource(ResPath target) + where T : BaseResource, new() + { + if (TryGetResource(target, out T? res)) + return res; + + throw new MissingStyleResourceException(this, target.ToString()); + } + + /// + /// Retrieves a resource, or falls back to the specified root. The resource should be present at the fallback + /// root, or else it throws + /// + /// + /// This should be used to allow common sheetlets to be generic over multiple stylesheets without forcing other + /// styles to have the resource present, if your sheetlet is stylesheet-specific you should not use this. + /// + /// The relative path of the target resource. + /// The root that this resource will always exist at + /// Type of the resource to read. + /// The retrieved resource + /// Thrown if the resource does not exist in the fallback root. + public T GetResourceOr(ResPath target, ResPath fallbackRoot) + where T : BaseResource, new() + { + DebugTools.Assert(fallbackRoot.IsRooted, + "Fallback root must be absolute. Roots can be retrieved from the stylesheets."); + + if (!ResCache.TryGetResource(fallbackRoot / target, out T? fallback)) + throw new ExpectedResourceException(this, target.ToString()); + + return TryGetResource(target, out T? res) ? res : fallback; + } +} + +/// +/// Exception thrown when the never-fail helpers in fail to locate a resource. +/// +/// The stylesheet +/// +public sealed class MissingStyleResourceException(BaseStylesheet sheet, string target) : Exception +{ + public override string Message => + $"Failed to find any resource at \"{target}\" for {sheet}. The roots are: {sheet.Roots}"; + + public override string? Source => sheet.ToString(); +} + +/// +/// Exception thrown when the never-fail helpers in expect a resource at a location +/// but do not find it. +/// +/// The stylesheet +/// +public sealed class ExpectedResourceException(BaseStylesheet sheet, string target) : Exception +{ + public override string Message => + $"Failed to find any resource at \"{target}\" for {sheet}, when such a resource was expected."; + + public override string? Source => sheet.ToString(); +} diff --git a/Content.Client/Stylesheets/BaseStylesheet.Sheetlets.cs b/Content.Client/Stylesheets/BaseStylesheet.Sheetlets.cs new file mode 100644 index 00000000000..0e219fdbd7a --- /dev/null +++ b/Content.Client/Stylesheets/BaseStylesheet.Sheetlets.cs @@ -0,0 +1,49 @@ +using Robust.Client.UserInterface; + +namespace Content.Client.Stylesheets; + +public abstract partial class BaseStylesheet +{ + public StyleRule[] GetSheetletRules(Type sheetletTy, StylesheetManager man) + { + Sheetlet? sheetlet = null; + try + { + if (sheetletTy.ContainsGenericParameters) + { + if (SandboxHelper.CreateInstance(sheetletTy.MakeGenericType(typeof(TSheetTy))) is Sheetlet + sheetlet1) + sheetlet = sheetlet1; + } + else if (SandboxHelper.CreateInstance(sheetletTy) is Sheetlet sheetlet2) + { + sheetlet = sheetlet2; + } + } + // thrown when `sheetletTy.MakeGenericType` is given a type that does not satisfy the type constraints of + // `sheetletTy` + catch (ArgumentException) { } + + if (sheetlet is not null) + { + man.UnusedSheetlets.Remove(sheetletTy); + return sheetlet.GetRules((TSheetTy)(object)this, _config); + } + else + return []; + } + + public StyleRule[] GetAllSheetletRules(StylesheetManager man) + where TAttrib : Attribute + { + var tys = ReflectionManager.FindTypesWithAttribute(); + var rules = new List(); + + foreach (var ty in tys) + { + rules.AddRange(GetSheetletRules(ty, man)); + } + + return rules.ToArray(); + } +} diff --git a/Content.Client/Stylesheets/BaseStylesheet.Textures.cs b/Content.Client/Stylesheets/BaseStylesheet.Textures.cs new file mode 100644 index 00000000000..a951d88373a --- /dev/null +++ b/Content.Client/Stylesheets/BaseStylesheet.Textures.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets; + +public abstract partial class BaseStylesheet +{ + #region Texture helpers + + /// + /// Attempts to locate a texture within the stylesheet's roots. + /// + /// The relative path of the target resource. + /// The retrieved texture, if any. + /// Whether is null. + public bool TryGetTexture(ResPath target, [NotNullWhen(true)] out Texture? texture) + { + if (TryGetResource(target, out TextureResource? resource)) + { + texture = resource.Texture; + return true; + } + + texture = null; + return false; + } + + /// + /// Retrieves a texture, or throws. + /// + /// The relative path of the target texture. + /// The retrieved texture + /// Thrown if the texture does not exist within the stylesheet's roots. + public Texture GetTexture(ResPath target) + { + return GetResource(target).Texture; + } + + /// + /// Retrieves a texture, or falls back to the specified root. The resource should be present at the fallback + /// root, or else it throws + /// + /// + /// This should be used to allow common sheetlets to be generic over multiple stylesheets without forcing other + /// styles to have the resource present, if your sheetlet is stylesheet-specific you should not use this. + /// + /// The relative path of the target texture. + /// The root that this resource will always exist at + /// The retrieved texture + /// Thrown if the texture does not exist in the fallback root. + public Texture GetTextureOr(ResPath target, ResPath fallbackRoot) + { + return GetResourceOr(target, fallbackRoot).Texture; + } + + #endregion +} diff --git a/Content.Client/Stylesheets/BaseStylesheet.cs b/Content.Client/Stylesheets/BaseStylesheet.cs new file mode 100644 index 00000000000..6c12dfd7e39 --- /dev/null +++ b/Content.Client/Stylesheets/BaseStylesheet.cs @@ -0,0 +1,33 @@ +namespace Content.Client.Stylesheets; + +public abstract partial class BaseStylesheet +{ + /// + /// The type used to describe a configuration for this stylesheet. + /// This will be constructed and passed into the constructor, and may be used for other things as well. + /// + /// + /// Must be a constant, changes to this after construction will not be reflected. + /// TODO: this type is currently unused. It can be used to, for example, use user-defined fonts or colors. + /// + public virtual Type StylesheetConfigType => typeof(NoConfig); + + /// + /// ex. "Nanotrasen", "System", "Syndicate" + /// + public abstract string StylesheetName { get; } + + public record NoConfig(); + + private object _config; + + /// + /// This constructor will not access any virtual or abstract properties, so you can set them from your config. + /// + protected BaseStylesheet(object config) + { + IoCManager.InjectDependencies(this); + _config = config; + Stylesheet = default!; + } +} diff --git a/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs new file mode 100644 index 00000000000..6af9308bbc3 --- /dev/null +++ b/Content.Client/Stylesheets/Colorspace/ColorExtensions.cs @@ -0,0 +1,96 @@ +using System.Numerics; +using JetBrains.Annotations; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.Colorspace; + +[PublicAPI] +public static class ColorExtensions +{ + /// + /// Adjusts the lightness of a color using the Oklab color space. + /// + public static Color WithLightness(this Color c, float lightness) + { + DebugTools.Assert(lightness is >= 0.0f and <= 1.0f); + + var oklab = c.LabFromSrgb(); + oklab.X = lightness; + + return oklab.LabToSrgb(); + } + + /// + /// Nudges the lightness (in OKLAB space) of by / 100 units + /// + public static Color NudgeLightness(this Color c, float lightnessShift) + { + var oklab = c.LabFromSrgb(); + oklab.X = Math.Clamp(oklab.X + lightnessShift, 0, 1); + + return oklab.LabToSrgb(); + } + + /// + /// Nudges the chroma (in OKLCH space) of by / 100 units + /// + /// + /// Referenced from https://github.com/jonathantneal/convert-colors/blob/master/src/lab-lch.js + /// + public static Color NudgeChroma(this Color c, float chromaShift) + { + var oklab = c.LabFromSrgb(); + var oklch = Color.ToLch(oklab); + + oklch.Y = Math.Clamp(oklch.Y + chromaShift, 0, 1); + + return Color.FromLch(oklch).LabToSrgb(); + } + + /// + /// Blends two colors in the Oklab color space. + /// + public static Color OkBlend(this Color from, Color to, float factor) + { + DebugTools.Assert(factor is >= 0.0f and <= 1.0f); + + var okFrom = from.LabFromSrgb(); + var okTo = to.LabFromSrgb(); + + var blended = Vector4.Lerp(okFrom, okTo, factor); + return blended.LabToSrgb(); + } + + /// + /// Converts a nonlinear sRGB ("normal") color to OkLAB. + /// + public static Vector4 LabFromSrgb(this Color from) + { + return Color.ToLab(Color.FromSrgb(from)); + } + + /// + /// Converts OkLAB to a nonlinear sRGB ("normal") color. + /// + public static Color LabToSrgb(this Vector4 from) + { + return Color.ToSrgb(Color.FromLab(from).SimpleClipGamut()); + } + + /// + /// Clips the gamut of the color so that all color channels are in the range 0 -> 1. + /// + /// + /// This uses no clever perceptual techniques, it literally just clamps the individual channels. + /// + public static Color SimpleClipGamut(this Color from) + { + return new Color + { + R = Math.Clamp(from.R, 0, 1), + G = Math.Clamp(from.G, 0, 1), + B = Math.Clamp(from.B, 0, 1), + A = from.A, + }; + } +} diff --git a/Content.Client/Stylesheets/CommonSheetletAttribute.cs b/Content.Client/Stylesheets/CommonSheetletAttribute.cs new file mode 100644 index 00000000000..1fdf78a5020 --- /dev/null +++ b/Content.Client/Stylesheets/CommonSheetletAttribute.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; + +namespace Content.Client.Stylesheets; + +/// +/// Attribute used to mark a sheetlet class. Stylesheets can use this attribute to locate and load sheetlets. +/// +[PublicAPI] +[MeansImplicitUse] +public sealed class CommonSheetletAttribute : Attribute +{ + +} diff --git a/Content.Client/Stylesheets/CommonStylesheet.cs b/Content.Client/Stylesheets/CommonStylesheet.cs new file mode 100644 index 00000000000..72b58e56483 --- /dev/null +++ b/Content.Client/Stylesheets/CommonStylesheet.cs @@ -0,0 +1,83 @@ +using Content.Client.Stylesheets.Palette; +using Content.Client.Stylesheets.SheetletConfigs; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets; + +public abstract class CommonStylesheet : PalettedStylesheet, IButtonConfig, IWindowConfig, IIconConfig, ITabContainerConfig, + ISliderConfig, IRadialMenuConfig, IPlaceholderConfig, ITooltipConfig, IPanelConfig, INanoHeadingConfig, + ILineEditConfig, IStripebackConfig, ICheckboxConfig, ISwitchButtonConfig +{ + /// + /// This constructor will not access any virtual or abstract properties, so you can set them from your config. + /// + protected CommonStylesheet(object config) : base(config) { } + + ResPath ICheckboxConfig.CheckboxUncheckedPath => new("checkbox_unchecked.svg.96dpi.png"); + ResPath ICheckboxConfig.CheckboxCheckedPath => new("checkbox_checked.svg.96dpi.png"); + + ResPath IStripebackConfig.StripebackPath => new("stripeback.svg.96dpi.png"); + + ResPath INanoHeadingConfig.NanoHeadingPath => new("nanoheading.svg.96dpi.png"); + + ResPath ILineEditConfig.LineEditPath => new("lineedit.png"); + + ResPath IPanelConfig.GeometricPanelBorderPath => new("geometric_panel_border.svg.96dpi.png"); + ResPath IPanelConfig.BlackPanelDarkThinBorderPath => new("black_panel_dark_thin_border.png"); + + ResPath ITooltipConfig.TooltipBoxPath => new("tooltip.png"); + ResPath ITooltipConfig.WhisperBoxPath => new("whisper.png"); + + ResPath IPlaceholderConfig.PlaceholderPath => new("placeholder.png"); + + ResPath IRadialMenuConfig.ButtonNormalPath => new("Radial/button_normal.png"); + ResPath IRadialMenuConfig.ButtonHoverPath => new("Radial/button_hover.png"); + ResPath IRadialMenuConfig.CloseNormalPath => new("Radial/close_normal.png"); + ResPath IRadialMenuConfig.CloseHoverPath => new("Radial/close_hover.png"); + ResPath IRadialMenuConfig.BackNormalPath => new("Radial/back_normal.png"); + ResPath IRadialMenuConfig.BackHoverPath => new("Radial/back_hover.png"); + + ResPath ISliderConfig.SliderFillPath => new("slider_fill.svg.96dpi.png"); + + ResPath ISliderConfig.SliderOutlinePath => new("slider_outline.svg.96dpi.png"); + + ResPath ISliderConfig.SliderGrabber => new("slider_grabber.svg.96dpi.png"); + + + ResPath ITabContainerConfig.TabContainerPanelPath => new("tabcontainer_panel.png"); + + ResPath IIconConfig.HelpIconPath => new("help.png"); + ResPath IIconConfig.CrossIconPath => new("cross.svg.png"); + ResPath IIconConfig.RefreshIconPath => new("circular_arrow.svg.96dpi.png"); + ResPath IIconConfig.InvertedTriangleIconPath => new("inverted_triangle.svg.png"); + + ResPath IWindowConfig.WindowHeaderTexturePath => new("window_header.png"); + ResPath IWindowConfig.WindowHeaderAlertTexturePath => new("window_header_alert.png"); + ResPath IWindowConfig.WindowBackgroundPath => new("window_background.png"); + ResPath IWindowConfig.WindowBackgroundBorderedPath => new("window_background_bordered.png"); + ResPath IWindowConfig.TransparentWindowBackgroundBorderedPath => new("transparent_window_background_bordered.png"); + + ResPath IButtonConfig.BaseButtonPath => new("button.svg.96dpi.png"); + ResPath IButtonConfig.OpenLeftButtonPath => new("button.svg.96dpi.png"); + ResPath IButtonConfig.OpenRightButtonPath => new("button.svg.96dpi.png"); + ResPath IButtonConfig.OpenBothButtonPath => new("button.svg.96dpi.png"); + ResPath IButtonConfig.SmallButtonPath => new("button_small.svg.96dpi.png"); + ResPath IButtonConfig.RoundedButtonPath => new("rounded_button.svg.96dpi.png"); + ResPath IButtonConfig.RoundedButtonBorderedPath => new("rounded_button_bordered.svg.96dpi.png"); + + ResPath IButtonConfig.MonotoneBaseButtonPath => new("Monotone/monotone_button.svg.96dpi.png"); + ResPath IButtonConfig.MonotoneOpenLeftButtonPath => new("Monotone/monotone_button_open_left.svg.96dpi.png"); + ResPath IButtonConfig.MonotoneOpenRightButtonPath => new("Monotone/monotone_button_open_right.svg.96dpi.png"); + ResPath IButtonConfig.MonotoneOpenBothButtonPath => new("Monotone/monotone_button_open_both.svg.96dpi.png"); + + ColorPalette IButtonConfig.ButtonPalette => PrimaryPalette with { PressedElement = PositivePalette.PressedElement }; + ColorPalette IButtonConfig.PositiveButtonPalette => PositivePalette; + ColorPalette IButtonConfig.NegativeButtonPalette => NegativePalette; + + ResPath ISwitchButtonConfig.SwitchButtonTrackFillPath => new("switchbutton_track_fill.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonTrackOutlinePath => new("switchbutton_track_outline.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonThumbFillPath => new("switchbutton_thumb_fill.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonThumbOutlinePath => new("switchbutton_thumb_outline.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonSymbolOffPath => new("switchbutton_symbol_off.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonSymbolOnPath => new("switchbutton_symbol_on.svg.96dpi.png"); +} diff --git a/Content.Client/Stylesheets/Fonts/FontKind.cs b/Content.Client/Stylesheets/Fonts/FontKind.cs new file mode 100644 index 00000000000..fef2b6bc46b --- /dev/null +++ b/Content.Client/Stylesheets/Fonts/FontKind.cs @@ -0,0 +1,57 @@ +namespace Content.Client.Stylesheets.Fonts; + +/// +/// The available kinds of font. +/// +public enum FontKind +{ + Regular, + Bold, + Italic, + BoldItalic, +} + +public static class FontKindExtensions +{ + internal static string AsFileName(this FontKind kind) + { + return kind switch + { + FontKind.Regular => "Regular", + FontKind.Bold => "Bold", + FontKind.Italic => "Italic", + FontKind.BoldItalic => "BoldItalic", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; + } + + internal static bool IsBold(this FontKind kind) + { + return kind is FontKind.Bold or FontKind.BoldItalic; + } + + internal static bool IsItalic(this FontKind kind) + { + return kind is FontKind.Italic or FontKind.BoldItalic; + } + + internal static FontKind SimplifyCompound(this FontKind kind) + { + return kind switch + { + FontKind.BoldItalic => FontKind.Bold, + _ => kind, + }; + } + + + internal static FontKind RegularOr(this FontKind kind, FontKind other) + { + return kind switch + { + FontKind.Regular => FontKind.Regular, + _ => other, + }; + } +} + diff --git a/Content.Client/Stylesheets/Fonts/NotoFontFamilyStack.cs b/Content.Client/Stylesheets/Fonts/NotoFontFamilyStack.cs new file mode 100644 index 00000000000..9239e5d1a10 --- /dev/null +++ b/Content.Client/Stylesheets/Fonts/NotoFontFamilyStack.cs @@ -0,0 +1,90 @@ +using Content.Client.Resources; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; + +namespace Content.Client.Stylesheets.Fonts; + +/// +/// This class should have a base type. The whole font system is currently kind of bad and completely temporary. +/// This class is just here because it does sort of work. +/// TODO: fix (once engine support is added for font properties?) +/// +/// +/// +[PublicAPI] +public sealed class NotoFontFamilyStack(IResourceCache resCache, string variant = "") +{ + /// + /// The primary font path, with string substitution markers. + /// + /// + /// If using the default GetFontPaths function, the substitutions are as follows: + /// 0 is the font kind. + /// 1 is the font kind with BoldItalic replaced with Bold when it occurs. + /// + private string _fontPrimary = $"/Fonts/NotoSans{variant}/NotoSans{variant}-{{0}}.ttf"; + + /// + /// The symbols font path, with string substitution markers. + /// + /// + /// If using the default GetFontPaths function, the substitutions are as follows: + /// 0 is the font kind. + /// 1 is the font kind with BoldItalic replaced with Bold when it occurs. + /// + private string _fontSymbols = "/Fonts/NotoSans/NotoSansSymbols-{2}.ttf"; + + /// + /// The fallback font path, exactly. (no string substitutions.) + /// + private string[] _extras = new[] { "/Fonts/NotoSans/NotoSansSymbols2-Regular.ttf" }; + + public HashSet AvailableKinds = [FontKind.Regular, FontKind.Bold, FontKind.Italic, FontKind.BoldItalic]; + + /// + /// This should return the paths of every font in this stack given the abstract members. + /// + /// Which font kind to use. + /// An array of + private string[] GetFontPaths(FontKind kind) + { + if (!AvailableKinds.Contains(kind)) + { + if (kind == FontKind.BoldItalic && AvailableKinds.Contains(FontKind.Bold)) + { + kind = FontKind.Bold; + } + else + { + kind = FontKind.Regular; + } + } + + var simpleKindStr = kind.SimplifyCompound().AsFileName(); + var boldOrRegularStr = kind.RegularOr(FontKind.Bold).AsFileName(); + + var kindStr = kind.AsFileName(); + var fontList = new List() + { + string.Format(_fontPrimary, kindStr, simpleKindStr, boldOrRegularStr), + string.Format(_fontSymbols, kindStr, simpleKindStr, boldOrRegularStr), + }; + fontList.AddRange(_extras); + return fontList.ToArray(); + } + + /// + /// Retrieves an in-style font, of the provided size and kind. + /// + /// Size of the font to provide. + /// Optional font kind. Defaults to Regular. + /// A Font resource. + public Font GetFont(int size, FontKind kind = FontKind.Regular) + { + //ALDebugTools.AssertContains(AvailableKinds, kind); + var paths = GetFontPaths(kind); + + return resCache.GetFont(paths, size); + } +} diff --git a/Content.Client/Stylesheets/ISheetletConfig.cs b/Content.Client/Stylesheets/ISheetletConfig.cs new file mode 100644 index 00000000000..277006c6f8a --- /dev/null +++ b/Content.Client/Stylesheets/ISheetletConfig.cs @@ -0,0 +1,6 @@ +namespace Content.Client.Stylesheets; + +public interface ISheetletConfig : IStyleResources +{ + // For access. +} diff --git a/Content.Client/Stylesheets/IStyleResources.cs b/Content.Client/Stylesheets/IStyleResources.cs new file mode 100644 index 00000000000..ed842ace819 --- /dev/null +++ b/Content.Client/Stylesheets/IStyleResources.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets; + +public interface IStyleResources +{ + /// + /// The file roots of the stylesheet, dictates where assets get read from for the given type of resource. + /// Roots will be checked in order for assets, avoid having a significant number of them. + /// + /// + /// Must be a constant, changes to this after construction will not be reflected. + /// + Dictionary Roots { get; } + + /// + /// Attempts to locate a resource within the stylesheet's roots. + /// + /// The relative path of the target resource. + /// The discovered/cached resource, if any. + /// Type of the resource to read. + /// Whether is null. + bool TryGetResource(ResPath target, [NotNullWhen(true)] out T? resource) + where T : BaseResource, new(); + + /// + /// Retrieves a resource, or throws. + /// + /// The relative path of the target resource. + /// Type of the resource to read. + /// The retrieved resource + /// Thrown if the resource does not exist within the stylesheet's roots. + T GetResource(ResPath target) + where T : BaseResource, new(); + + /// + /// Retrieves a resource, or falls back to the specified root. The resource should be present at the fallback + /// root, or else it throws + /// + /// + /// This should be used to allow common sheetlets to be generic over multiple stylesheets without forcing other + /// styles to have the resource present, if your sheetlet is stylesheet-specific you should not use this. + /// + /// The relative path of the target resource. + /// The root that this resource will always exist at + /// Type of the resource to read. + /// The retrieved resource + /// Thrown if the resource does not exist in the fallback root. + T GetResourceOr(ResPath target, ResPath fallbackRoot) + where T : BaseResource, new(); + + /// + /// Attempts to locate a texture within the stylesheet's roots. + /// + /// The relative path of the target resource. + /// The retrieved texture, if any. + /// Whether is null. + bool TryGetTexture(ResPath target, [NotNullWhen(true)] out Texture? texture); + + /// + /// Retrieves a texture, or throws. + /// + /// The relative path of the target texture. + /// The retrieved texture + /// Thrown if the texture does not exist within the stylesheet's roots. + Texture GetTexture(ResPath target); + + /// + /// Retrieves a texture, or falls back to the specified root. The resource should be present at the fallback + /// root, or else it throws + /// + /// + /// This should be used to allow common sheetlets to be generic over multiple stylesheets without forcing other + /// styles to have the resource present, if your sheetlet is stylesheet-specific you should not use this. + /// + /// The relative path of the target texture. + /// The root that this resource will always exist at + /// The retrieved texture + /// Thrown if the texture does not exist in the fallback root. + Texture GetTextureOr(ResPath target, ResPath fallbackRoot); +} diff --git a/Content.Client/Stylesheets/IStylesheetManager.cs b/Content.Client/Stylesheets/IStylesheetManager.cs index 134c7fd3bec..2fa795a9dea 100644 --- a/Content.Client/Stylesheets/IStylesheetManager.cs +++ b/Content.Client/Stylesheets/IStylesheetManager.cs @@ -1,12 +1,32 @@ +using System.Diagnostics.CodeAnalysis; using Robust.Client.UserInterface; -namespace Content.Client.Stylesheets +namespace Content.Client.Stylesheets; + +public interface IStylesheetManager { - public interface IStylesheetManager - { - Stylesheet SheetNano { get; } - Stylesheet SheetSpace { get; } + /// Nanotrasen styles: the default style! Use this for most UIs + Stylesheet SheetNanotrasen { get; } + + /// + /// System styles: use this for any admin / debug menus, and any odds and ends (like the changelog for some reason) + /// + Stylesheet SheetSystem { get; } + + + [Obsolete("Update to use SheetNanotrasen instead")] + Stylesheet SheetNano { get; } + + [Obsolete("Update to use SheetSystem instead")] + Stylesheet SheetSpace { get; } + + /// get a stylesheet by name + public bool TryGetStylesheet(string name, [MaybeNullWhen(false)] out Stylesheet stylesheet); + + void Initialize(); - void Initialize(); - } + /// + /// Sheetlets marked with CommonSheetlet that have not satisfied the type constraints of any stylesheet + /// + public HashSet UnusedSheetlets { get; } } diff --git a/Content.Client/Stylesheets/NTSheetlets/NanoHeadingSheetlet.cs b/Content.Client/Stylesheets/NTSheetlets/NanoHeadingSheetlet.cs new file mode 100644 index 00000000000..fb6d37ac0d1 --- /dev/null +++ b/Content.Client/Stylesheets/NTSheetlets/NanoHeadingSheetlet.cs @@ -0,0 +1,36 @@ +using Content.Client.Stylesheets.SheetletConfigs; +using Content.Client.Stylesheets.Stylesheets; +using Content.Client.UserInterface.Controls; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client.Stylesheets.NTSheetlets; + +/// Not NTHeading because NanoHeading is the name of the element +[CommonSheetlet] +public sealed class NanoHeadingSheetlet : Sheetlet +{ + public override StyleRule[] GetRules(NanotrasenStylesheet sheet, object config) + { + INanoHeadingConfig nanoHeadingCfg = sheet; + + var nanoHeadingTex = sheet.GetTexture(nanoHeadingCfg.NanoHeadingPath); + var nanoHeadingBox = new StyleBoxTexture + { + Texture = nanoHeadingTex, + PatchMarginRight = 10, + PatchMarginTop = 10, + ContentMarginTopOverride = 2, + ContentMarginLeftOverride = 10, + PaddingTop = 4, + }; + nanoHeadingBox.SetPatchMargin(StyleBox.Margin.Left | StyleBox.Margin.Bottom, 2); + + return + [ + E().ParentOf(E()).Panel(nanoHeadingBox), + ]; + } +} diff --git a/Content.Client/Stylesheets/Palette/ColorPalette.cs b/Content.Client/Stylesheets/Palette/ColorPalette.cs new file mode 100644 index 00000000000..89405e6cb40 --- /dev/null +++ b/Content.Client/Stylesheets/Palette/ColorPalette.cs @@ -0,0 +1,81 @@ +using Content.Client.Stylesheets.Colorspace; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Content.Client.Stylesheets.Palette; + +/// +/// Don't be afraid to add a lot of fields here! This class is made for readability. +/// +public record ColorPalette( + Color Base, + + float LightnessShift, + float ChromaShift, + + Color Element, + Color HoveredElement, + Color PressedElement, + Color DisabledElement, + + Color Background, + Color BackgroundLight, + Color BackgroundDark, + + Color Text, + Color TextDark +) +{ + /// + /// Helper method for generating a ColorPalette from a specified base hex string, with the + /// option to override specific parts of the palette + /// + public static ColorPalette FromHexBase( + string hex, + float lightnessShift = 0.06f, + float chromaShift = 0.00f, + Color? element = null, + Color? background = null, + Color? text = null + ) + { + var @base = Color.FromHex(hex); + + element ??= Shift(@base, lightnessShift, chromaShift, -1); // Shift(@base, -1) + var hoveredElement = Shift(element.Value, lightnessShift, chromaShift, 1); // Shift(@base, 0) + var pressedElement = Shift(element.Value, lightnessShift, chromaShift, -1); // Shift(@base, -2) + var disabledElement = Shift(element.Value, lightnessShift, chromaShift, -2) // Shift(@base, -3) + .NudgeChroma(-chromaShift * 2); + + background ??= Shift(@base, lightnessShift, chromaShift, -3); // Shift(@base, -3) + var backgroundLight = Shift(background.Value, lightnessShift, chromaShift, 1); // Shift(@base, -2) + var backgroundDark = Shift(background.Value, lightnessShift, chromaShift, -1); // Shift(@base, -4) + + text ??= @base; // Shift(@base, 0) + var textDark = Shift(text.Value, lightnessShift, chromaShift, -1); // Shift(@base, -1) + + return new ColorPalette( + Base: @base, + + LightnessShift: lightnessShift, + ChromaShift: chromaShift, + + Element: element.Value, + HoveredElement: hoveredElement, + PressedElement: pressedElement, + DisabledElement: disabledElement, + + Background: background.Value, + BackgroundLight: backgroundLight, + BackgroundDark: backgroundDark, + + Text: text.Value, + TextDark: textDark + ); + } + + private static Color Shift(Color from, float lightnessShift, float chromaShift, float factor) + { + return from.NudgeLightness(lightnessShift * factor).NudgeChroma(chromaShift * factor); + } +} diff --git a/Content.Client/Stylesheets/Palette/Palettes.cs b/Content.Client/Stylesheets/Palette/Palettes.cs new file mode 100644 index 00000000000..aab61ac69a3 --- /dev/null +++ b/Content.Client/Stylesheets/Palette/Palettes.cs @@ -0,0 +1,30 @@ +namespace Content.Client.Stylesheets.Palette; + +/// +/// Stores all style palettes in one accessible location +/// +/// +/// Technically not limited to only colors, can store like, standard padding amounts, and font sizes, maybe? +/// +public static class Palettes +{ + // muted tones + public static readonly ColorPalette Navy = ColorPalette.FromHexBase("#4f5376", lightnessShift: 0.05f, chromaShift: 0.0045f); + public static readonly ColorPalette Cyan = ColorPalette.FromHexBase("#42586a", lightnessShift: 0.05f, chromaShift: 0.0045f); + public static readonly ColorPalette Slate = ColorPalette.FromHexBase("#545562"); + public static readonly ColorPalette Neutral = ColorPalette.FromHexBase("#555555"); + + // status tones + public static readonly ColorPalette Red = ColorPalette.FromHexBase("#b62124", chromaShift: 0.02f); + public static readonly ColorPalette Amber = ColorPalette.FromHexBase("#c18e36"); + public static readonly ColorPalette Green = ColorPalette.FromHexBase("#3c854a"); + public static readonly StatusPalette Status = new([Red.Base, Amber.Base, Green.Base]); + + // highlight tones + public static readonly ColorPalette Gold = ColorPalette.FromHexBase("#a88b5e"); + public static readonly ColorPalette Maroon = ColorPalette.FromHexBase("#9b2236"); + + // Intended to be used with `ModulateSelf` to darken / lighten something + public static readonly ColorPalette AlphaModulate = ColorPalette.FromHexBase("#ffffff"); + +} diff --git a/Content.Client/Stylesheets/Palette/StatusPalette.cs b/Content.Client/Stylesheets/Palette/StatusPalette.cs new file mode 100644 index 00000000000..f2b19cf8c2c --- /dev/null +++ b/Content.Client/Stylesheets/Palette/StatusPalette.cs @@ -0,0 +1,57 @@ +using Content.Client.Stylesheets.Colorspace; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.Palette; + +public sealed class StatusPalette +{ + /// The status colors to blend between, these are equally spaced from 0 to 1 with 0 being StatusColors[0], and 1 being + /// StatusColors.Last() + /// + /// Traditionally, something like { Red, Yellow, Green } + public readonly Color[] StatusColors; + + /// StatusPalette.GetStatusColor(1.0f) + public readonly Color Good; + /// StatusPalette.GetStatusColor(0.75f) + public readonly Color Okay; + /// StatusPalette.GetStatusColor(0.5f) + public readonly Color Warning; + /// StatusPalette.GetStatusColor(0.25f) + public readonly Color Bad; + /// StatusPalette.GetStatusColor(0.0f) + public readonly Color Critical; + + public StatusPalette(Color[] statusColors) + { + StatusColors = statusColors; + Good = GetStatusColor(1.0f); + Okay = GetStatusColor(0.75f); + Warning = GetStatusColor(0.5f); + Bad = GetStatusColor(0.25f); + Critical = GetStatusColor(0.0f); + } + + /// The severity of this status from 0 to 1. Traditionally, 1 will be green and 0 red + /// The color for this status, linearly Oklab blended between equal intervals of the colors in StatusColor + public Color GetStatusColor(float factor) + { + DebugTools.Assert(factor >= 0.0 && factor <= 1.0); + DebugTools.Assert(StatusColors.Length >= 1); + + if (StatusColors.Length == 1) + return StatusColors[0]; + + var intervals = StatusColors.Length - 1; + + var fromIdx = (int) Math.Floor(factor * intervals); + if (factor is 1.0f or 0.0f) + return StatusColors[fromIdx]; + + var from = StatusColors[fromIdx]; + var to = StatusColors[fromIdx + 1]; + var f = (factor - (float) fromIdx / intervals) * intervals; + + return from.OkBlend(to, f); + } +} diff --git a/Content.Client/Stylesheets/PalettedStylesheet.Palettes.cs b/Content.Client/Stylesheets/PalettedStylesheet.Palettes.cs new file mode 100644 index 00000000000..e8fe7174f4d --- /dev/null +++ b/Content.Client/Stylesheets/PalettedStylesheet.Palettes.cs @@ -0,0 +1,12 @@ +using Content.Client.Stylesheets.Palette; + +namespace Content.Client.Stylesheets; + +public abstract partial class PalettedStylesheet +{ + public abstract ColorPalette PrimaryPalette { get; } + public abstract ColorPalette SecondaryPalette { get; } + public abstract ColorPalette PositivePalette { get; } + public abstract ColorPalette NegativePalette { get; } + public abstract ColorPalette HighlightPalette { get; } +} diff --git a/Content.Client/Stylesheets/PalettedStylesheet.cs b/Content.Client/Stylesheets/PalettedStylesheet.cs new file mode 100644 index 00000000000..682a50c228a --- /dev/null +++ b/Content.Client/Stylesheets/PalettedStylesheet.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; + +namespace Content.Client.Stylesheets; + +/// +/// The base class for all stylesheets, providing core functionality and helpers. +/// +[PublicAPI] +public abstract partial class PalettedStylesheet : BaseStylesheet +{ + protected PalettedStylesheet(object config) : base(config) + { + } +} diff --git a/Content.Client/Stylesheets/Sheetlet.cs b/Content.Client/Stylesheets/Sheetlet.cs new file mode 100644 index 00000000000..7f55b5a7c30 --- /dev/null +++ b/Content.Client/Stylesheets/Sheetlet.cs @@ -0,0 +1,16 @@ +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; + +namespace Content.Client.Stylesheets; + +public abstract class Sheetlet +{ + [Dependency] protected readonly IResourceCache ResCache = default!; + + protected Sheetlet() + { + IoCManager.InjectDependencies(this); + } + + public abstract StyleRule[] GetRules(T sheet, object config); +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/IButtonConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IButtonConfig.cs new file mode 100644 index 00000000000..01155c6e465 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IButtonConfig.cs @@ -0,0 +1,23 @@ +using Content.Client.Stylesheets.Palette; +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IButtonConfig : ISheetletConfig +{ + public ResPath BaseButtonPath { get; } + public ResPath OpenLeftButtonPath { get; } + public ResPath OpenRightButtonPath { get; } + public ResPath OpenBothButtonPath { get; } + public ResPath SmallButtonPath { get; } + public ResPath RoundedButtonPath { get; } + public ResPath RoundedButtonBorderedPath { get; } + public ResPath MonotoneBaseButtonPath { get; } + public ResPath MonotoneOpenLeftButtonPath { get; } + public ResPath MonotoneOpenRightButtonPath { get; } + public ResPath MonotoneOpenBothButtonPath { get; } + + public ColorPalette ButtonPalette { get; } + public ColorPalette PositiveButtonPalette { get; } + public ColorPalette NegativeButtonPalette { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/ICheckboxConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ICheckboxConfig.cs new file mode 100644 index 00000000000..1f3e54c0ad8 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ICheckboxConfig.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ICheckboxConfig +{ + public ResPath CheckboxUncheckedPath { get; } + public ResPath CheckboxCheckedPath { get; } +} + diff --git a/Content.Client/Stylesheets/SheetletConfigs/IIconConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IIconConfig.cs new file mode 100644 index 00000000000..837e5f88d4d --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IIconConfig.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IIconConfig : ISheetletConfig +{ + public ResPath HelpIconPath { get; } + public ResPath CrossIconPath { get; } + public ResPath RefreshIconPath { get; } + public ResPath InvertedTriangleIconPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/ILineEditConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ILineEditConfig.cs new file mode 100644 index 00000000000..10540911e0f --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ILineEditConfig.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ILineEditConfig : ISheetletConfig +{ + public ResPath LineEditPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/INanoHeadingConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/INanoHeadingConfig.cs new file mode 100644 index 00000000000..4804c1ad971 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/INanoHeadingConfig.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface INanoHeadingConfig : ISheetletConfig +{ + public ResPath NanoHeadingPath { get; } +} + diff --git a/Content.Client/Stylesheets/SheetletConfigs/IPanelConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IPanelConfig.cs new file mode 100644 index 00000000000..6e12a75ed26 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IPanelConfig.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IPanelConfig : ISheetletConfig +{ + public ResPath GeometricPanelBorderPath { get; } + public ResPath BlackPanelDarkThinBorderPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/IPlaceholderConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IPlaceholderConfig.cs new file mode 100644 index 00000000000..81dd6e3dac2 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IPlaceholderConfig.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IPlaceholderConfig : ISheetletConfig +{ + public ResPath PlaceholderPath { get; } +} + diff --git a/Content.Client/Stylesheets/SheetletConfigs/IRadialMenuConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IRadialMenuConfig.cs new file mode 100644 index 00000000000..6cd142daf62 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IRadialMenuConfig.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IRadialMenuConfig : ISheetletConfig +{ + public ResPath ButtonNormalPath { get; } + public ResPath ButtonHoverPath { get; } + public ResPath CloseNormalPath { get; } + public ResPath CloseHoverPath { get; } + public ResPath BackNormalPath { get; } + public ResPath BackHoverPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/ISliderConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ISliderConfig.cs new file mode 100644 index 00000000000..037fb4ee203 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ISliderConfig.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ISliderConfig : ISheetletConfig +{ + public ResPath SliderFillPath { get; } + public ResPath SliderOutlinePath { get; } + public ResPath SliderGrabber { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/IStripebackConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IStripebackConfig.cs new file mode 100644 index 00000000000..90b3daf5ae6 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IStripebackConfig.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IStripebackConfig : ISheetletConfig +{ + public ResPath StripebackPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs new file mode 100644 index 00000000000..0697bc96263 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ISwitchButtonConfig +{ + public ResPath SwitchButtonTrackFillPath { get; } + public ResPath SwitchButtonTrackOutlinePath { get; } + public ResPath SwitchButtonThumbFillPath { get; } + public ResPath SwitchButtonThumbOutlinePath { get; } + public ResPath SwitchButtonSymbolOffPath { get; } + public ResPath SwitchButtonSymbolOnPath { get; } +} + diff --git a/Content.Client/Stylesheets/SheetletConfigs/ITabContainerConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ITabContainerConfig.cs new file mode 100644 index 00000000000..bfd33d90092 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ITabContainerConfig.cs @@ -0,0 +1,8 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ITabContainerConfig : ISheetletConfig +{ + public ResPath TabContainerPanelPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/ITooltipConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ITooltipConfig.cs new file mode 100644 index 00000000000..eef4e7fc255 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ITooltipConfig.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ITooltipConfig : ISheetletConfig +{ + public ResPath TooltipBoxPath { get; } + public ResPath WhisperBoxPath { get; } +} diff --git a/Content.Client/Stylesheets/SheetletConfigs/IWindowConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/IWindowConfig.cs new file mode 100644 index 00000000000..669d2ce0a30 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/IWindowConfig.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface IWindowConfig : ISheetletConfig +{ + public ResPath WindowHeaderTexturePath { get; } + public ResPath WindowHeaderAlertTexturePath { get; } + public ResPath WindowBackgroundPath { get; } + public ResPath WindowBackgroundBorderedPath { get; } + public ResPath TransparentWindowBackgroundBorderedPath { get; } +} diff --git a/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs new file mode 100644 index 00000000000..8b92e1410aa --- /dev/null +++ b/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs @@ -0,0 +1,186 @@ +using System.Numerics; +using Content.Client.Stylesheets.Palette; +using Content.Client.Stylesheets.SheetletConfigs; +using Content.Client.Stylesheets.Stylesheets; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using static Content.Client.Stylesheets.StylesheetHelpers; + +namespace Content.Client.Stylesheets.Sheetlets; + +[CommonSheetlet] +public sealed class ButtonSheetlet : Sheetlet where T : PalettedStylesheet, IButtonConfig, IIconConfig +{ + public override StyleRule[] GetRules(T sheet, object config) + { + IButtonConfig buttonCfg = sheet; + IIconConfig iconCfg = sheet; + + var crossTex = sheet.GetTextureOr(iconCfg.CrossIconPath, NanotrasenStylesheet.TextureRoot); + var refreshTex = sheet.GetTextureOr(iconCfg.RefreshIconPath, NanotrasenStylesheet.TextureRoot); + var helpTex = sheet.GetTextureOr(iconCfg.HelpIconPath, NanotrasenStylesheet.TextureRoot); + + var rules = new List + { + // Set textures for the kinds of buttons + CButton() + .Box(StyleBoxHelpers.BaseStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonOpenLeft) + .Box(StyleBoxHelpers.OpenLeftStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonOpenRight) + .Box(StyleBoxHelpers.OpenRightStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonOpenBoth) + .Box(StyleBoxHelpers.SquareStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonSquare) + .Box(StyleBoxHelpers.SquareStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonSmall) + .Box(StyleBoxHelpers.SmallStyleBox(sheet)), + CButton() + .Class(StyleClass.ButtonSmall) + .ParentOf(E -[Virtual] public abstract class RadialMenuButtonBase : BaseButton { /// diff --git a/Content.Client/UserInterface/Controls/SlotControl.cs b/Content.Client/UserInterface/Controls/SlotControl.cs index 2b43f2397de..d55b4acf55e 100644 --- a/Content.Client/UserInterface/Controls/SlotControl.cs +++ b/Content.Client/UserInterface/Controls/SlotControl.cs @@ -9,7 +9,6 @@ namespace Content.Client.UserInterface.Controls { - [Virtual] public abstract class SlotControl : Control, IEntityControl { public static int DefaultButtonSize = 64; diff --git a/Content.Client/UserInterface/Controls/SwitchButton.cs b/Content.Client/UserInterface/Controls/SwitchButton.cs new file mode 100644 index 00000000000..cf7af5baf34 --- /dev/null +++ b/Content.Client/UserInterface/Controls/SwitchButton.cs @@ -0,0 +1,317 @@ +using System.Numerics; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.UserInterface.Controls +{ + /// + /// A type of toggleable button that a switch icon and a secondary text label both showing the current state + /// + [Virtual] + public class SwitchButton : ContainerButton + { + public const string StyleClassTrackFill = "trackFill"; + public const string StyleClassTrackOutline = "trackOutline"; + public const string StyleClassThumbFill = "thumbFill"; + public const string StyleClassThumbOutline = "thumbOutline"; + public const string StyleClassSymbol = "symbol"; + + public const string StylePropertySeparation = "separation"; + + private const int DefaultSeparation = 0; + + private int ActualSeparation + { + get + { + if (TryGetStyleProperty(StylePropertySeparation, out int separation)) + { + return separation; + } + + return SeparationOverride ?? DefaultSeparation; + } + } + + public int? SeparationOverride { get; set; } + public Label Label { get; } + public Label OffStateLabel { get; } + public Label OnStateLabel { get; } + + // I tried to find a way not to have five textures here, but the other + // options were worse. + public TextureRect TrackFill { get; } + public TextureRect TrackOutline { get; } + public TextureRect ThumbFill { get; } + public TextureRect ThumbOutline { get; } + public TextureRect Symbol { get; } + + public SwitchButton() + { + ToggleMode = true; + + TrackFill = new TextureRect + { + StyleClasses = { StyleClassTrackFill }, + VerticalAlignment = VAlignment.Center, + }; + + TrackOutline = new TextureRect + { + StyleClasses = { StyleClassTrackOutline }, + VerticalAlignment = VAlignment.Center, + }; + + ThumbFill = new TextureRect + { + StyleClasses = { StyleClassThumbFill }, + VerticalAlignment = VAlignment.Center, + }; + + ThumbOutline = new TextureRect + { + StyleClasses = { StyleClassThumbOutline }, + VerticalAlignment = VAlignment.Center, + }; + + Symbol = new TextureRect + { + StyleClasses = { StyleClassSymbol }, + VerticalAlignment = VAlignment.Center, + }; + + Label = new Label(); + Label.Visible = false; + + OffStateLabel = new Label(); + OffStateLabel.Text = Loc.GetString("toggle-switch-default-off-state-label"); + OffStateLabel.ReservesSpace = true; + + OnStateLabel = new Label(); + OnStateLabel.Text = Loc.GetString("toggle-switch-default-on-state-label"); + OnStateLabel.ReservesSpace = true; + OnStateLabel.Visible = false; + + Label.HorizontalExpand = true; + + AddChild(Label); + AddChild(TrackFill); + AddChild(TrackOutline); + AddChild(ThumbFill); + AddChild(ThumbOutline); + AddChild(Symbol); + AddChild(OffStateLabel); + AddChild(OnStateLabel); + } + + protected override void DrawModeChanged() + { + // Workaround for child controls not being updated automatically. + // Remove once https://github.com/space-wizards/RobustToolbox/pull/6264 + // or similar is merged. + var relevantChangeMade = false; + + if (Disabled) + { + if (!HasStylePseudoClass(StylePseudoClassDisabled)) + { + AddStylePseudoClass(StylePseudoClassDisabled); + relevantChangeMade = true; + } + } + else + { + if (HasStylePseudoClass(StylePseudoClassDisabled)) + { + RemoveStylePseudoClass(StylePseudoClassDisabled); + relevantChangeMade = true; + } + } + + if (Pressed) + { + if (!HasStylePseudoClass(StylePseudoClassPressed)) + { + AddStylePseudoClass(StylePseudoClassPressed); + relevantChangeMade = true; + } + } + else + { + if (HasStylePseudoClass(StylePseudoClassPressed)) + { + RemoveStylePseudoClass(StylePseudoClassPressed); + relevantChangeMade = true; + } + } + + if (relevantChangeMade) + { + Label.RemoveStyleClass("dummy"); + TrackFill.RemoveStyleClass("dummy"); + TrackOutline.RemoveStyleClass("dummy"); + ThumbFill.RemoveStyleClass("dummy"); + ThumbOutline.RemoveStyleClass("dummy"); + Symbol.RemoveStyleClass("dummy"); + OffStateLabel.RemoveStyleClass("dummy"); + OnStateLabel.RemoveStyleClass("dummy"); + } + + // no base.DrawModeChanged() call - ContainerButton's pseudoclass handling + // doesn't support a button being both pressed and disabled + + UpdateAppearance(); + } + + /// + /// If true, the button will allow shrinking and clip text of the main + /// label to prevent the text from going outside the bounds of the button. + /// If false, the minimum size will always fit the contained text. + /// + [ViewVariables] + public bool ClipText { get => Label.ClipText; set => Label.ClipText = value; } + + /// + /// The text displayed by the button's main label. + /// + [ViewVariables] + public string? Text + { + get => Label.Text; + set + { + Label.Text = value; + Label.Visible = !string.IsNullOrEmpty(value); + } + } + + /// + /// The text displayed by the button's secondary label in the off state. + /// + [ViewVariables] + public string? OffStateText + { + get => OffStateLabel.Text; + set => OffStateLabel.Text = value; + } + + /// + /// The text displayed by the button's secondary label in the on state. + /// + [ViewVariables] + public string? OnStateText + { + get => OnStateLabel.Text; + set => OnStateLabel.Text = value; + } + + private void UpdateAppearance() + { + if (OffStateLabel is not null) + { + OffStateLabel.Visible = !Pressed; + } + + if (OnStateLabel is not null) + { + OnStateLabel.Visible = Pressed; + } + } + + protected override void StylePropertiesChanged() + { + base.StylePropertiesChanged(); + UpdateAppearance(); + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + var desiredSize = Vector2.Zero; + var separation = ActualSeparation; + + // Start with the icon, since it always appears + if (TrackOutline is not null) + { + TrackOutline.Measure(availableSize); + desiredSize = TrackOutline.DesiredSize; + } + + // Add space for the label if it has text + if (! string.IsNullOrEmpty(Label?.Text)) + { + Label.Measure(availableSize); + desiredSize.X += separation + Label.DesiredSize.X; + desiredSize.Y = float.Max(desiredSize.Y, Label.DesiredSize.Y); + } + + // Add space for the state labels if at least one of them has text + var stateLabelSpace = Vector2.Zero; + if (! string.IsNullOrEmpty(OffStateLabel?.Text)) + { + OffStateLabel.Measure(availableSize); + stateLabelSpace = OffStateLabel.DesiredSize; + } + + if (! string.IsNullOrEmpty(OnStateLabel?.Text)) + { + OnStateLabel.Measure(availableSize); + stateLabelSpace.Y = float.Max(stateLabelSpace.Y, OnStateLabel.DesiredSize.Y); + stateLabelSpace.X = float.Max(stateLabelSpace.X, OnStateLabel.DesiredSize.X); + } + + if (stateLabelSpace != Vector2.Zero) + { + desiredSize.X += separation + stateLabelSpace.X; + desiredSize.Y = float.Max(desiredSize.Y, stateLabelSpace.Y); + } + + return desiredSize; + } + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + var separation = ActualSeparation; + + var actualMainLabelWidth = finalSize.X - separation - TrackOutline.DesiredSize.X; + float iconPosition = 0; + float stateLabelPosition = 0; + + if (string.IsNullOrEmpty(Label?.Text)) + { + stateLabelPosition = TrackOutline.DesiredSize.X + separation; + } + else + { + if (!string.IsNullOrEmpty(OffStateLabel?.Text) || !string.IsNullOrEmpty(OnStateLabel?.Text)) + { + var stateLabelsWidth = float.Max(OffStateLabel!.DesiredSize.X, OnStateLabel.DesiredSize.X); + actualMainLabelWidth -= (separation + stateLabelsWidth); + } + actualMainLabelWidth = float.Max(actualMainLabelWidth, 0); + iconPosition = actualMainLabelWidth + separation; + stateLabelPosition = iconPosition + TrackOutline.DesiredSize.X + separation; + } + + var mainLabelTargetBox = new UIBox2(0, 0, actualMainLabelWidth, finalSize.Y); + Label?.Arrange(mainLabelTargetBox); + + var iconTargetBox = new UIBox2(iconPosition, 0, iconPosition + TrackOutline.DesiredSize.X, finalSize.Y); + TrackFill.Arrange(iconTargetBox); + TrackOutline.Arrange(iconTargetBox); + Symbol.Arrange(iconTargetBox); + + ThumbOutline.Measure(TrackOutline.DesiredSize); // didn't measure in MeasureOverride, don't need its size there + var thumbLeft = iconTargetBox.Left; + if (Pressed) + thumbLeft = iconTargetBox.Right - ThumbOutline.DesiredSize.X; + var thumbTargetBox = new UIBox2(thumbLeft, 0, thumbLeft + ThumbOutline.DesiredSize.X, finalSize.Y); + ThumbFill.Arrange(thumbTargetBox); + ThumbOutline.Arrange(thumbTargetBox); + + var stateLabelsTargetBox = new UIBox2(stateLabelPosition, 0, finalSize.X, finalSize.Y); + OffStateLabel?.Arrange(stateLabelsTargetBox); + OnStateLabel?.Arrange(stateLabelsTargetBox); + + return finalSize; + } + } +} diff --git a/Content.Client/UserInterface/RichText/MonoTag.cs b/Content.Client/UserInterface/RichText/MonoTag.cs index d97997dc83e..5f66217a36a 100644 --- a/Content.Client/UserInterface/RichText/MonoTag.cs +++ b/Content.Client/UserInterface/RichText/MonoTag.cs @@ -10,7 +10,7 @@ namespace Content.Client.UserInterface.RichText; /// /// Sets the font to a monospaced variant /// -public sealed class MonoTag : IMarkupTag +public sealed class MonoTag : IMarkupTagHandler { public static readonly ProtoId MonoFont = "Monospace"; diff --git a/Content.Client/UserInterface/RichText/ScrambleTag.cs b/Content.Client/UserInterface/RichText/ScrambleTag.cs index a93af993caa..353a9c9533c 100644 --- a/Content.Client/UserInterface/RichText/ScrambleTag.cs +++ b/Content.Client/UserInterface/RichText/ScrambleTag.cs @@ -10,7 +10,7 @@ namespace Content.Client.UserInterface.RichText; /// Adds a specified length of random characters that scramble at a set rate. /// [UsedImplicitly] -public sealed class ScrambleTag : IMarkupTag +public sealed class ScrambleTag : IMarkupTagHandler { [Dependency] private readonly IGameTiming _timing = default!; diff --git a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml index 69c7b20c6e6..fdf66e8fb05 100644 --- a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml +++ b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml @@ -19,7 +19,7 @@ - + diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml index 579dd73e301..35d00af95e4 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml @@ -34,8 +34,8 @@ - - + + diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs index f343ca45755..71e1f5c1aee 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Client.Stylesheets; using Content.Client.UserInterface.Systems.Chat.Widgets; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; @@ -9,6 +10,9 @@ namespace Content.Client.UserInterface.Screens; [GenerateTypedNameReferences] public sealed partial class SeparatedChatGameScreen : InGameScreen { + public const string StyleClassChatContainer = "ChatContainer"; + public const string StyleClassChatOutput = "ChatOutput"; + public SeparatedChatGameScreen() { RobustXamlLoader.Load(this); diff --git a/Content.Client/UserInterface/StatValuesEui.cs b/Content.Client/UserInterface/StatValuesEui.cs index 33035940b57..0e714817b29 100644 --- a/Content.Client/UserInterface/StatValuesEui.cs +++ b/Content.Client/UserInterface/StatValuesEui.cs @@ -11,7 +11,7 @@ public sealed class StatValuesEui : BaseEui public StatValuesEui() { _window = new StatsWindow(); - _window.Title = "Melee stats"; + _window.Title = Loc.GetString("stat-values-ui-title"); _window.OpenCentered(); _window.OnClose += Closed; } diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs index be3af28b157..b77d81dc6e1 100644 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs +++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs @@ -22,6 +22,8 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls; public sealed class ActionButton : Control, IEntityControl { + public const string StyleClassActionHighlightRect = "ActionHighlightRect"; + private IEntityManager _entities; private IPlayerManager _player; private SpriteSystem? _spriteSys; @@ -79,7 +81,7 @@ public ActionButton(IEntityManager entities, SpriteSystem? spriteSys = null, Act }; HighlightRect = new PanelContainer { - StyleClasses = {StyleNano.StyleClassHandSlotHighlight}, + StyleClasses = { StyleClassActionHighlightRect }, MinSize = new Vector2(32, 32), Visible = false }; @@ -196,8 +198,8 @@ private void OnUnpressed(GUIBoundKeyEventArgs args) if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata)) return null; - var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName)); - var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription)); + var name = FormattedMessage.FromMarkupPermissive(metadata.EntityName); + var desc = FormattedMessage.FromMarkupPermissive(metadata.EntityDescription); if (_player.LocalEntity is null) return null; diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml b/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml deleted file mode 100644 index 22893ccef4c..00000000000 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml.cs deleted file mode 100644 index 92d9b50ebe4..00000000000 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionTooltip.xaml.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; - -namespace Content.Client.UserInterface.Systems.Actions.Controls; - -[GenerateTypedNameReferences] -public sealed partial class ActionTooltip : PanelContainer -{ - public ActionTooltip() - { - RobustXamlLoader.Load(this); - } -} diff --git a/Content.Client/UserInterface/Systems/Actions/Windows/ActionsWindow.xaml.cs b/Content.Client/UserInterface/Systems/Actions/Windows/ActionsWindow.xaml.cs index f972a96eb74..4aaabb3d05d 100644 --- a/Content.Client/UserInterface/Systems/Actions/Windows/ActionsWindow.xaml.cs +++ b/Content.Client/UserInterface/Systems/Actions/Windows/ActionsWindow.xaml.cs @@ -8,6 +8,8 @@ namespace Content.Client.UserInterface.Systems.Actions.Windows; [GenerateTypedNameReferences] public sealed partial class ActionsWindow : DefaultWindow { + public const string StyleClassActionSearchBox = "actionSearchBox"; + public MultiselectOptionButton FilterButton { get; private set; } /// diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs index 19e578fda38..52951b8eb6b 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankBoundUserInterface.cs @@ -46,7 +46,7 @@ protected override void UpdateState(BoundUserInterfaceState state) if (EntMan.TryGetComponent(Owner, out GasTankComponent? component)) { var canConnect = EntMan.System().CanConnectToInternals((Owner, component)); - _window?.Update(canConnect, component.IsConnected, component.OutputPressure); + _window?.Update(canConnect, component.IsConnected, component.ReleasePressure); } if (state is GasTankBoundUserInterfaceState cast) diff --git a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs index 7b2680e21d1..1a7a8079b85 100644 --- a/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs +++ b/Content.Client/UserInterface/Systems/Atmos/GasTank/GasTankWindow.cs @@ -95,7 +95,7 @@ public GasTankWindow() _topLabel = new Label { FontOverride = font, - FontColorOverride = StyleNano.NanoGold, + StyleClasses = { StyleClass.LabelKeyText }, VerticalAlignment = VAlignment.Center, HorizontalExpand = true, HorizontalAlignment = HAlignment.Left, diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs index 3dea15c36ca..4c2fe40a0de 100644 --- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs +++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs @@ -232,7 +232,7 @@ public void PopOut() helper.ClydeWindow = _clyde.CreateWindow(new WindowCreateParameters { Maximized = false, - Title = "Admin Help", + Title = Loc.GetString("bwoink-admin-title"), Monitor = monitor, // imperial space старое значение: 900 Width = 1000, @@ -251,15 +251,15 @@ public void PopOut() private void UnreadAHelpReceived() { - GameAHelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton); - LobbyAHelpButton?.StyleClasses.Add(StyleNano.StyleClassButtonColorRed); + GameAHelpButton?.StyleClasses.Add(StyleClass.Negative); + LobbyAHelpButton?.StyleClasses.Add(StyleClass.Negative); _hasUnreadAHelp = true; } private void UnreadAHelpRead() { - GameAHelpButton?.StyleClasses.Remove(MenuButton.StyleClassRedTopButton); - LobbyAHelpButton?.StyleClasses.Remove(StyleNano.StyleClassButtonColorRed); + GameAHelpButton?.StyleClasses.Remove(StyleClass.Negative); + LobbyAHelpButton?.StyleClasses.Remove(StyleClass.Negative); _hasUnreadAHelp = false; } diff --git a/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs b/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs index 5b20604338c..b9dcc57a511 100644 --- a/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs +++ b/Content.Client/UserInterface/Systems/Character/CharacterUIController.cs @@ -169,7 +169,7 @@ private void CharacterUpdated(CharacterData data) var objectiveLabel = new RichTextLabel { - StyleClasses = { StyleNano.StyleClassTooltipActionTitle } + StyleClasses = { StyleClass.TooltipTitle } }; objectiveLabel.SetMessage(objectiveText); diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 98dd0bb8e1e..a7cc29751bf 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -278,7 +278,7 @@ private void SetChatWindowOpacity(float opacity) && style is StyleBoxFlat propStyleBoxFlat) color = propStyleBoxFlat.BackgroundColor; else - color = StyleNano.ChatBackgroundColor; + color = Color.FromHex("#25252ADD"); panel.PanelOverride = new StyleBoxFlat { @@ -690,7 +690,7 @@ private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioCha radioChannel = null; return _player.LocalEntity is EntityUid { Valid: true } uid && _chatSys != null - && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true); + && _chatSys.TryProcessRadioMessage(uid, text, out _, out radioChannel, quiet: true); } public void UpdateSelectedChannel(ChatBox box) diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs index bf37a412e88..e96d35f825c 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs @@ -6,6 +6,9 @@ namespace Content.Client.UserInterface.Systems.Chat.Controls; public sealed class ChannelSelectorItemButton : Button { + public const string StyleClassChatSelectorOptionButton = "ChatSelectorOptionButton"; + + public readonly ChatSelectChannel Channel; public bool IsHidden => Parent == null; @@ -13,7 +16,7 @@ public sealed class ChannelSelectorItemButton : Button public ChannelSelectorItemButton(ChatSelectChannel selector) { Channel = selector; - AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); + AddStyleClass(StyleClassChatSelectorOptionButton); Text = ChannelSelectorButton.ChannelSelectorName(selector); diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs index 0326664bd69..e9345d25f93 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.xaml.cs @@ -1,5 +1,4 @@ -using Content.Client.Stylesheets; -using Content.Shared.Chat; +using Content.Shared.Chat; using Content.Shared.Input; using Robust.Client.UserInterface.Controls; @@ -8,6 +7,10 @@ namespace Content.Client.UserInterface.Systems.Chat.Controls; [Virtual] public class ChatInputBox : PanelContainer { + public const string StyleClassChatPanel = "ChatPanel"; + public const string StyleClassChatLineEdit = "ChatLineEdit"; + public const string StyleClassChatFilterOptionButton = "ChatFilterOptionButton"; + public readonly ChannelSelectorButton ChannelSelector; public readonly HistoryLineEdit Input; public readonly ChannelFilterButton FilterButton; @@ -27,7 +30,7 @@ public ChatInputBox() { Name = "ChannelSelector", ToggleMode = true, - StyleClasses = {"chatSelectorOptionButton"}, + StyleClasses = { ChannelSelectorItemButton.StyleClassChatSelectorOptionButton }, MinWidth = 75 }; Container.AddChild(ChannelSelector); @@ -36,16 +39,16 @@ public ChatInputBox() Name = "Input", PlaceHolder = GetChatboxInfoPlaceholder(), HorizontalExpand = true, - StyleClasses = {"chatLineEdit"} + StyleClasses = { StyleClassChatLineEdit } }; Container.AddChild(Input); FilterButton = new ChannelFilterButton { Name = "FilterButton", - StyleClasses = {"chatFilterOptionButton"} + StyleClasses = { StyleClassChatFilterOptionButton } }; Container.AddChild(FilterButton); - AddStyleClass(StyleNano.StyleClassChatSubPanel); + AddStyleClass(StyleClassChatPanel); ChannelSelector.OnChannelSelect += UpdateActiveChannel; } @@ -56,12 +59,17 @@ private void UpdateActiveChannel(ChatSelectChannel selectedChannel) private static string GetChatboxInfoPlaceholder() { - return (BoundKeyHelper.IsBound(ContentKeyFunctions.FocusChat), BoundKeyHelper.IsBound(ContentKeyFunctions.CycleChatChannelForward)) switch - { - (true, true) => Loc.GetString("hud-chatbox-info", ("talk-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.FocusChat)), ("cycle-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.CycleChatChannelForward))), - (true, false) => Loc.GetString("hud-chatbox-info-talk", ("talk-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.FocusChat))), - (false, true) => Loc.GetString("hud-chatbox-info-cycle", ("cycle-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.CycleChatChannelForward))), - (false, false) => Loc.GetString("hud-chatbox-info-unbound") - }; + return (BoundKeyHelper.IsBound(ContentKeyFunctions.FocusChat), + BoundKeyHelper.IsBound(ContentKeyFunctions.CycleChatChannelForward)) switch + { + (true, true) => Loc.GetString("hud-chatbox-info", + ("talk-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.FocusChat)), + ("cycle-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.CycleChatChannelForward))), + (true, false) => Loc.GetString("hud-chatbox-info-talk", + ("talk-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.FocusChat))), + (false, true) => Loc.GetString("hud-chatbox-info-cycle", + ("cycle-key", BoundKeyHelper.ShortKeyName(ContentKeyFunctions.CycleChatChannelForward))), + (false, false) => Loc.GetString("hud-chatbox-info-unbound") + }; } } diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs index c389cf4dfb7..6e8a41de7c9 100644 --- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs @@ -117,7 +117,7 @@ public void AddLine(string message, Color color) formatted.PushColor(color); formatted.AddMarkupOrThrow(message); formatted.Pop(); - Contents.AddMessage(formatted); + Contents.AddMessage(formatted, tagsAllowed: null); } public void Focus(ChatSelectChannel? channel = null) diff --git a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs index 55d00fec185..3c14e7c6b7e 100644 --- a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs +++ b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs @@ -1,8 +1,10 @@ -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; 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; @@ -20,6 +22,8 @@ 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 DamageableSystem _damageable = default!; private Overlays.DamageOverlay _overlay = default!; public override void Initialize() @@ -88,6 +92,7 @@ private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, Damag return; //this entity intentionally has no overlays } + var damagePerGroup = _damageable.GetDamagePerGroup((entity, damageable)); var critThreshold = foundThreshold.Value; _overlay.State = mobState.CurrentState; @@ -98,11 +103,12 @@ private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, Damag FixedPoint2 painLevel = 0; _overlay.PainLevel = 0; - if (!EntityManager.HasComponent(entity)) + if (!_statusEffects.TryEffectsWithComp(entity, out _)) { foreach (var painDamageType in damageable.PainDamageGroups) { - damageable.DamagePerGroup.TryGetValue(painDamageType, out var painDamage); + + damagePerGroup.TryGetValue(painDamageType, out var painDamage); painLevel += painDamage; } _overlay.PainLevel = FixedPoint2.Min(1f, painLevel / critThreshold).Float(); @@ -113,7 +119,7 @@ private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, Damag } } - if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage)) + if (damagePerGroup.TryGetValue("Airloss", out var oxyDamage)) { _overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float(); } @@ -125,7 +131,7 @@ private void UpdateOverlays(EntityUid entity, MobStateComponent? mobState, Damag case MobState.Critical: { if (!_mobThresholdSystem.TryGetDeadPercentage(entity, - FixedPoint2.Max(0.0, damageable.TotalDamage), out var critLevel)) + FixedPoint2.Max(0.0, _damageable.GetTotalDamage((entity, damageable))), out var critLevel)) return; _overlay.CritLevel = critLevel.Value.Float(); diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs index fdcc3c45a2f..193db2a4864 100644 --- a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs +++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs @@ -147,7 +147,7 @@ private IEnumerable ConvertToButtons(IEnumerable + { + CloseEscapeWindow(); + _feedback.ToggleWindow(); + }; + _escapeWindow.ChangelogButton.OnPressed += _ => { CloseEscapeWindow(); diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml index ff8e56f8fe2..2cf3f89145d 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml @@ -1,47 +1,47 @@ - - - - - - - - - - - [TestFixture] [TestOf(typeof(DeltaPressureSystem))] -public sealed class DeltaPressureTest +public sealed class DeltaPressureTest : AtmosTest { #region Prototypes @@ -91,7 +88,13 @@ public sealed class DeltaPressureTest #endregion - private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml"); + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml"); + + // TODO ATMOS TESTS + // - Check for directional windows (partial airtight ents) properly computing pressure differences + // - Check for multi-tick damage (window with n damage threshold should take n ticks to destroy) + // - Check that all maps do not explode into a million pieces on load due to dP + // - Ensure that all tests work for a map that has an origin at a non zero coordinate /// /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList @@ -104,40 +107,15 @@ public sealed class DeltaPressureTest [Test] public async Task ProcessingListAutoJoinTest() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entMan = server.EntMan; - var mapLoader = entMan.System(); - var atmosphereSystem = entMan.System(); - var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true }; - - Entity grid = default; - Entity dpEnt; - - // Load our test map in and assert that it exists. - await server.WaitPost(() => + await Server.WaitAssertion(() => { -#pragma warning disable NUnit2045 - Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions), - $"Failed to load map {_testMap}."); - Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!"); -#pragma warning restore NUnit2045 + var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero)); + var dpEnt = new Entity(uid, SEntMan.GetComponent(uid)); - grid = gridSet.First(); + Assert.That(SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was not in processing list when it should have automatically joined!"); + SEntMan.DeleteEntity(uid); + Assert.That(!SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was still in processing list after deletion!"); }); - - await server.WaitAssertion(() => - { - var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero)); - dpEnt = new Entity(uid, entMan.GetComponent(uid)); - - Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!"); - entMan.DeleteEntity(uid); - Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!"); - }); - - await pair.CleanReturnAsync(); } /// @@ -147,45 +125,27 @@ await server.WaitAssertion(() => [Test] public async Task ProcessingDeltaStandbyTest() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entMan = server.EntMan; - var mapLoader = entMan.System(); - var atmosphereSystem = entMan.System(); - var transformSystem = entMan.System(); - var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true }; - - Entity grid = default; Entity dpEnt = default; TileAtmosphere tile = null!; AtmosDirection direction = default; // Load our test map in and assert that it exists. - await server.WaitPost(() => + await Server.WaitPost(() => { -#pragma warning disable NUnit2045 - Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions), - $"Failed to load map {_testMap}."); - Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!"); -#pragma warning restore NUnit2045 - - grid = gridSet.First(); - var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero)); - dpEnt = new Entity(uid, entMan.GetComponent(uid)); - Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); + var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero)); + dpEnt = new Entity(uid, SEntMan.GetComponent(uid)); + Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt, dpEnt), "Entity was not in processing list when it should have been added!"); }); for (var i = 0; i < Atmospherics.Directions; i++) { - await server.WaitPost(() => + await Server.WaitPost(() => { - var indices = transformSystem.GetGridOrMapTilePosition(dpEnt); - var gridAtmosComp = entMan.GetComponent(grid); + var indices = Transform.GetGridOrMapTilePosition(dpEnt); direction = (AtmosDirection)(1 << i); var offsetIndices = indices.Offset(direction); - tile = gridAtmosComp.Tiles[offsetIndices]; + tile = RelevantAtmos.Comp.Tiles[offsetIndices]; Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!"); @@ -195,19 +155,17 @@ await server.WaitPost(() => tile.Air!.AdjustMoles(Gas.Nitrogen, moles); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); // Entity should exist, if it took one tick of damage then it should be instantly destroyed. - await server.WaitAssertion(() => + await Server.WaitAssertion(() => { - Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!"); + Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!"); tile.Air!.Clear(); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); } - - await pair.CleanReturnAsync(); } /// @@ -217,47 +175,43 @@ await server.WaitAssertion(() => [Test] public async Task ProcessingDeltaDamageTest() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entMan = server.EntMan; - var mapLoader = entMan.System(); - var atmosphereSystem = entMan.System(); - var transformSystem = entMan.System(); - var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true }; - - Entity grid = default; Entity dpEnt = default; - TileAtmosphere tile = null!; AtmosDirection direction = default; // Load our test map in and assert that it exists. - await server.WaitPost(() => + await Server.WaitPost(() => { -#pragma warning disable NUnit2045 - Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions), - $"Failed to load map {_testMap}."); - Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!"); -#pragma warning restore NUnit2045 - - grid = gridSet.First(); + SAtmos.SetAtmosphereSimulation(ProcessEnt, false); }); for (var i = 0; i < Atmospherics.Directions; i++) { - await server.WaitPost(() => + /* + RUNNING REGULAR TICKS USING WaitRunTicks AND GUESSING AS TO HOW MANY ATMOS SIMULATION TICKS ARE HAPPENING + WILL CAUSE A RACE CONDITION THAT IS A PAIN IN THE ASS TO DEBUG + + AN ENTITY MAY BE REMOVED AND ADDED BETWEEN A SUBTICK. IF LINDA PROCESSING IS ENABLED IT MIGHT CAUSE + AN EQUALIZATION TO PUT AIR IN OTHER TILES IN THE SMALL WINDOW WHERE THE TILE IS NOT AIRTIGHT + WHICH WILL THROW OFF DELTAS + */ + + await Server.WaitPost(() => + { + SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate); + }); + + await Server.WaitPost(() => { // Need to spawn an entity each run to ensure it works for all directions. - var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero)); - dpEnt = new Entity(uid, entMan.GetComponent(uid)); - Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); + var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero)); + dpEnt = new Entity(uid, SEntMan.GetComponent(uid)); + Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); - var indices = transformSystem.GetGridOrMapTilePosition(dpEnt); - var gridAtmosComp = entMan.GetComponent(grid); + var indices = Transform.GetGridOrMapTilePosition(dpEnt); direction = (AtmosDirection)(1 << i); var offsetIndices = indices.Offset(direction); - tile = gridAtmosComp.Tiles[offsetIndices]; + var tile = ProcessEnt.Comp1.Tiles[offsetIndices]; Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!"); @@ -267,19 +221,29 @@ await server.WaitPost(() => tile.Air!.AdjustMoles(Gas.Nitrogen, moles); }); - await server.WaitRunTicks(30); - - // Entity should exist, if it took one tick of damage then it should be instantly destroyed. - await server.WaitAssertion(() => + // get jiggy with it! hit that dance white boy! + await Server.WaitPost(() => { - Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!"); - tile.Air!.Clear(); + SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate); }); - await server.WaitRunTicks(30); - } + // need to run some ticks as deleted entities are queued for removal + // and not removed instantly + await Server.WaitRunTicks(30); - await pair.CleanReturnAsync(); + // Entity shouldn't exist, if it took one tick of damage then it should be instantly destroyed. + await Server.WaitAssertion(() => + { + Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!"); + + // Double whammy: in case any unintended gas leak occured due to a race condition, + // clear out all the tiles. + foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt)) + { + mix.Clear(); + } + }); + } } /// @@ -289,39 +253,23 @@ await server.WaitAssertion(() => [Test] public async Task ProcessingAbsoluteStandbyTest() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entMan = server.EntMan; - var mapLoader = entMan.System(); - var atmosphereSystem = entMan.System(); - var transformSystem = entMan.System(); - var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true }; - - Entity grid = default; Entity dpEnt = default; TileAtmosphere tile = null!; AtmosDirection direction = default; - await server.WaitPost(() => + await Server.WaitPost(() => { -#pragma warning disable NUnit2045 - Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions), - $"Failed to load map {_testMap}."); - Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!"); -#pragma warning restore NUnit2045 - grid = gridSet.First(); - var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero)); - dpEnt = new Entity(uid, entMan.GetComponent(uid)); - Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); + var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero)); + dpEnt = new Entity(uid, SEntMan.GetComponent(uid)); + Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); }); for (var i = 0; i < Atmospherics.Directions; i++) { - await server.WaitPost(() => + await Server.WaitPost(() => { - var indices = transformSystem.GetGridOrMapTilePosition(dpEnt); - var gridAtmosComp = entMan.GetComponent(grid); + var indices = Transform.GetGridOrMapTilePosition(dpEnt); + var gridAtmosComp = SEntMan.GetComponent(ProcessEnt); direction = (AtmosDirection)(1 << i); var offsetIndices = indices.Offset(direction); @@ -333,18 +281,16 @@ await server.WaitPost(() => tile.Air!.AdjustMoles(Gas.Nitrogen, moles); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); - await server.WaitAssertion(() => + await Server.WaitAssertion(() => { - Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!"); + Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!"); tile.Air!.Clear(); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); } - - await pair.CleanReturnAsync(); } /// @@ -354,41 +300,21 @@ await server.WaitAssertion(() => [Test] public async Task ProcessingAbsoluteDamageTest() { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entMan = server.EntMan; - var mapLoader = entMan.System(); - var atmosphereSystem = entMan.System(); - var transformSystem = entMan.System(); - var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true }; - - Entity grid = default; Entity dpEnt = default; TileAtmosphere tile = null!; AtmosDirection direction = default; - await server.WaitPost(() => - { -#pragma warning disable NUnit2045 - Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions), - $"Failed to load map {_testMap}."); - Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!"); -#pragma warning restore NUnit2045 - grid = gridSet.First(); - }); - for (var i = 0; i < Atmospherics.Directions; i++) { - await server.WaitPost(() => + await Server.WaitPost(() => { // Spawn fresh entity each iteration to verify all directions work - var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero)); - dpEnt = new Entity(uid, entMan.GetComponent(uid)); - Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); + var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero)); + dpEnt = new Entity(uid, SEntMan.GetComponent(uid)); + Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!"); - var indices = transformSystem.GetGridOrMapTilePosition(dpEnt); - var gridAtmosComp = entMan.GetComponent(grid); + var indices = Transform.GetGridOrMapTilePosition(dpEnt); + var gridAtmosComp = SEntMan.GetComponent(ProcessEnt); direction = (AtmosDirection)(1 << i); var offsetIndices = indices.Offset(direction); @@ -401,17 +327,15 @@ await server.WaitPost(() => tile.Air!.AdjustMoles(Gas.Nitrogen, moles); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); - await server.WaitAssertion(() => + await Server.WaitAssertion(() => { - Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!"); + Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!"); tile.Air!.Clear(); }); - await server.WaitRunTicks(30); + await Server.WaitRunTicks(30); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs index 07caf447bd9..eda9061281c 100644 --- a/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GasArrayTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Robust.Shared.GameObjects; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Atmos; [TestFixture] [TestOf(typeof(Atmospherics))] -public sealed class GasArrayTest +public sealed class GasArrayTest : GameTest { private const string GasTankTestDummyId = "GasTankTestDummy"; @@ -42,7 +43,7 @@ public sealed class GasArrayTest [Test] public async Task TestGasArrayDeserialization() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var compFactory = server.ResolveDependency(); @@ -80,6 +81,5 @@ await server.WaitAssertion(() => } }); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs b/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs index 1cb8fd8b6f2..70bbd0ac0cd 100644 --- a/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GasMixtureTest.cs @@ -1,4 +1,5 @@ -using Content.Server.Atmos; +using Content.IntegrationTests.Fixtures; +using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; using Robust.Shared.GameObjects; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.Atmos { [TestFixture] [TestOf(typeof(GasMixture))] - public sealed class GasMixtureTest + public sealed class GasMixtureTest : GameTest { [Test] public async Task TestMerge() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var atmosphereSystem = server.ResolveDependency().GetEntitySystem(); @@ -56,8 +57,6 @@ await server.WaitAssertion(() => Assert.That(a.GetMoles(Gas.Oxygen), Is.EqualTo(50)); }); }); - - await pair.CleanReturnAsync(); } [Test] @@ -69,7 +68,7 @@ await server.WaitAssertion(() => [TestCase(Atmospherics.BreathPercentage)] public async Task RemoveRatio(float ratio) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitAssertion(() => @@ -103,8 +102,6 @@ await server.WaitAssertion(() => Assert.That(a.GetMoles(Gas.Nitrogen), Is.EqualTo(100 - b.GetMoles(Gas.Nitrogen))); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Atmos/GetAirflowDirectionsTest.cs b/Content.IntegrationTests/Tests/Atmos/GetAirflowDirectionsTest.cs new file mode 100644 index 00000000000..4677cc5f7ee --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/GetAirflowDirectionsTest.cs @@ -0,0 +1,50 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Atmos; + +/// +/// Class for testing some airflow retrieval API methods. +/// +public sealed class GetAirflowDirectionsTest : AtmosTest +{ + // i will keep using this test map until it has been drained + // of all use + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml"); + + [Test] + [TestCase(0, 0, AtmosDirection.All)] + [TestCase(0, 1, AtmosDirection.South)] + [TestCase(0, -1, AtmosDirection.North)] + [TestCase(1, 0, AtmosDirection.West)] + [TestCase(-1, 0, AtmosDirection.East)] + [TestCase(1, 1, AtmosDirection.Invalid)] + [TestCase(100, 100, AtmosDirection.Invalid)] + public async Task TestLookup(int x, int y, AtmosDirection expectedDirections) + { + await Server.WaitPost(delegate + { + // yea + var coords = new Vector2i(x, y); + var directions = SAtmos.GetAirflowDirections(RelevantAtmos, coords); + Assert.That(directions, Is.EqualTo(expectedDirections)); + }); + } + + /// + /// Tests that a grident with no atmosphere will return . + /// + [Test] + public async Task TestLookup_BadEnt() + { + await Server.WaitPost(delegate + { + var directions = SAtmos.GetAirflowDirections(EntityUid.Invalid, Vector2i.Zero); + Assert.That(directions, Is.EqualTo(AtmosDirection.Invalid)); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs index 3a1ec7fd40e..9b43e85396a 100644 --- a/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/GridJoinTest.cs @@ -1,20 +1,20 @@ -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; +using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.Piping.EntitySystems; +using Content.Shared.Atmos.Components; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Atmos; [TestFixture] -public sealed class GridJoinTest +public sealed class GridJoinTest : GameTest { private const string CanisterProtoId = "AirCanister"; [Test] public async Task TestGridJoinAtmosphere() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -47,7 +47,5 @@ await server.WaitPost(() => // Make sure that the canister is now properly tracked as on-grid Assert.That(atmosDeviceSystem.IsJoinedOffGrid(canisterEnt), Is.False); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs b/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs new file mode 100644 index 00000000000..4cdc0894ca9 --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs @@ -0,0 +1,127 @@ +using Content.Shared.Atmos; +using Content.Shared.Coordinates; +using Content.Shared.Tests; +using Robust.Shared.GameObjects; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Atmos; + +public sealed class RoomSpacingTest : AtmosTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_room.yml"); + + /// + /// Checks that deleting an outer wall spaces the room. + /// + [Test] + public async Task DeleteWall() + { + var markers = SEntMan.AllEntities(); + + EntityUid source, floor, wallPos; + source = floor = wallPos = EntityUid.Invalid; + + Assert.Multiple(() => + { + Assert.That(GetMarker(markers, "source", out source)); + Assert.That(GetMarker(markers, "floor", out floor)); + Assert.That(GetMarker(markers, "wall", out wallPos)); + }); + + var lookup = LookupSystem.GetEntitiesIntersecting(wallPos); + var wall = lookup.FirstOrNull(); + Assert.That(wall, Is.Not.Null); + + Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0)); + + var sourceMix = SAtmos.GetTileMixture(source, true); + Assert.That(sourceMix, Is.Not.EqualTo(null)); + sourceMix.AdjustMoles(Gas.Frezon, Moles); + + await Server.WaitRunTicks(500); + + var mix1 = SAtmos.GetTileMixture(floor); + Assert.That(mix1, Is.Not.EqualTo(null)); + + AssertMixMoles(sourceMix, mix1, Tolerance); + AssertGridMoles(Moles, Tolerance); + + // Space the room + await Server.WaitAssertion(() => + { + SEntMan.DeleteEntity(wall); + }); + + await Server.WaitRunTicks(10); + + await Server.WaitPost(() => + { + for (var i = 0; i < 50; i++) + { + SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate); + } + }); + + AssertMixMoles(sourceMix, mix1, Tolerance); + AssertGridMoles(0, Tolerance); + } + + /// + /// Checks that exposing tile lattice spaces the room. + /// + [Test] + public async Task PryLattice() + { + var markers = SEntMan.AllEntities(); + + EntityUid source, floor, wallPos; + source = floor = wallPos = EntityUid.Invalid; + + Assert.Multiple(() => + { + Assert.That(GetMarker(markers, "source", out source)); + Assert.That(GetMarker(markers, "floor", out floor)); + Assert.That(GetMarker(markers, "wall", out wallPos)); + }); + + var lookup = LookupSystem.GetEntitiesIntersecting(wallPos); + var wall = lookup.FirstOrNull(); + Assert.That(wall, Is.Not.Null); + + Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0)); + + var sourceMix = SAtmos.GetTileMixture(source, true); + Assert.That(sourceMix, Is.Not.EqualTo(null)); + sourceMix.AdjustMoles(Gas.Frezon, Moles); + + await Server.WaitPost(() => + { + SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate); + }); + + var mix1 = SAtmos.GetTileMixture(floor); + Assert.That(mix1, Is.Not.EqualTo(null)); + + AssertMixMoles(sourceMix, mix1, Tolerance); + AssertGridMoles(Moles, Tolerance); + + // Space the room + await SetTile(Lattice, SEntMan.GetNetCoordinates(floor.ToCoordinates()), MapData.Grid); + + await Server.WaitRunTicks(10); + + await Server.WaitPost(() => + { + for (var i = 0; i < 50; i++) + { + SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate); + } + }); + + mix1 = SAtmos.GetTileMixture(floor); + Assert.That(mix1, Is.Not.EqualTo(null)); + + AssertMixMoles(sourceMix, mix1, Tolerance); + AssertGridMoles(0, Tolerance); + } +} diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs new file mode 100644 index 00000000000..7269b1b4739 --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs @@ -0,0 +1,281 @@ +using Content.Client.Atmos.EntitySystems; +using Content.IntegrationTests.Pair; +using Content.Shared.Atmos; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.UnitTesting; + +namespace Content.IntegrationTests.Tests.Atmos; + +/// +/// Tests for asserting that various gas specific heat operations agree with each other and do not deviate +/// across client and server. +/// +[TestOf(nameof(SharedAtmosphereSystem)), FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public sealed class SharedGasSpecificHeatsTest +{ + private IConfigurationManager _sConfig; + private IConfigurationManager _cConfig; + + private TestPair _pair = default!; + + private RobustIntegrationTest.ServerIntegrationInstance Server => _pair.Server; + private RobustIntegrationTest.ClientIntegrationInstance Client => _pair.Client; + + private IEntityManager _sEntMan = default!; + private Content.Server.Atmos.EntitySystems.AtmosphereSystem _sAtmos = default!; + + private IEntityManager _cEntMan = default!; + private AtmosphereSystem _cAtmos = default!; + + [SetUp] + public async Task SetUp() + { + var poolSettings = new PoolSettings + { + Connected = true, + }; + _pair = await PoolManager.GetServerClient(poolSettings, new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); + + _sEntMan = Server.ResolveDependency(); + _cEntMan = Client.ResolveDependency(); + + _sAtmos = _sEntMan.System(); + _cAtmos = _cEntMan.System(); + } + + [TearDown] + public async Task TearDown() + { + await _pair.CleanReturnAsync(); + } + + /// + /// Asserts that the cached gas specific heat arrays agree with each other. + /// + [Test] + public async Task GasSpecificHeats_Agree() + { + var serverSpecificHeats = Array.Empty(); + var clientSpecificHeats = Array.Empty(); + await Server.WaitPost(delegate + { + serverSpecificHeats = _sAtmos.GasMolarHeatCapacities; + }); + + await Client.WaitPost(delegate + { + clientSpecificHeats = _cAtmos.GasMolarHeatCapacities; + }); + + Assert.That(serverSpecificHeats, + Is.EqualTo(clientSpecificHeats), + "Server and client gas specific heat arrays do not agree."); + } + + /// + /// Asserts that heat capacity calculations agree for the same gas mixture. + /// + [Test] + public async Task HeatCapacity_Agree() + { + const float volume = 2500f; + const float temperature = 293.15f; + + const float o2 = 12.3f; + const float n2 = 45.6f; + const float co2 = 0.42f; + const float plasma = 0.05f; + + var serverScaled = 0f; + var serverUnscaled = 0f; + var clientScaled = 0f; + var clientUnscaled = 0f; + + await Server.WaitPost(delegate + { + var mix = new GasMixture(volume) { Temperature = temperature }; + mix.AdjustMoles(Gas.Oxygen, o2); + mix.AdjustMoles(Gas.Nitrogen, n2); + mix.AdjustMoles(Gas.CarbonDioxide, co2); + mix.AdjustMoles(Gas.Plasma, plasma); + + serverScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true); + serverUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false); + }); + + await Client.WaitPost(delegate + { + var mix = new GasMixture(volume) { Temperature = temperature }; + mix.AdjustMoles(Gas.Oxygen, o2); + mix.AdjustMoles(Gas.Nitrogen, n2); + mix.AdjustMoles(Gas.CarbonDioxide, co2); + mix.AdjustMoles(Gas.Plasma, plasma); + + clientScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true); + clientUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false); + }); + + // none of these should be exploding or nonzero. + // they could potentially agree at insane values and pass the test + // so check for if they're sane. + using (Assert.EnterMultipleScope()) + { + Assert.That(serverScaled, + Is.GreaterThan(0f), + "Heat capacity calculated on server with scaling is not greater than zero."); + Assert.That(serverUnscaled, + Is.GreaterThan(0f), + "Heat capacity calculated on server without scaling is not greater than zero."); + Assert.That(clientScaled, + Is.GreaterThan(0f), + "Heat capacity calculated on client with scaling is not greater than zero."); + Assert.That(clientUnscaled, + Is.GreaterThan(0f), + "Heat capacity calculated on client without scaling is not greater than zero."); + + Assert.That(float.IsFinite(serverScaled), + Is.True, + "Heat capacity calculated on server with scaling is not finite."); + Assert.That(float.IsFinite(serverUnscaled), + Is.True, + "Heat capacity calculated on server without scaling is not finite."); + Assert.That(float.IsFinite(clientScaled), + Is.True, + "Heat capacity calculated on client with scaling is not finite."); + Assert.That(float.IsFinite(clientUnscaled), + Is.True, + "Heat capacity calculated on client without scaling is not finite."); + } + + const float epsilon = 1e-4f; + using (Assert.EnterMultipleScope()) + { + Assert.That(serverScaled, + Is.EqualTo(clientScaled).Within(epsilon), + "Heat capacity calculated with scaling does not agree between client and server."); + Assert.That(serverUnscaled, + Is.EqualTo(clientUnscaled).Within(epsilon), + "Heat capacity calculated without scaling does not agree between client and server."); + + Assert.That(serverUnscaled, + Is.EqualTo(serverScaled * _sAtmos.HeatScale).Within(epsilon), + "Heat capacity calculated on server without scaling does not equal scaled value multiplied by HeatScale."); + Assert.That(clientUnscaled, + Is.EqualTo(clientScaled * _cAtmos.HeatScale).Within(epsilon), + "Heat capacity calculated on client without scaling does not equal scaled value multiplied by HeatScale."); + } + } + + /// + /// HeatScale CVAR is required for specific heat calculations. + /// Assert that they agree across client and server, and that changing the CVAR + /// replicates properly and updates the cached value. + /// Also assert that calculations using the updated HeatScale agree properly. + /// + [Test] + public async Task HeatScaleCVar_Replicates_Agree() + { + // ensure that replicated value changes by testing a new value + const float newHeatScale = 13f; + + _sConfig = Server.ResolveDependency(); + _cConfig = Client.ResolveDependency(); + + await Server.WaitPost(delegate + { + _sConfig.SetCVar(CCVars.AtmosHeatScale, newHeatScale); + }); + + await Server.WaitRunTicks(5); + await Client.WaitRunTicks(5); + + // assert agreement between client and server + float serverCVar = 0; + float clientCVar = 0; + float serverHeatScale = 0; + float clientHeatScale = 0; + + await Server.WaitPost(delegate + { + serverCVar = _sConfig.GetCVar(CCVars.AtmosHeatScale); + serverHeatScale = _sAtmos.HeatScale; + }); + + await Client.WaitPost(delegate + { + clientCVar = _cConfig.GetCVar(CCVars.AtmosHeatScale); + clientHeatScale = _cAtmos.HeatScale; + }); + + const float epsilon = 1e-4f; + using (Assert.EnterMultipleScope()) + { + Assert.That(serverCVar, + Is.EqualTo(newHeatScale).Within(epsilon), + "Server CVAR value for AtmosHeatScale does not equal the set value."); + Assert.That(clientCVar, + Is.EqualTo(newHeatScale).Within(epsilon), + "Client CVAR value for AtmosHeatScale does not equal the set value."); + + Assert.That(serverHeatScale, + Is.EqualTo(newHeatScale).Within(epsilon), + "Server cached HeatScale does not equal the set CVAR value."); + Assert.That(clientHeatScale, + Is.EqualTo(newHeatScale).Within(epsilon), + "Client cached HeatScale does not equal the set CVAR value."); + + Assert.That(serverHeatScale, + Is.EqualTo(clientHeatScale).Within(epsilon), + "Client and server cached HeatScale values do not agree."); + } + + // verify that anything calculated using the shared HeatScale agrees properly + const float volume = 2500f; + const float temperature = 293.15f; + + var sScaled = 0f; + var sUnscaled = 0f; + var cScaled = 0f; + var cUnscaled = 0f; + + await Server.WaitPost(delegate + { + var mix = new GasMixture(volume) { Temperature = temperature }; + mix.AdjustMoles(Gas.Oxygen, 10f); + mix.AdjustMoles(Gas.Nitrogen, 20f); + + sScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true); + sUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false); + }); + + await Client.WaitPost(delegate + { + var mix = new GasMixture(volume) { Temperature = temperature }; + mix.AdjustMoles(Gas.Oxygen, 10f); + mix.AdjustMoles(Gas.Nitrogen, 20f); + + cScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true); + cUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false); + }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(sScaled, + Is.GreaterThan(0f), + "Heat capacity calculated on server with scaling is not greater than zero after CVAR change."); + Assert.That(cScaled, + Is.GreaterThan(0f), + "Heat capacity calculated on client with scaling is not greater than zero after CVAR change."); + + Assert.That(sUnscaled, + Is.EqualTo(sScaled * serverHeatScale).Within(epsilon), + "Heat capacity calculated on server without scaling does not equal scaled value multiplied by updated HeatScale."); + Assert.That(cUnscaled, + Is.EqualTo(cScaled * clientHeatScale).Within(epsilon), + "Heat capacity calculated on client without scaling does not equal scaled value multiplied by updated HeatScale."); + } + } +} diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasTileOverlaySystemTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasTileOverlaySystemTest.cs new file mode 100644 index 00000000000..4a8e1d1bac0 --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/SharedGasTileOverlaySystemTest.cs @@ -0,0 +1,103 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using System.Linq; +using System.Numerics; + +namespace Content.IntegrationTests.Tests.Atmos; + +/// +/// GasTileOverlay is being tested here +/// +public sealed class GasTileOverlayTemperatureNetworkingTest : AtmosTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml"); + + [Test] + public async Task TestGasOverlayDataSync() + { + var sMapSys = Server.System(); + + var gridComp = ProcessEnt.Comp3; + var gridNetEnt = Server.EntMan.GetNetEntity(ProcessEnt); + + var gridCoords = new EntityCoordinates(ProcessEnt, Vector2.Zero); + var tileIndices = sMapSys.TileIndicesFor(ProcessEnt, gridComp, gridCoords); + var mixture = SAtmos.GetTileMixture(ProcessEnt, null, tileIndices, true); + + // Get data for client side. + var cGridEnt = CEntMan.GetEntity(gridNetEnt); + Assert.That(CEntMan.TryGetComponent(cGridEnt, out var cOverlay), + "Client grid is missing GasTileOverlayComponent"); + + // Check if the server actually sent the gas chunks + Assert.That(cOverlay, Is.Not.Null, "Gas overlay is null on the client."); + Assert.That(cOverlay.Chunks, Is.Not.Empty, "Gas overlay chunks are empty on the client."); + + //Start real tests + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, 400f); + + await CheckForInjectedGas(cOverlay, tileIndices, 400f); + + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, 800f + ThermalByte.TempDegreeResolution - 1); // Rounding test + + await CheckForInjectedGas(cOverlay, tileIndices, 800f); + + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMaximum + 200f); // This one hits max temperature + + await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMaximum); + + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum); + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2) - 1); // Test the networking optimisation, this should not be networked yet + + await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMinimum); + + await InjectHotPlasma(ProcessEnt, tileIndices, mixture, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2)); // This should + + await CheckForInjectedGas(cOverlay, tileIndices, ThermalByte.TempMinimum + (ThermalByte.TempDegreeResolution * 2)); + } + + private async Task CheckForInjectedGas(GasTileOverlayComponent overlay, Vector2i indices, float expectedTemp) + { + await Client.WaitPost(() => + { + var chunkIndices = SharedGasTileOverlaySystem.GetGasChunkIndices(indices); + + Assert.That(overlay.Chunks.TryGetValue(chunkIndices, out var chunk), "Chunk not found"); + Assert.That(chunk, Is.Not.Null, "Chunk not found"); + + // Calculate the exact index in the TileData array + var localX = MathHelper.Mod(indices.X, SharedGasTileOverlaySystem.ChunkSize); + var localY = MathHelper.Mod(indices.Y, SharedGasTileOverlaySystem.ChunkSize); + int tileIndex = localX + localY * SharedGasTileOverlaySystem.ChunkSize; + + var tile = chunk.TileData[tileIndex]; + tile.ByteGasTemperature.TryGetTemperature(out var actualTemp); + + Assert.That(actualTemp, Is.EqualTo(expectedTemp).Within(0.01f), $"Tile at {indices} had wrong temperature!"); + }); + } + + private async Task InjectHotPlasma(EntityUid gridEnt, Vector2i tileIndices, GasMixture mixture, float temperature) + { + //Server makes atmos + await Server.WaitPost(() => + { + if (mixture != null) + { + mixture.Clear(); + mixture.AdjustMoles(Gas.Plasma, 100f); // Inject hot plasma + mixture.Temperature = temperature; + SAtmos.InvalidateVisuals(gridEnt, tileIndices); + } + }); + + await RunTicks(60); + await Task.WhenAll(Client.WaitIdleAsync(), Server.WaitIdleAsync()); + } +} diff --git a/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs b/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs new file mode 100644 index 00000000000..368bb63c1d0 --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs @@ -0,0 +1,159 @@ +using Content.Shared.Atmos; +using Content.Shared.CCVar; +using Content.Shared.Coordinates; +using Content.Shared.Tests; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Atmos; + +[TestOf(typeof(Atmospherics))] +public abstract class TileAtmosphereTest : AtmosTest +{ + /// + /// Spawns gas in an enclosed space and checks that pressure equalizes within reasonable time. + /// Checks that mole count stays the same. + /// + [Test] + public async Task GasSpreading() + { + var markers = SEntMan.AllEntities(); + + EntityUid source, point1, point2; + source = point1 = point2 = EntityUid.Invalid; + + Assert.Multiple(() => + { + Assert.That(GetMarker(markers, "source", out source)); + Assert.That(GetMarker(markers, "point1", out point1)); + Assert.That(GetMarker(markers, "point2", out point2)); + }); + + Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0.0f)); + + var sourceMix = SAtmos.GetTileMixture(source, true); + Assert.That(sourceMix, Is.Not.EqualTo(null)); + sourceMix.AdjustMoles(Gas.Frezon, Moles); + + await Pair.Server.WaitPost(() => + { + SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate); + }); + + var mix1 = SAtmos.GetTileMixture(point1); + var mix2 = SAtmos.GetTileMixture(point2); + + Assert.Multiple(() => + { + Assert.That(mix1, Is.Not.EqualTo(null)); + Assert.That(mix2, Is.Not.EqualTo(null)); + }); + + AssertMixMoles(mix1, mix2, Tolerance); + AssertGridMoles(Moles, Tolerance); + } + + /// + /// Spawns a combustible mixture and sets it ablaze. + /// Checks that fire propages through the entire grid. + /// + [Test] + public async Task FireSpreading() + { + var markers = SEntMan.AllEntities(); + + EntityUid source, point1, point2; + source = point1 = point2 = EntityUid.Invalid; + + Vector2i sourceXY, point1XY, point2XY; + sourceXY = point1XY = point2XY = Vector2i.Zero; + + Assert.Multiple(() => + { + Assert.That(GetMarker(markers, "source", out source)); + Assert.That(GetMarker(markers, "point1", out point1)); + Assert.That(GetMarker(markers, "point2", out point2)); + + Assert.That(Transform.TryGetGridTilePosition(source, out sourceXY, MapData.Grid)); + Assert.That(Transform.TryGetGridTilePosition(source, out point1XY, MapData.Grid)); + Assert.That(Transform.TryGetGridTilePosition(source, out point2XY, MapData.Grid)); + }); + + Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0)); + + var sourceMix = SAtmos.GetTileMixture(source, true); + Assert.That(sourceMix, Is.Not.EqualTo(null)); + + sourceMix.AdjustMoles(Gas.Plasma, Moles / 10); + sourceMix.AdjustMoles(Gas.Oxygen, Moles - Moles / 10); + sourceMix.Temperature = Atmospherics.FireMinimumTemperatureToExist - 10; + + Assert.Multiple(() => + { + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.False); + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.False); + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.False); + }); + + await Server.WaitAssertion(() => + { + var welder = SEntMan.SpawnEntity("Welder", source.ToCoordinates()); + Assert.That(ItemToggleSys.TryActivate(welder)); + }); + + await Server.WaitRunTicks(500); + + Assert.Multiple(() => + { + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.True); + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.True); + Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.True); + }); + + var mix1 = SAtmos.GetTileMixture(point1); + var mix2 = SAtmos.GetTileMixture(point2); + + Assert.Multiple(() => + { + Assert.That(mix1, Is.Not.EqualTo(null)); + Assert.That(mix2, Is.Not.EqualTo(null)); + }); + + AssertMixMoles(mix1, mix2, Tolerance); + AssertGridMoles(Moles, Tolerance); + } +} + +// Declare separate fixtures to override the TestMap and configure CVars +public sealed class TileAtmosphereTest_X : TileAtmosphereTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml"); +} + +public sealed class TileAtmosphereTest_Snake : TileAtmosphereTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml"); +} + +public sealed class TileAtmosphereTest_LINDA_X : TileAtmosphereTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml"); + public override async Task Setup() + { + await base.Setup(); + Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization)); + Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false); + } +} + +public sealed class TileAtmosphereTest_LINDA_Snake : TileAtmosphereTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml"); + public override async Task Setup() + { + await base.Setup(); + Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization)); + Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false); + } +} diff --git a/Content.IntegrationTests/Tests/Body/GibTest.cs b/Content.IntegrationTests/Tests/Body/GibTest.cs deleted file mode 100644 index 4627c79f64d..00000000000 --- a/Content.IntegrationTests/Tests/Body/GibTest.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable enable -using Content.Server.Body.Systems; -using Robust.Shared.GameObjects; - -namespace Content.IntegrationTests.Tests.Body; - -[TestFixture] -public sealed class GibTest -{ - [Test] - public async Task TestGib() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); - var (server, client) = (pair.Server, pair.Client); - var map = await pair.CreateTestMap(); - - EntityUid target1 = default; - EntityUid target2 = default; - - await server.WaitAssertion(() => target1 = server.EntMan.Spawn("MobHuman", map.MapCoords)); - await server.WaitAssertion(() => target2 = server.EntMan.Spawn("MobHuman", map.MapCoords)); - await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target1)} CaptainGear"); - await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target2)} CaptainGear"); - - await pair.RunTicksSync(5); - var nuid1 = pair.ToClientUid(target1); - var nuid2 = pair.ToClientUid(target2); - Assert.That(client.EntMan.EntityExists(nuid1)); - Assert.That(client.EntMan.EntityExists(nuid2)); - - await server.WaitAssertion(() => server.System().GibBody(target1, gibOrgans: false)); - await server.WaitAssertion(() => server.System().GibBody(target2, gibOrgans: true)); - - await pair.RunTicksSync(5); - await pair.WaitCommand("dirty"); - await pair.RunTicksSync(5); - - Assert.That(!client.EntMan.EntityExists(nuid1)); - Assert.That(!client.EntMan.EntityExists(nuid2)); - - await pair.CleanReturnAsync(); - } -} diff --git a/Content.IntegrationTests/Tests/Body/GibbingTest.cs b/Content.IntegrationTests/Tests/Body/GibbingTest.cs new file mode 100644 index 00000000000..a3f3c1bcc37 --- /dev/null +++ b/Content.IntegrationTests/Tests/Body/GibbingTest.cs @@ -0,0 +1,59 @@ +using Content.IntegrationTests.Fixtures; +using Content.Shared.Body; +using Content.Shared.Gibbing; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Body; + +[TestFixture] +[TestOf(typeof(GibbableOrganSystem))] +public sealed class GibletTest : GameTest +{ + [TestPrototypes] + private const string Prototypes = @" +- type: entity + id: GibbingBody + components: + - type: Body + - type: EntityTableContainerFill + containers: + body_organs: !type:AllSelector + children: + - id: Giblet + - id: Giblet + - id: Giblet + +- type: entity + id: Giblet + components: + - type: Organ + - type: GibbableOrgan + - type: Physics +"; + + [Test] + public async Task GibletCountTest() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapData = await pair.CreateTestMap(); + + await server.WaitAssertion(() => + { + var body = entityManager.SpawnEntity("GibbingBody", mapData.GridCoords); + var gibbing = entityManager.System(); + var giblets = gibbing.Gib(body); + + Assert.That(giblets.Count, Is.EqualTo(3)); + + foreach (var giblet in giblets) + { + Assert.That(entityManager.HasComponent(giblet), Is.True); + } + }); + } +} diff --git a/Content.IntegrationTests/Tests/Body/HandOrganTest.cs b/Content.IntegrationTests/Tests/Body/HandOrganTest.cs new file mode 100644 index 00000000000..3d885186f18 --- /dev/null +++ b/Content.IntegrationTests/Tests/Body/HandOrganTest.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Body; +using Content.Shared.Hands.Components; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Body; + +[TestFixture] +[TestOf(typeof(HandOrganSystem))] +public sealed class HandOrganTest : GameTest +{ + [TestPrototypes] + private const string Prototypes = @" +- type: entity + id: TheBody + components: + - type: Body + - type: Hands + - type: EntityTableContainerFill + containers: + body_organs: !type:AllSelector + children: + - id: LeftHand + - id: RightHand + +- type: entity + id: LeftHand + components: + - type: Organ + - type: HandOrgan + handID: left + data: + location: Left + +- type: entity + id: RightHand + components: + - type: Organ + - type: HandOrgan + handID: right + data: + location: Right +"; + [Test] + public async Task HandInsertionAndRemovalTest() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapData = await pair.CreateTestMap(); + + await server.WaitAssertion(() => + { + var container = entityManager.System(); + var body = entityManager.SpawnEntity("TheBody", mapData.GridCoords); + var hands = entityManager.GetComponent(body); + + Assert.That(hands.Count, Is.EqualTo(2)); + + var handsContainer = container.GetContainer(body, BodyComponent.ContainerID); + + var expectedCount = 2; + var contained = handsContainer.ContainedEntities.ToList(); + foreach (var hand in contained) + { + expectedCount--; + container.Remove(hand, handsContainer); + Assert.That(hands.Count, Is.EqualTo(expectedCount)); + } + + var protos = new List() { "LeftHand", "RightHand" }; + foreach (var proto in protos) + { + expectedCount++; + entityManager.SpawnInContainerOrDrop(proto, body, BodyComponent.ContainerID); + Assert.That(hands.Count, Is.EqualTo(expectedCount)); + } + }); + } +} diff --git a/Content.IntegrationTests/Tests/Body/LegTest.cs b/Content.IntegrationTests/Tests/Body/LegTest.cs deleted file mode 100644 index 7b49bbe84a3..00000000000 --- a/Content.IntegrationTests/Tests/Body/LegTest.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Numerics; -using Content.Server.Body.Systems; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Content.Shared.Rotation; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; - -namespace Content.IntegrationTests.Tests.Body -{ - [TestFixture] - [TestOf(typeof(BodyPartComponent))] - [TestOf(typeof(BodyComponent))] - public sealed class LegTest - { - [TestPrototypes] - private const string Prototypes = @" -- type: entity - name: HumanBodyAndAppearanceDummy - id: HumanBodyAndAppearanceDummy - components: - - type: Appearance - - type: Body - prototype: Human - - type: StandingState -"; - - [Test] - public async Task RemoveLegsFallTest() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - EntityUid human = default!; - AppearanceComponent appearance = null; - - var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); - var appearanceSystem = entityManager.System(); - var xformSystem = entityManager.System(); - - var map = await pair.CreateTestMap(); - - await server.WaitAssertion(() => - { - BodyComponent body = null; - - human = entityManager.SpawnEntity("HumanBodyAndAppearanceDummy", - new MapCoordinates(Vector2.Zero, map.MapId)); - - Assert.Multiple(() => - { - Assert.That(entityManager.TryGetComponent(human, out body)); - Assert.That(entityManager.TryGetComponent(human, out appearance)); - }); - - Assert.That(!appearanceSystem.TryGetData(human, RotationVisuals.RotationState, out RotationState _, appearance)); - - var bodySystem = entityManager.System(); - var legs = bodySystem.GetBodyChildrenOfType(human, BodyPartType.Leg, body); - - foreach (var leg in legs) - { - xformSystem.DetachEntity(leg.Id, entityManager.GetComponent(leg.Id)); - } - }); - - await server.WaitAssertion(() => - { -#pragma warning disable NUnit2045 - // Interdependent assertions. - Assert.That(appearanceSystem.TryGetData(human, RotationVisuals.RotationState, out RotationState state, appearance)); - Assert.That(state, Is.EqualTo(RotationState.Horizontal)); -#pragma warning restore NUnit2045 - }); - await pair.CleanReturnAsync(); - } - } -} diff --git a/Content.IntegrationTests/Tests/Body/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs deleted file mode 100644 index 8ac3a3021f1..00000000000 --- a/Content.IntegrationTests/Tests/Body/LungTest.cs +++ /dev/null @@ -1,193 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Shared.Body.Components; -using Robust.Server.GameObjects; -using Robust.Shared; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using System.Linq; -using System.Numerics; -using Robust.Shared.EntitySerialization.Systems; -using Robust.Shared.Utility; - -namespace Content.IntegrationTests.Tests.Body -{ - [TestFixture] - [TestOf(typeof(LungSystem))] - public sealed class LungTest - { - [TestPrototypes] - private const string Prototypes = @" -- type: entity - name: HumanLungDummy - id: HumanLungDummy - components: - - type: SolutionContainerManager - - type: Body - prototype: Human - - type: MobState - allowedStates: - - Alive - - type: Damageable - - type: ThermalRegulator - metabolismHeat: 5000 - radiatedHeat: 400 - implicitHeatRegulation: 5000 - sweatHeatRegulation: 5000 - shiveringHeatRegulation: 5000 - normalBodyTemperature: 310.15 - thermalRegulationTemperatureThreshold: 25 - - type: Respirator - damage: - types: - Asphyxiation: 1.5 - damageRecovery: - types: - Asphyxiation: -1.5 -"; - - [Test] - public async Task AirConsistencyTest() - { - // --- Setup - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - await server.WaitIdleAsync(); - - var entityManager = server.ResolveDependency(); - var mapLoader = entityManager.System(); - var mapSys = entityManager.System(); - - EntityUid? grid = null; - BodyComponent body = default; - RespiratorComponent resp = default; - EntityUid human = default; - GridAtmosphereComponent relevantAtmos = default; - var startingMoles = 0.0f; - - var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml"); - - await server.WaitPost(() => - { - mapSys.CreateMap(out var mapId); - Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt)); - grid = gridEnt!.Value.Owner; - }); - - Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); - - float GetMapMoles() - { - var totalMapMoles = 0.0f; - foreach (var tile in relevantAtmos.Tiles.Values) - { - totalMapMoles += tile.Air?.TotalMoles ?? 0.0f; - } - - return totalMapMoles; - } - - await server.WaitAssertion(() => - { - var center = new Vector2(0.5f, 0.5f); - var coordinates = new EntityCoordinates(grid.Value, center); - human = entityManager.SpawnEntity("HumanLungDummy", coordinates); - relevantAtmos = entityManager.GetComponent(grid.Value); - startingMoles = 100f; // Hardcoded because GetMapMoles returns 900 here for some reason. - -#pragma warning disable NUnit2045 - Assert.That(entityManager.TryGetComponent(human, out body), Is.True); - Assert.That(entityManager.TryGetComponent(human, out resp), Is.True); -#pragma warning restore NUnit2045 - }); - - // --- End setup - - var inhaleCycles = 100; - for (var i = 0; i < inhaleCycles; i++) - { - // Breathe in - await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Exhaling); - Assert.That( - GetMapMoles(), Is.LessThan(startingMoles), - "Did not inhale in any gas" - ); - - // Breathe out - await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Inhaling); - Assert.That( - GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0002), - "Did not exhale as much gas as was inhaled" - ); - } - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task NoSuffocationTest() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var mapManager = server.ResolveDependency(); - var entityManager = server.ResolveDependency(); - var cfg = server.ResolveDependency(); - var mapLoader = entityManager.System(); - var mapSys = entityManager.System(); - - EntityUid? grid = null; - RespiratorComponent respirator = null; - EntityUid human = default; - - var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml"); - - await server.WaitPost(() => - { - mapSys.CreateMap(out var mapId); - Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt)); - grid = gridEnt!.Value.Owner; - }); - - Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); - - await server.WaitAssertion(() => - { - var center = new Vector2(0.5f, 0.5f); - - var coordinates = new EntityCoordinates(grid.Value, center); - human = entityManager.SpawnEntity("HumanLungDummy", coordinates); - - var mixture = entityManager.System().GetContainingMixture(human); -#pragma warning disable NUnit2045 - Assert.That(mixture.TotalMoles, Is.GreaterThan(0)); - Assert.That(entityManager.HasComponent(human), Is.True); - Assert.That(entityManager.TryGetComponent(human, out respirator), Is.True); - Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold)); -#pragma warning restore NUnit2045 - }); - - var increment = 10; - - // 20 seconds - var total = 20 * cfg.GetCVar(CVars.NetTickrate); - - for (var tick = 0; tick < total; tick += increment) - { - await server.WaitRunTicks(increment); - await server.WaitAssertion(() => - { - Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold), - $"Entity {entityManager.GetComponent(human).EntityName} is suffocating on tick {tick}"); - }); - } - - await pair.CleanReturnAsync(); - } - } -} diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs deleted file mode 100644 index 67163d07961..00000000000 --- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Robust.Shared.Containers; -using Robust.Shared.EntitySerialization.Systems; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Utility; - -namespace Content.IntegrationTests.Tests.Body; - -[TestFixture] -public sealed class SaveLoadReparentTest -{ - [TestPrototypes] - private const string Prototypes = @" -- type: entity - name: HumanBodyDummy - id: HumanBodyDummy - components: - - type: Body - prototype: Human -"; - - [Test] - public async Task Test() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var entities = server.ResolveDependency(); - var maps = server.ResolveDependency(); - var mapLoader = entities.System(); - var bodySystem = entities.System(); - var containerSystem = entities.System(); - var mapSys = entities.System(); - - await server.WaitAssertion(() => - { - mapSys.CreateMap(out var mapId); - maps.CreateGrid(mapId); - var human = entities.SpawnEntity("HumanBodyDummy", new MapCoordinates(0, 0, mapId)); - - Assert.That(entities.HasComponent(human), Is.True); - - var parts = bodySystem.GetBodyChildren(human).Skip(1).ToArray(); - var organs = bodySystem.GetBodyOrgans(human).ToArray(); - - Assert.Multiple(() => - { - Assert.That(parts, Is.Not.Empty); - Assert.That(organs, Is.Not.Empty); - }); - - foreach (var (id, component) in parts) - { - Assert.Multiple(() => - { - Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(component.Body, Is.Not.Null); - var parent = bodySystem.GetParentPartOrNull(id); - Assert.That(parent, Is.Not.EqualTo(default(EntityUid))); - if (!bodySystem.IsPartRoot(component.Body.Value, id, null, component)) - { - Assert.That(parent, Is.Not.Null); - } - else - { - Assert.That(parent, Is.Null); - } - }); - - foreach (var (slotId, slot) in component.Children) - { - Assert.Multiple(() => - { - Assert.That(slot.Id, Is.EqualTo(slotId)); - var container = - containerSystem.GetContainer(id, SharedBodySystem.GetPartSlotContainerId(slotId)); - Assert.That(container.ContainedEntities, Is.Not.Empty); - }); - } - } - - foreach (var (id, component) in organs) - { - var parent = bodySystem.GetParentPartOrNull(id); - - Assert.Multiple(() => - { - Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(parent, Is.Not.Null); - Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); - }); - } - - // Converts an entity query enumerator to an enumerable. - static IEnumerable<(EntityUid Uid, TComp Comp)> EnumerateQueryEnumerator(EntityQueryEnumerator query) - where TComp : Component - { - while (query.MoveNext(out var uid, out var comp)) - yield return (uid, comp); - } - - Assert.That( - EnumerateQueryEnumerator( - entities.EntityQueryEnumerator() - ).Where((e) => - entities.GetComponent(e.Uid).EntityPrototype!.Name == "HumanBodyDummy" - ), - Is.Not.Empty - ); - - var mapPath = new ResPath($"/{nameof(SaveLoadReparentTest)}{nameof(Test)}map.yml"); - - Assert.That(mapLoader.TrySaveMap(mapId, mapPath)); - mapSys.DeleteMap(mapId); - - Assert.That(mapLoader.TryLoadMap(mapPath, out var map, out _), Is.True); - - var query = EnumerateQueryEnumerator( - entities.EntityQueryEnumerator() - ).Where((e) => - entities.GetComponent(e.Uid).EntityPrototype!.Name == "HumanBodyDummy" - ).ToArray(); - - Assert.That(query, Is.Not.Empty); - foreach (var (uid, body) in query) - { - human = uid; - parts = bodySystem.GetBodyChildren(human).Skip(1).ToArray(); - organs = bodySystem.GetBodyOrgans(human).ToArray(); - - Assert.Multiple(() => - { - Assert.That(parts, Is.Not.Empty); - Assert.That(organs, Is.Not.Empty); - }); - - foreach (var (id, component) in parts) - { - var parent = bodySystem.GetParentPartOrNull(id); - - Assert.Multiple(() => - { - Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(parent, Is.Not.Null); - Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); - }); - - foreach (var (slotId, slot) in component.Children) - { - Assert.Multiple(() => - { - Assert.That(slot.Id, Is.EqualTo(slotId)); - var container = - containerSystem.GetContainer(id, SharedBodySystem.GetPartSlotContainerId(slotId)); - Assert.That(container.ContainedEntities, Is.Not.Empty); - }); - } - } - - foreach (var (id, component) in organs) - { - var parent = bodySystem.GetParentPartOrNull(id); - - Assert.Multiple(() => - { - Assert.That(component.Body, Is.EqualTo(human)); - Assert.That(parent, Is.Not.Null); - Assert.That(parent.Value, Is.Not.EqualTo(default(EntityUid))); - }); - } - - entities.DeleteEntity(map); - } - }); - - await pair.CleanReturnAsync(); - } -} diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs index d9cce764ab7..444bd159f38 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs @@ -12,7 +12,7 @@ public sealed partial class BuckleTest [Test] public async Task BuckleInteractUnbuckleOther() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -55,14 +55,12 @@ await server.WaitAssertion(() => Assert.That(strap.BuckledEntities, Does.Not.Contain(victim)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task BuckleInteractBuckleUnbuckleSelf() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -102,7 +100,5 @@ await server.WaitAssertion(() => Assert.That(strap.BuckledEntities, Does.Not.Contain(user)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index d6dec6fe151..9c12da3fe8a 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -1,9 +1,7 @@ using System.Numerics; -using Content.Server.Body.Systems; +using Content.IntegrationTests.Fixtures; using Content.Shared.Buckle; using Content.Shared.ActionBlocker; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; using Content.Shared.Buckle.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -15,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Buckle [TestFixture] [TestOf(typeof(BuckleComponent))] [TestOf(typeof(StrapComponent))] - public sealed partial class BuckleTest + public sealed partial class BuckleTest : GameTest { private const string BuckleDummyId = "BuckleDummy"; private const string StrapDummyId = "StrapDummy"; @@ -31,6 +29,8 @@ public sealed partial class BuckleTest - type: Hands - type: ComplexInteraction - type: InputMover + - type: Physics + bodyType: KinematicController - type: Body prototype: Human - type: StandingState @@ -51,7 +51,7 @@ public sealed partial class BuckleTest [Test] public async Task BuckleUnbuckleCooldownRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -229,14 +229,12 @@ await server.WaitAssertion(() => Assert.That(strap.BuckledEntities, Is.Empty); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task BuckledDyingDropItemsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -245,7 +243,6 @@ public async Task BuckledDyingDropItemsTest() EntityUid human = default; BuckleComponent buckle = null; HandsComponent hands = null; - BodyComponent body = null; await server.WaitIdleAsync(); @@ -265,7 +262,6 @@ await server.WaitAssertion(() => Assert.That(entityManager.TryGetComponent(human, out buckle)); Assert.That(entityManager.HasComponent(chair)); Assert.That(entityManager.TryGetComponent(human, out hands)); - Assert.That(entityManager.TryGetComponent(human, out body)); }); // Buckle @@ -292,46 +288,21 @@ await server.WaitAssertion(() => // Still buckled Assert.That(buckle.Buckled); - // With items in all hands + // Still with items in hand foreach (var hand in hands.Hands.Keys) { Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null); } - var bodySystem = entityManager.System(); - var legs = bodySystem.GetBodyChildrenOfType(human, BodyPartType.Leg, body); - - // Break our guy's kneecaps - foreach (var leg in legs) - { - entityManager.DeleteEntity(leg.Id); - } - }); - - await server.WaitRunTicks(10); - - await server.WaitAssertion(() => - { - // Still buckled - Assert.That(buckle.Buckled); - - // Now with no item in any hand - foreach (var hand in hands.Hands.Keys) - { - Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null); - } - buckleSystem.Unbuckle(human, human); Assert.That(buckle.Buckled, Is.False); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ForceUnbuckleBuckleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -399,7 +370,6 @@ await server.WaitAssertion(() => Assert.That(buckle.Buckled); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs index aad87b711a4..0c8fc2f0fb4 100644 --- a/Content.IntegrationTests/Tests/CargoTest.cs +++ b/Content.IntegrationTests/Tests/CargoTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; using Content.Server.Nutrition.Components; @@ -17,7 +18,7 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class CargoTest +public sealed class CargoTest : GameTest { private static readonly HashSet> Ignored = [ @@ -28,7 +29,7 @@ public sealed class CargoTest [Test] public async Task NoCargoOrderArbitrage() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -54,13 +55,11 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task NoCargoBountyArbitrageTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -94,14 +93,12 @@ await server.WaitAssertion(() => mapSystem.DeleteMap(mapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task NoStaticPriceAndStackPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ProtoMan; @@ -133,8 +130,6 @@ await server.WaitAssertion(() => } } }); - - await pair.CleanReturnAsync(); } /// @@ -144,7 +139,7 @@ await server.WaitAssertion(() => [Test] public async Task NoSliceableBountyArbitrageTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -209,19 +204,14 @@ await server.WaitAssertion(() => } mapSystem.DeleteMap(mapId); }); - - await pair.CleanReturnAsync(); } [TestPrototypes] private const string StackProto = @" -- type: entity - id: A - - type: stack id: StackProto name: stack-steel - spawn: A + spawn: StackEnt - type: entity id: StackEnt @@ -236,7 +226,7 @@ await server.WaitAssertion(() => [Test] public async Task StackPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -248,14 +238,12 @@ await server.WaitAssertion(() => var price = priceSystem.GetPrice(ent); Assert.That(price, Is.EqualTo(100.0)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task MobPrice() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var componentFactory = pair.Server.ResolveDependency(); @@ -269,7 +257,5 @@ await pair.Server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs b/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs new file mode 100644 index 00000000000..55f405f039e --- /dev/null +++ b/Content.IntegrationTests/Tests/Chemistry/DrainTest.cs @@ -0,0 +1,124 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids.Components; +using Content.Shared.Nutrition.Components; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Chemistry; + +public sealed class DrainTest : InteractionTest +{ + private static readonly EntProtoId PizzaPrototype = "FoodPizzaMargherita"; + private static readonly EntProtoId DrainPrototype = "FloorDrain"; + private static readonly EntProtoId BucketPrototype = "Bucket"; + private static readonly ProtoId BloodReagent = "Blood"; + private static readonly ProtoId WaterReagent = "Water"; + private static readonly FixedPoint2 WaterVolume = 50; // 50u + private static readonly FixedPoint2 PuddleVolume = 30; // 30u + + [TestPrototypes] + private static readonly string Prototypes = @$" +- type: entity + parent: Puddle + id: PuddleBloodTest + suffix: Blood (30u) + components: + - type: SolutionContainerManager + solutions: + puddle: + maxVol: 1000 + reagents: + - ReagentId: {BloodReagent} + Quantity: {PuddleVolume} +"; + + + /// + /// Tests that drag drop interactions with drains are working as intended. + /// + [Test] + public async Task DragDropOntoDrainTest() + { + var solutionContainerSys = SEntMan.System(); + + // Spawn a drain one tile away. + var drain = await Spawn(DrainPrototype); + + // Spawn a bucket at the player's coordinates. + var bucket = await Spawn(BucketPrototype, PlayerCoords); + + // Add water to the bucket. + Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out var solutionEnt, out var solution), "Bucket had no drainable solution."); + await Server.WaitAssertion(() => + { + Assert.That(solutionContainerSys.TryAddReagent(solutionEnt.Value, WaterReagent, WaterVolume), "Could not add water to the bucket."); + }); + + // Check that the bucket was filled. + Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after filling it."); + Assert.That(solution.Volume, Is.EqualTo(WaterVolume)); + + // Drag drop the bucket onto the drain. + await DragDrop(bucket, drain); + + // Check that the bucket is empty. + Assert.That(solutionContainerSys.TryGetDrainableSolution(ToServer(bucket), out solutionEnt, out solution), "Bucket had no drainable solution after draining it."); + Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.Zero), "Bucket was not empty after draining it."); + + await Delete(bucket); + + // Spawn a pizza at the player's coordinates. + var pizza = await Spawn(PizzaPrototype, PlayerCoords); + + // Check that the pizza is not empty. + var edibleSolutionId = Comp(pizza).Solution; + Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution."); + var pizzaVolume = solution.Volume; + Assert.That(pizzaVolume, Is.GreaterThan(FixedPoint2.Zero), "Pizza had no reagents inside its edible solution."); + + // Drag drop the pizza onto the drain. + // Yes, this was a bug that existed before. + await DragDrop(pizza, drain); + + // Check that the pizza did not get deleted or had its reagents drained. + AssertExists(pizza); + Assert.That(solutionContainerSys.TryGetSolution(ToServer(pizza), edibleSolutionId, out solutionEnt, out solution), "Pizza had no edible solution."); + Assert.That(solution.Volume, Is.EqualTo(pizzaVolume), "Pizza lost reagents when drag dropped onto a drain."); + } + + /// + /// Tests that drains make puddles next to them disappear. + /// + [Test] + public async Task DrainPuddleTest() + { + var solutionContainerSys = SEntMan.System(); + + // Spawn a puddle at the player coordinates; + var puddle = await Spawn("PuddleBloodTest", PlayerCoords); + + // Make sure the reagent chosen for this test does not evaporate on its own. + // If you are a fork that made more reagents evaporate, just change BloodReagent ProtoId above to something else. + Assert.That(HasComp(puddle), Is.False, "The chosen reagent is evaporating on its own and we cannot use it for the drain test."); + + var puddleSolutionId = Comp(puddle).SolutionName; + Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out var solution), "Puddle had no solution."); + Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning."); + + // Wait a few seconds and check that the puddle did not disappear on its own. + await RunSeconds(10); + Assert.That(solutionContainerSys.TryGetSolution(ToServer(puddle), puddleSolutionId, out _, out solution), "Puddle had no solution."); + Assert.That(solution.Volume, Is.EqualTo(PuddleVolume), "Puddle had the wrong amount of reagents after spawning."); + + // Spawn a drain one tile away. + await Spawn(DrainPrototype); + + // Wait a few seconds. + await RunSeconds(10); + + // Make sure the puddle was deleted by the drain. + AssertDeleted(puddle); + } +} diff --git a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs index f488734655a..9ac45549a29 100644 --- a/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/ReagentDataTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Tests.Interaction; using Content.Shared.Chemistry.Reagent; using Robust.Shared.Reflection; @@ -8,12 +9,12 @@ namespace Content.IntegrationTests.Tests.Chemistry; [TestFixture] [TestOf(typeof(ReagentData))] -public sealed class ReagentDataTest : InteractionTest +public sealed class ReagentDataTest : GameTest { [Test] public async Task ReagentDataIsSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var reflection = pair.Server.ResolveDependency(); Assert.Multiple(() => @@ -24,7 +25,5 @@ public async Task ReagentDataIsSerializable() Assert.That(instance.HasCustomAttribute(), $"{instance} must have the serializable attribute."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs index 89d33186a27..f188fd0f66e 100644 --- a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Chemistry; [TestFixture] [TestOf(typeof(ChemicalReactionSystem))] -public sealed class SolutionRoundingTest +public sealed class SolutionRoundingTest : GameTest { // This test tests two things: // * A rounding error in reaction code while I was making chloral hydrate @@ -64,10 +65,15 @@ public sealed class SolutionRoundingTest SolutionRoundingTestReagentD: 1 "; + private const string SolutionRoundingTestReagentA = "SolutionRoundingTestReagentA"; + private const string SolutionRoundingTestReagentB = "SolutionRoundingTestReagentB"; + private const string SolutionRoundingTestReagentC = "SolutionRoundingTestReagentC"; + private const string SolutionRoundingTestReagentD = "SolutionRoundingTestReagentD"; + [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -84,12 +90,12 @@ await server.WaitPost(() => solutionEnt = newSolutionEnt!.Value; solution = newSolution!; - system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentC", 50)); - system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentB", 30)); + system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentC, 50)); + system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentB, 30)); for (var i = 0; i < 9; i++) { - system.TryAddSolution(solutionEnt, new Solution("SolutionRoundingTestReagentA", 10)); + system.TryAddSolution(solutionEnt, new Solution(SolutionRoundingTestReagentA, 10)); } }); @@ -98,25 +104,23 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That( - solution.ContainsReagent("SolutionRoundingTestReagentA", null), + solution.ContainsReagent(SolutionRoundingTestReagentA, null), Is.False, "Solution should not contain reagent A"); Assert.That( - solution.ContainsReagent("SolutionRoundingTestReagentB", null), + solution.ContainsReagent(SolutionRoundingTestReagentB, null), Is.False, "Solution should not contain reagent B"); Assert.That( - solution![new ReagentId("SolutionRoundingTestReagentC", null)].Quantity, + solution![new ReagentId(SolutionRoundingTestReagentC, null)].Quantity, Is.EqualTo((FixedPoint2) 20)); Assert.That( - solution![new ReagentId("SolutionRoundingTestReagentD", null)].Quantity, + solution![new ReagentId(SolutionRoundingTestReagentD, null)].Quantity, Is.EqualTo((FixedPoint2) 30)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs index 6b71dd08be0..ffaca012a34 100644 --- a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Chemistry; // reactions can change this assumption [TestFixture] [TestOf(typeof(SharedSolutionContainerSystem))] -public sealed class SolutionSystemTests +public sealed class SolutionSystemTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -43,10 +44,17 @@ public sealed class SolutionSystemTests desc: reagent-desc-nothing physicalDesc: reagent-physical-desc-nothing "; + + private const string TestReagentA = "TestReagentA"; + private const string TestReagentB = "TestReagentB"; + private const string TestReagentC = "TestReagentC"; + private const string Water = "Water"; + private const string Oil = "Oil"; + [Test] public async Task TryAddTwoNonReactiveReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -62,8 +70,8 @@ await server.WaitAssertion(() => var oilQuantity = FixedPoint2.New(15); var waterQuantity = FixedPoint2.New(10); - var oilAdded = new Solution("Oil", oilQuantity); - var originalWater = new Solution("Water", waterQuantity); + var oilAdded = new Solution(Oil, oilQuantity); + var originalWater = new Solution(Water, waterQuantity); beaker = entityManager.SpawnEntity("SolutionTarget", coordinates); Assert.That(containerSystem @@ -73,16 +81,14 @@ await server.WaitAssertion(() => Assert.That(containerSystem .TryAddSolution(solutionEnt.Value, oilAdded)); - var water = solution.GetTotalPrototypeQuantity("Water"); - var oil = solution.GetTotalPrototypeQuantity("Oil"); + var water = solution.GetTotalPrototypeQuantity(Water); + var oil = solution.GetTotalPrototypeQuantity(Oil); Assert.Multiple(() => { Assert.That(water, Is.EqualTo(waterQuantity)); Assert.That(oil, Is.EqualTo(oilQuantity)); }); }); - - await pair.CleanReturnAsync(); } // This test mimics current behavior @@ -90,7 +96,7 @@ await server.WaitAssertion(() => [Test] public async Task TryAddTooMuchNonReactiveReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -107,8 +113,8 @@ await server.WaitAssertion(() => var oilQuantity = FixedPoint2.New(1500); var waterQuantity = FixedPoint2.New(10); - var oilAdded = new Solution("Oil", oilQuantity); - var originalWater = new Solution("Water", waterQuantity); + var oilAdded = new Solution(Oil, oilQuantity); + var originalWater = new Solution(Water, waterQuantity); beaker = entityManager.SpawnEntity("SolutionTarget", coordinates); Assert.That(containerSystem @@ -118,23 +124,21 @@ await server.WaitAssertion(() => Assert.That(containerSystem .TryAddSolution(solutionEnt.Value, oilAdded), Is.False); - var water = solution.GetTotalPrototypeQuantity("Water"); - var oil = solution.GetTotalPrototypeQuantity("Oil"); + var water = solution.GetTotalPrototypeQuantity(Water); + var oil = solution.GetTotalPrototypeQuantity(Oil); Assert.Multiple(() => { Assert.That(water, Is.EqualTo(waterQuantity)); Assert.That(oil, Is.EqualTo(FixedPoint2.Zero)); }); }); - - await pair.CleanReturnAsync(); } // Unlike TryAddSolution this adds and two solution without then splits leaving only threshold in original [Test] public async Task TryMixAndOverflowTooMuchReagent() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; @@ -153,8 +157,8 @@ await server.WaitAssertion(() => var waterQuantity = FixedPoint2.New(10); var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int()); - var oilAdded = new Solution("Oil", oilQuantity); - var originalWater = new Solution("Water", waterQuantity); + var oilAdded = new Solution(Oil, oilQuantity); + var originalWater = new Solution(Water, waterQuantity); beaker = entityManager.SpawnEntity("SolutionTarget", coordinates); Assert.That(containerSystem @@ -168,28 +172,26 @@ await server.WaitAssertion(() => { Assert.That(solution.Volume, Is.EqualTo(FixedPoint2.New(threshold))); - var waterMix = solution.GetTotalPrototypeQuantity("Water"); - var oilMix = solution.GetTotalPrototypeQuantity("Oil"); + var waterMix = solution.GetTotalPrototypeQuantity(Water); + var oilMix = solution.GetTotalPrototypeQuantity(Oil); Assert.That(waterMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1)))); Assert.That(oilMix, Is.EqualTo(FixedPoint2.New(threshold / (ratio + 1) * ratio))); Assert.That(overflowingSolution.Volume, Is.EqualTo(FixedPoint2.New(80))); - var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity("Water"); - var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity("Oil"); + var waterOverflow = overflowingSolution.GetTotalPrototypeQuantity(Water); + var oilOverFlow = overflowingSolution.GetTotalPrototypeQuantity(Oil); Assert.That(waterOverflow, Is.EqualTo(waterQuantity - waterMix)); Assert.That(oilOverFlow, Is.EqualTo(oilQuantity - oilMix)); }); }); - - await pair.CleanReturnAsync(); } // TryMixAndOverflow will fail if Threshold larger than MaxVolume [Test] public async Task TryMixAndOverflowTooBigOverflow() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -207,8 +209,8 @@ await server.WaitAssertion(() => var waterQuantity = FixedPoint2.New(10); var oilQuantity = FixedPoint2.New(ratio * waterQuantity.Int()); - var oilAdded = new Solution("Oil", oilQuantity); - var originalWater = new Solution("Water", waterQuantity); + var oilAdded = new Solution(Oil, oilQuantity); + var originalWater = new Solution(Water, waterQuantity); beaker = entityManager.SpawnEntity("SolutionTarget", coordinates); Assert.That(containerSystem @@ -219,14 +221,12 @@ await server.WaitAssertion(() => .TryMixAndOverflow(solutionEnt.Value, oilAdded, threshold, out _), Is.False); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestTemperatureCalculations() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); const float temp = 100.0f; @@ -234,31 +234,28 @@ public async Task TestTemperatureCalculations() // Adding reagent with adjusts temperature await server.WaitAssertion(() => { - - var solution = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp }; + var solution = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp }; Assert.That(solution.Temperature, Is.EqualTo(temp * 1)); - solution.AddSolution(new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan); + solution.AddSolution(new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp * 3 }, protoMan); Assert.That(solution.Temperature, Is.EqualTo(temp * 2)); - solution.AddSolution(new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan); + solution.AddSolution(new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp * 5 }, protoMan); Assert.That(solution.Temperature, Is.EqualTo(temp * 3)); }); // adding solutions combines thermal energy await server.WaitAssertion(() => { - var solutionOne = new Solution("TestReagentA", FixedPoint2.New(100)) { Temperature = temp }; + var solutionOne = new Solution(TestReagentA, FixedPoint2.New(100)) { Temperature = temp }; - var solutionTwo = new Solution("TestReagentB", FixedPoint2.New(100)) { Temperature = temp }; - solutionTwo.AddReagent("TestReagentC", FixedPoint2.New(100)); + var solutionTwo = new Solution(TestReagentB, FixedPoint2.New(100)) { Temperature = temp }; + solutionTwo.AddReagent(TestReagentC, FixedPoint2.New(100)); var thermalEnergyOne = solutionOne.GetHeatCapacity(protoMan) * solutionOne.Temperature; var thermalEnergyTwo = solutionTwo.GetHeatCapacity(protoMan) * solutionTwo.Temperature; solutionOne.AddSolution(solutionTwo, protoMan); Assert.That(solutionOne.GetHeatCapacity(protoMan) * solutionOne.Temperature, Is.EqualTo(thermalEnergyOne + thermalEnergyTwo)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs index 6d860f0ac38..e720387425e 100644 --- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs @@ -5,13 +5,15 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Shared.Chemistry.EntitySystems; namespace Content.IntegrationTests.Tests.Chemistry { [TestFixture] [TestOf(typeof(ReactionPrototype))] - public sealed class TryAllReactionsTest + public sealed class TryAllReactionsTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -24,10 +26,15 @@ public sealed class TryAllReactionsTest maxVol: 50 canMix: true"; + private static string[] _reactions = GameDataScrounger.PrototypesOfKind(); + [Test] - public async Task TryAllTest() + [TestCaseSource(nameof(_reactions))] + [TestOf(typeof(ReactionPrototype))] + [Description("Tries an individual reaction to see if it succeeds.")] + public async Task TryReaction(string reaction) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -36,85 +43,98 @@ public async Task TryAllTest() var coordinates = testMap.GridCoords; var solutionContainerSystem = entityManager.System(); - foreach (var reactionPrototype in prototypeManager.EnumeratePrototypes()) - { - //since i have no clue how to isolate each loop assert-wise im just gonna throw this one in for good measure - Console.WriteLine($"Testing {reactionPrototype.ID}"); + var reactionPrototype = prototypeManager.Index(reaction); - EntityUid beaker = default; - Entity? solutionEnt = default!; - Solution solution = null; + EntityUid beaker = default; + Solution solution = null; + Entity? solutionEnt = default!; await server.WaitAssertion(() => { beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates); Assert.That(solutionContainerSystem .TryGetSolution(beaker, "beaker", out solutionEnt, out solution)); + solutionContainerSystem.SetCanReact(solutionEnt!.Value, false); foreach (var (id, reactant) in reactionPrototype.Reactants) { #pragma warning disable NUnit2045 - Assert.That(solutionContainerSystem - .TryAddReagent(solutionEnt.Value, id, reactant.Amount, out var quantity)); - Assert.That(reactant.Amount, Is.EqualTo(quantity)); + Assert.That(solutionContainerSystem + .TryAddReagent(solutionEnt.Value, + id, + reactant.Amount, + out var quantity, + reactionPrototype.MinimumTemperature)); + Assert.That(reactant.Amount, Is.EqualTo(quantity)); #pragma warning restore NUnit2045 - } + } - //Get all possible reactions with the current reagents - var possibleReactions = prototypeManager.EnumeratePrototypes() - .Where(x => x.Reactants.All(id => solution.Contents.Any(s => s.Reagent.Prototype == id.Key))) - .ToList(); + //Get all possible reactions with the current reagents + var possibleReactions = prototypeManager.EnumeratePrototypes() + .Where(x => x.Reactants.All(id => solution.Contents.Any(s => s.Reagent.Prototype == id.Key))) + .ToList(); - //Check if the reaction is the first to occur when heated - foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature)) + //Check if the reaction is the first to occur when heated + foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature)) + { + if (possibleReaction.Priority >= reactionPrototype.Priority && + possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && + possibleReaction.MixingCategories == reactionPrototype.MixingCategories) { - if (possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories) - { - Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated."); - } + Assert.Fail( + $"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated."); } + } - //Check if the reaction is the first to occur when freezing - foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature)) + //Check if the reaction is the first to occur when freezing + foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature)) + { + if (possibleReaction.Priority >= reactionPrototype.Priority && + possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && + possibleReaction.MixingCategories == reactionPrototype.MixingCategories) { - if (possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature && possibleReaction.MixingCategories == reactionPrototype.MixingCategories) - { - Assert.Fail($"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing."); - } + Assert.Fail( + $"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing."); } + } //Now safe set the temperature and mix the reagents solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature); + solutionContainerSystem.SetCanReact(solutionEnt.Value, true); - if (reactionPrototype.MixingCategories != null) - { - var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace); - var mixerComponent = entityManager.AddComponent(dummyEntity); - mixerComponent.ReactionTypes = reactionPrototype.MixingCategories; - solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent); - } - }); + if (reactionPrototype.MixingCategories != null) + { + var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace); + var mixerComponent = entityManager.AddComponent(dummyEntity); + mixerComponent.ReactionTypes = reactionPrototype.MixingCategories; + solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent); + } + }); - await server.WaitIdleAsync(); + await server.WaitIdleAsync(); - await server.WaitAssertion(() => + await server.WaitAssertion(() => + { + //you just got linq'd fool + //(i'm sorry) + var foundProductsMap = reactionPrototype.Products + .Concat(reactionPrototype.Reactants + .Where(x => x.Value.Catalyst) + .ToDictionary(x => x.Key, x => x.Value.Amount) + ) + .ToDictionary(x => x, _ => false); + + foreach (var (reagent, quantity) in solution.Contents) { - //you just got linq'd fool - //(i'm sorry) - var foundProductsMap = reactionPrototype.Products - .Concat(reactionPrototype.Reactants.Where(x => x.Value.Catalyst).ToDictionary(x => x.Key, x => x.Value.Amount)) - .ToDictionary(x => x, _ => false); - foreach (var (reagent, quantity) in solution.Contents) - { - Assert.That(foundProductsMap.TryFirstOrNull(x => x.Key.Key == reagent.Prototype && x.Key.Value == quantity, out var foundProduct)); - foundProductsMap[foundProduct.Value.Key] = true; - } + Assert.That(foundProductsMap.TryFirstOrNull( + x => x.Key.Key == reagent.Prototype && x.Key.Value == quantity, + out var foundProduct)); + foundProductsMap[foundProduct!.Value.Key] = true; + } - Assert.That(foundProductsMap.All(x => x.Value)); - }); + Assert.That(foundProductsMap.All(x => x.Value)); - } - await pair.CleanReturnAsync(); + server.EntMan.DeleteEntity(beaker); + }); } } - } diff --git a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs index e2bff03501a..037224fec55 100644 --- a/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs +++ b/Content.IntegrationTests/Tests/Cleanup/EuiManagerTest.cs @@ -1,35 +1,37 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Administration.UI; using Content.Server.EUI; using Robust.Server.Player; namespace Content.IntegrationTests.Tests.Cleanup; -public sealed class EuiManagerTest +public sealed class EuiManagerTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + Dirty = true + }; + [Test] + [Retry(2)] + // Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes public async Task EuiManagerRecycleWithOpenWindowTest() { - // Even though we are using the server EUI here, we actually want to see if the client EUIManager crashes - for (var i = 0; i < 2; i++) - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true - }); - var server = pair.Server; + var pair = Pair; + var server = pair.Server; - var sPlayerManager = server.ResolveDependency(); - var eui = server.ResolveDependency(); + var sPlayerManager = server.ResolveDependency(); + var eui = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var clientSession = sPlayerManager.Sessions.Single(); + var ui = new AdminAnnounceEui(); + eui.OpenEui(ui, clientSession); + }); - await server.WaitAssertion(() => - { - var clientSession = sPlayerManager.Sessions.Single(); - var ui = new AdminAnnounceEui(); - eui.OpenEui(ui, clientSession); - }); - await pair.CleanReturnAsync(); - } + await RunUntilSynced(); } } diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs index aaac421ed1a..fb20cd90388 100644 --- a/Content.IntegrationTests/Tests/ClickableTest.cs +++ b/Content.IntegrationTests/Tests/ClickableTest.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Clickable; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class ClickableTest + public sealed class ClickableTest : GameTest { private const double DirSouth = 0; private const double DirNorth = Math.PI; @@ -44,7 +45,7 @@ public sealed class ClickableTest [TestCase("ClickTestRotatingCornerInvisibleNoRot", 0.25f, 0.25f, DirSouthEastJustShy, 1, ExpectedResult = true)] public async Task Test(string prototype, float clickPosX, float clickPosY, double angle, float scale) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -66,7 +67,7 @@ await server.WaitPost(() => }); // Let client sync up. - await pair.RunTicksSync(5); + await RunUntilSynced(); var hit = false; var clientEnt = clientEntManager.GetEntity(serverEntManager.GetNetEntity(serverEnt)); @@ -89,8 +90,6 @@ await server.WaitPost(() => serverEntManager.DeleteEntity(serverEnt); }); - await pair.CleanReturnAsync(); - return hit; } } diff --git a/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs b/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs index 9edc115eee2..c21bcf922ae 100644 --- a/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs @@ -1,8 +1,9 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Cloning; namespace Content.IntegrationTests.Tests.Cloning; -public sealed class CloningSettingsPrototypeTest +public sealed class CloningSettingsPrototypeTest : GameTest { /// /// Checks that the components named in every are valid components known to the server. @@ -12,7 +13,7 @@ public sealed class CloningSettingsPrototypeTest [Test] public async Task ValidatePrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; var compFactory = server.EntMan.ComponentFactory; @@ -40,7 +41,5 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs b/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs index 3fa7e64f1a4..566edd47cae 100644 --- a/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs +++ b/Content.IntegrationTests/Tests/Commands/ForceMapTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Maps; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests.Commands; [TestFixture] -public sealed class ForceMapTest +public sealed class ForceMapTest : GameTest { private const string DefaultMapName = "Empty"; private const string BadMapName = "asdf_asd-fa__sdfAsd_f"; // Hopefully no one ever names a map this... @@ -44,7 +45,7 @@ public sealed class ForceMapTest [Test] public async Task TestForceMapCommand() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -82,7 +83,5 @@ await server.WaitAssertion(() => // Cleanup configManager.SetCVar(CCVars.GameMap, DefaultMapName); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs index a77761a7d17..eb18d8e1ae0 100644 --- a/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs +++ b/Content.IntegrationTests/Tests/Commands/ObjectiveCommandsTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Objectives; using Content.Shared.Mind; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests.Commands; -public sealed class ObjectiveCommandsTest +public sealed class ObjectiveCommandsTest : GameTest { private const string ObjectiveProtoId = "MindCommandsTestObjective"; @@ -27,6 +28,11 @@ public sealed class ObjectiveCommandsTest - type: DieCondition """; + public override PoolSettings PoolSettings => new () + { + Connected = false + }; + /// /// Creates a dummy session, and assigns it a mind, then /// tests using addobjective, lsobjectives, @@ -35,7 +41,7 @@ public sealed class ObjectiveCommandsTest [Test] public async Task AddListRemoveObjectiveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; var playerMan = server.ResolveDependency(); @@ -54,7 +60,7 @@ await server.WaitPost(() => }); Assert.That(mindEnt, Is.Not.Null); - var mindComp = mindEnt.Value.Comp; + var mindComp = mindEnt!.Value.Comp; Assert.That(mindComp.Objectives, Is.Empty, "Dummy player started with objectives."); await pair.WaitCommand($"addobjective {playerSession.Name} {ObjectiveProtoId}"); @@ -66,7 +72,5 @@ await server.WaitPost(() => await pair.WaitCommand($"rmobjective {playerSession.Name} 0"); Assert.That(mindComp.Objectives, Is.Empty, "rmobjective failed to remove objective"); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs index 9e57cd4b0e6..2d84eebf962 100644 --- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs +++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Database; using Robust.Server.Console; using Robust.Server.Player; @@ -8,14 +9,14 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(PardonCommand))] - public sealed class PardonCommand + public sealed class PardonCommand : GameTest { private static readonly TimeSpan MarginOfError = TimeSpan.FromMinutes(1); [Test] public async Task PardonTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -32,9 +33,9 @@ public async Task PardonTest() // No bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty); }); // Try to pardon a ban that does not exist @@ -43,9 +44,9 @@ public async Task PardonTest() // Still no bans on record Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty); }); var banReason = "test"; @@ -57,9 +58,9 @@ public async Task PardonTest() // Should have one ban on record now Assert.Multiple(async () => { - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); await pair.RunTicksSync(5); @@ -70,17 +71,17 @@ public async Task PardonTest() await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2")); // The existing ban is unaffected - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null); - var ban = await sDatabase.GetServerBanAsync(1); + var ban = await sDatabase.GetBanAsync(1); Assert.Multiple(async () => { Assert.That(ban, Is.Not.Null); - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); // Check that it matches Assert.That(ban.Id, Is.EqualTo(1)); - Assert.That(ban.UserId, Is.EqualTo(clientId)); + Assert.That(ban.UserIds, Is.EquivalentTo([clientId])); Assert.That(ban.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError)); Assert.That(ban.ExpirationTime, Is.Not.Null); Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); @@ -95,20 +96,20 @@ public async Task PardonTest() await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1")); // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban - var pardonedBan = await sDatabase.GetServerBanAsync(1); + var pardonedBan = await sDatabase.GetBanAsync(1); Assert.Multiple(async () => { // Check that it matches Assert.That(pardonedBan, Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); Assert.That(pardonedBan.Id, Is.EqualTo(1)); - Assert.That(pardonedBan.UserId, Is.EqualTo(clientId)); + Assert.That(pardonedBan.UserIds, Is.EquivalentTo([clientId])); Assert.That(pardonedBan.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError)); Assert.That(pardonedBan.ExpirationTime, Is.Not.Null); Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError)); @@ -133,13 +134,13 @@ public async Task PardonTest() Assert.Multiple(async () => { // No bans should be returned - Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null); + Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null); // Direct id lookup returns a pardoned ban - Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null); + Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null); // The list is still returned since that ignores pardons - Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); + Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1)); }); // Reconnect client. Slightly faster than dirtying the pair. @@ -148,8 +149,6 @@ public async Task PardonTest() await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!)); await pair.RunTicksSync(5); Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(1)); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs index e4ec7e907a4..15d93c40aed 100644 --- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs @@ -1,7 +1,9 @@ -using Content.Server.Administration.Commands; -using Content.Server.Administration.Systems; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Administration.Systems; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -13,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(RejuvenateSystem))] - public sealed class RejuvenateTest + public sealed class RejuvenateTest : GameTest { private static readonly ProtoId TestDamageGroup = "Toxin"; @@ -35,7 +37,7 @@ public sealed class RejuvenateTest [Test] public async Task RejuvenateDeadTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var prototypeManager = server.ResolveDependency(); @@ -88,10 +90,9 @@ await server.WaitAssertion(() => Assert.That(mobStateSystem.IsDead(human, mobState), Is.False); Assert.That(mobStateSystem.IsIncapacitated(human, mobState), Is.False); - Assert.That(damageable.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + Assert.That(damSystem.GetTotalDamage((human, damageable)), Is.EqualTo(FixedPoint2.Zero)); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs index 72a05b5246f..bcacc8011bc 100644 --- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; using Content.Shared.CCVar; @@ -10,25 +11,27 @@ namespace Content.IntegrationTests.Tests.Commands { [TestFixture] [TestOf(typeof(RestartRoundNowCommand))] - public sealed class RestartRoundNowTest + public sealed class RestartRoundNowTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Dirty = true + }; + [Test] [TestCase(true)] [TestCase(false)] public async Task RestartRoundAfterStart(bool lobbyEnabled) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var configManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTicker = entityManager.System(); - await pair.RunTicksSync(5); + await pair.RunUntilSynced(); GameTick tickBeforeRestart = default; @@ -58,8 +61,7 @@ await server.WaitAssertion(() => Assert.That(tickBeforeRestart, Is.LessThan(tickAfterRestart)); }); - await pair.RunTicksSync(5); - await pair.CleanReturnAsync(); + await pair.RunUntilSynced(); } } } diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs index c232ccf415e..f78bd4949ea 100644 --- a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs +++ b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs @@ -1,6 +1,9 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; 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.FixedPoint; using Content.Shared.Ghost; @@ -19,7 +22,7 @@ namespace Content.IntegrationTests.Tests.Commands; [TestFixture] -public sealed class SuicideCommandTests +public sealed class SuicideCommandTests : GameTest { [TestPrototypes] @@ -55,6 +58,13 @@ public sealed class SuicideCommandTests private static readonly ProtoId CannotSuicideTag = "CannotSuicide"; private static readonly ProtoId DamageType = "Slash"; + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + Dirty = true, + DummyTicker = false + }; + /// /// Run the suicide command in the console /// Should successfully kill the player and ghost them @@ -62,12 +72,7 @@ public sealed class SuicideCommandTests [Test] public async Task TestSuicide() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -102,8 +107,6 @@ await server.WaitAssertion(() => !ghostComp.CanReturnToBody); }); }); - - await pair.CleanReturnAsync(); } /// @@ -113,12 +116,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSuicideWhileDamaged() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -136,7 +134,6 @@ public async Task TestSuicideWhileDamaged() MindComponent mindComponent = default; MobStateComponent mobStateComp = default; MobThresholdsComponent mobThresholdsComp = default; - DamageableComponent damageableComp = default; await server.WaitPost(() => { if (mind != null) @@ -144,7 +141,6 @@ await server.WaitPost(() => mobStateComp = entManager.GetComponent(player); mobThresholdsComp = entManager.GetComponent(player); - damageableComp = entManager.GetComponent(player); var slashProto = protoMan.Index(DamageType); damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5))); @@ -163,11 +159,9 @@ await server.WaitAssertion(() => Assert.That(mobStateSystem.IsDead(player, mobStateComp)); Assert.That(entManager.TryGetComponent(mindComponent.CurrentEntity, out var ghostComp) && !ghostComp.CanReturnToBody); - Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(lethalDamageThreshold)); + Assert.That(damageableSystem.GetTotalDamage(player), Is.EqualTo(lethalDamageThreshold)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -177,12 +171,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSuicideWhenCannotSuicide() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -217,8 +206,6 @@ await server.WaitAssertion(() => !ghostComp.CanReturnToBody); }); }); - - await pair.CleanReturnAsync(); } @@ -228,12 +215,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSuicideByHeldItem() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -280,7 +262,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { // Heal all damage first (possible low pressure damage taken) - damageableSystem.SetAllDamage(player, damageableComp, 0); + damageableSystem.ClearAllDamage((player, damageableComp)); consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last(); @@ -289,11 +271,9 @@ await server.WaitAssertion(() => Assert.That(mobStateSystem.IsDead(player, mobStateComp)); Assert.That(entManager.TryGetComponent(mindComponent.CurrentEntity, out var ghostComp) && !ghostComp.CanReturnToBody); - Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold)); + Assert.That(damageableSystem.GetAllDamage((player, damageableComp)).DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -303,12 +283,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSuicideByHeldItemSpreadDamage() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var consoleHost = server.ResolveDependency(); var entManager = server.ResolveDependency(); @@ -355,7 +330,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { // Heal all damage first (possible low pressure damage taken) - damageableSystem.SetAllDamage(player, damageableComp, 0); + damageableSystem.ClearAllDamage((player, damageableComp)); consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide"); var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last(); @@ -364,10 +339,8 @@ await server.WaitAssertion(() => Assert.That(mobStateSystem.IsDead(player, mobStateComp)); Assert.That(entManager.TryGetComponent(mindComponent.CurrentEntity, out var ghostComp) && !ghostComp.CanReturnToBody); - Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2)); + Assert.That(damageableSystem.GetAllDamage((player, damageableComp)).DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ConfigPresetTests.cs b/Content.IntegrationTests/Tests/ConfigPresetTests.cs index ebeea7f391d..224a5677c05 100644 --- a/Content.IntegrationTests/Tests/ConfigPresetTests.cs +++ b/Content.IntegrationTests/Tests/ConfigPresetTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using Content.IntegrationTests.Fixtures; using Content.Server.Entry; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ConfigPresetTests +public sealed class ConfigPresetTests : GameTest { [Test] public async Task TestLoadAll() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var resources = server.ResolveDependency(); @@ -70,7 +71,5 @@ await server.WaitPost(() => Assert.Fail($"CVar {name} was not reset to its original value."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs index c59b42d6389..8c68e5b1926 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionActionValid.cs @@ -1,4 +1,5 @@ using System.Text; +using Content.IntegrationTests.Fixtures; using Content.Server.Construction.Completions; using Content.Shared.Construction; using Content.Shared.Construction.Prototypes; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests.Construction { [TestFixture] - public sealed class ConstructionActionValid + public sealed class ConstructionActionValid : GameTest { private bool IsValid(IGraphAction action, IPrototypeManager protoMan, out string prototype) { @@ -47,7 +48,7 @@ private bool IsValid(IGraphAction action, IPrototypeManager protoMan, out string [Test] public async Task ConstructionGraphSpawnPrototypeValid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -84,13 +85,12 @@ await server.WaitPost(() => }); Assert.That(valid, Is.True, $"One or more SpawnPrototype actions specified invalid entity prototypes!\n{message}"); - await pair.CleanReturnAsync(); } [Test] public async Task ConstructionGraphEdgeValid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -118,7 +118,6 @@ await server.WaitPost(() => }); Assert.That(valid, Is.True, $"One or more edges specified invalid node targets!\n{message}"); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs index 75281e63cb0..49cc76e9458 100644 --- a/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Construction/ConstructionPrototypeTest.cs @@ -1,133 +1,64 @@ -using System.Numerics; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Server.Construction.Components; using Content.Shared.Construction.Prototypes; using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Construction { [TestFixture] - public sealed class ConstructionPrototypeTest + public sealed class ConstructionPrototypeTest : GameTest { // discount linter for construction graphs // TODO: Create serialization validators for these? // Top test definitely can be but writing a serializer takes ages. + private static string[] _constructablePrototypes = GameDataScrounger.EntitiesWithComponent("Construction"); + private static string[] _constructions = GameDataScrounger.PrototypesOfKind(); + /// /// Checks every entity prototype with a construction component has a valid start node. /// [Test] - public async Task TestStartNodeValid() + [TestOf(typeof(ConstructionComponent))] + [TestCaseSource(nameof(_constructablePrototypes))] + [Description("Tests that a given entity specifies a valid node for construction, and optionally a valid one for deconstruction.")] + public async Task ConstructionComponentValid(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; - var entMan = server.ResolveDependency(); var protoMan = server.ResolveDependency(); - var map = await pair.CreateTestMap(); - await server.WaitAssertion(() => { - foreach (var proto in protoMan.EnumeratePrototypes()) - { - if (!proto.Components.ContainsKey("Construction")) - continue; - - var ent = entMan.SpawnEntity(proto.ID, new MapCoordinates(Vector2.Zero, map.MapId)); - var construction = entMan.GetComponent(ent); - - var graph = protoMan.Index(construction.Graph); - entMan.DeleteEntity(ent); - - Assert.That(graph.Nodes.ContainsKey(construction.Node), - $"Found no startNode \"{construction.Node}\" on graph \"{graph.ID}\" for entity \"{proto.ID}\"!"); - } - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task TestStartIsValid() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; + var proto = protoMan.Index(protoKey); + var construction = (ConstructionComponent)proto.Components["Construction"].Component; - var protoMan = server.ResolveDependency(); + var graph = protoMan.Index(construction.Graph); - await server.WaitAssertion(() => - { - foreach (var proto in protoMan.EnumeratePrototypes()) + using (Assert.EnterMultipleScope()) { - var start = proto.StartNode; - var graph = protoMan.Index(proto.Graph); - - Assert.That(graph.Nodes.ContainsKey(start), - $"Found no startNode \"{start}\" on graph \"{graph.ID}\" for construction prototype \"{proto.ID}\"!"); - } - }); - await pair.CleanReturnAsync(); - } - - [Test] - public async Task TestTargetIsValid() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; + Assert.That(graph.Nodes.ContainsKey(construction.Node), + $"Found no node \"{construction.Node}\" on graph \"{graph.ID}\" for entity \"{proto.ID}\"!"); - var protoMan = server.ResolveDependency(); - - await server.WaitAssertion(() => - { - foreach (var proto in protoMan.EnumeratePrototypes()) - { - var target = proto.TargetNode; - var graph = protoMan.Index(proto.Graph); + if (construction.DeconstructionNode is not { } target) + return; Assert.That(graph.Nodes.ContainsKey(target), - $"Found no targetNode \"{target}\" on graph \"{graph.ID}\" for construction prototype \"{proto.ID}\"!"); + $"Invalid deconstruction node \"{target}\" on graph \"{graph.ID}\" for construction entity \"{proto.ID}\"!"); } }); - await pair.CleanReturnAsync(); } [Test] - public async Task DeconstructionIsValid() + [TestOf(typeof(ConstructionPrototype))] + [TestCaseSource(nameof(_constructions))] + [Description("Tests that a given construction prototype has a valid starting and target node, and a valid path between them.")] + public async Task ConstructionFormsValidGraph(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var protoMan = server.ResolveDependency(); - var compFact = server.ResolveDependency(); - - var name = compFact.GetComponentName(); - Assert.Multiple(() => - { - foreach (var proto in protoMan.EnumeratePrototypes()) - { - if (proto.Abstract || pair.IsTestPrototype(proto) || !proto.Components.TryGetValue(name, out var reg)) - continue; - - var comp = (ConstructionComponent) reg.Component; - var target = comp.DeconstructionNode; - if (target == null) - continue; - - var graph = protoMan.Index(comp.Graph); - Assert.That(graph.Nodes.ContainsKey(target), $"Invalid deconstruction node \"{target}\" on graph \"{graph.ID}\" for construction entity \"{proto.ID}\"!"); - } - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task TestStartReachesValidTarget() - { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -135,30 +66,34 @@ public async Task TestStartReachesValidTarget() await server.WaitAssertion(() => { - foreach (var proto in protoMan.EnumeratePrototypes()) + var proto = protoMan.Index(protoKey); + var start = proto.StartNode; + var target = proto.TargetNode; + var graph = protoMan.Index(proto.Graph); + + using (Assert.EnterMultipleScope()) { - var start = proto.StartNode; - var target = proto.TargetNode; - var graph = protoMan.Index(proto.Graph); + Assert.That(graph.Nodes.ContainsKey(start), + $"Found no startNode \"{start}\" on graph \"{graph.ID}\"!"); + Assert.That(graph.Nodes.ContainsKey(target), + $"Found no targetNode \"{target}\" on graph \"{graph.ID}\"!"); + } #pragma warning disable NUnit2045 // Interdependent assertions. - Assert.That(graph.TryPath(start, target, out var path), - $"Unable to find path from \"{start}\" to \"{target}\" on graph \"{graph.ID}\""); - Assert.That(path, Has.Length.GreaterThanOrEqualTo(1), - $"Unable to find path from \"{start}\" to \"{target}\" on graph \"{graph.ID}\"."); - var next = path[0]; - var nextId = next.Entity.GetId(null, null, new(entMan)); - Assert.That(nextId, Is.Not.Null, - $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) must specify an entity! Graph: {graph.ID}"); - Assert.That(protoMan.TryIndex(nextId, out EntityPrototype entity), - $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) specified an invalid entity prototype ({nextId} [{next.Entity}])"); - Assert.That(entity.Components.ContainsKey("Construction"), - $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) specified an entity prototype ({next.Entity}) without a ConstructionComponent."); + Assert.That(graph.TryPath(start, target, out var path), + $"Unable to find path from \"{start}\" to \"{target}\" on graph \"{graph.ID}\""); + Assert.That(path, Has.Length.GreaterThanOrEqualTo(1), + $"Unable to find path from \"{start}\" to \"{target}\" on graph \"{graph.ID}\"."); + var next = path![0]; + var nextId = next.Entity.GetId(null, null, new(entMan)); + Assert.That(nextId, Is.Not.Null, + $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) must specify an entity! Graph: {graph.ID}"); + Assert.That(protoMan.TryIndex(nextId, out EntityPrototype entity), + $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) specified an invalid entity prototype ({nextId} [{next.Entity}])"); + Assert.That(entity!.Components.ContainsKey("Construction"), + $"The next node ({next.Name}) in the path from the start node ({start}) to the target node ({target}) specified an entity prototype ({next.Entity}) without a ConstructionComponent."); #pragma warning restore NUnit2045 - } }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/BorgConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/BorgConstruction.cs new file mode 100644 index 00000000000..b7a21419a4b --- /dev/null +++ b/Content.IntegrationTests/Tests/Construction/Interaction/BorgConstruction.cs @@ -0,0 +1,63 @@ +using Content.IntegrationTests.Tests.Interaction; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Construction.Interaction; + +public sealed class BorgConstruction : InteractionTest +{ + /// + /// ID of the base endoskeleton to which parts will be added. + /// + private static readonly EntProtoId BorgEndoskeletonId = "CyborgEndoskeleton"; + + /// + /// ID of the entity that should be produced when construction is complete. + /// + private static readonly EntProtoId BorgCompleteId = "BorgChassisSelectable"; + + /// + /// ID of the Flashes needed for borg construction. + /// + private static readonly EntProtoId FlashId = "Flash"; + + /// + /// IDs of all the parts that need to be added to the endoskeleton to complete construction. + /// + private static readonly EntProtoId[] Parts = + [ + "TorsoBorg", + "LightHeadBorg", + "LeftArmBorg", + "RightArmBorg", + "LeftLegBorg", + "RightLegBorg", + ]; + + /// + /// Spawns an endoskeleton and makes the player add each part to it by interacting with the parts in hand. + /// Verifies that the correct entity is produced as a result. + /// + [Test] + public async Task ConstructBorg() + { + // Spawn the endoskeleton frame. + await SpawnTarget(BorgEndoskeletonId); + + // Have the player add each part needed for construction. + foreach (var part in Parts) + { + await InteractUsing(part); + } + + // Finish construction. + await Interact( + Cable, + FlashId, + FlashId, + Screw + ); + + // Construction should have finished - make sure to the resulting entity is correct. + AssertPrototype(BorgCompleteId, Target); + } +} diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs index 05e8197c8da..a7b96bdd2f9 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -95,8 +95,8 @@ public async Task CancelCraft() Assert.That(sys.IsEntityInContainer(shard), Is.True); Assert.That(sys.IsEntityInContainer(rods), Is.False); Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(rodStack, Has.Count.EqualTo(8)); - Assert.That(wireStack, Has.Count.EqualTo(7)); + Assert.That(rodStack.Count, Is.EqualTo(8)); + Assert.That(wireStack.Count, Is.EqualTo(7)); await FindEntity(Spear, shouldSucceed: false); diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs index 192604edfd5..582a05e9740 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs @@ -1,6 +1,8 @@ using Content.IntegrationTests.Tests.Interaction; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; @@ -17,17 +19,16 @@ public async Task RepairReinforcedWindow() // Damage the entity. var sys = SEntMan.System(); - var comp = Comp(); var damageType = Server.ProtoMan.Index(BluntDamageType); var damage = new DamageSpecifier(damageType, FixedPoint2.New(10)); - Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero)); - await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true)); + Assert.That(sys.GetTotalDamage(STarget.Value), Is.EqualTo(FixedPoint2.Zero)); + await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true)); await RunTicks(5); - Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero)); + Assert.That(sys.GetTotalDamage(STarget.Value), Is.GreaterThan(FixedPoint2.Zero)); // Repair the entity await InteractUsing(Weld); - Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sys.GetTotalDamage(STarget.Value), Is.EqualTo(FixedPoint2.Zero)); // Validate that we can still deconstruct the entity (i.e., that welding deconstruction is not blocked). await Interact( diff --git a/Content.IntegrationTests/Tests/Construction/RCDTest.cs b/Content.IntegrationTests/Tests/Construction/RCDTest.cs new file mode 100644 index 00000000000..770f004517d --- /dev/null +++ b/Content.IntegrationTests/Tests/Construction/RCDTest.cs @@ -0,0 +1,243 @@ +using System.Numerics; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Charges.Systems; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Construction; + +public sealed class RCDTest : InteractionTest +{ + private static readonly EntProtoId RCDProtoId = "RCD"; + private static readonly ProtoId RCDSettingWall = "WallSolid"; + private static readonly ProtoId RCDSettingAirlock = "Airlock"; + private static readonly ProtoId RCDSettingPlating = "Plating"; + private static readonly ProtoId RCDSettingFloorSteel = "FloorSteel"; + private static readonly ProtoId RCDSettingDeconstruct = "Deconstruct"; + private static readonly ProtoId RCDSettingDeconstructTile = "DeconstructTile"; + private static readonly ProtoId RCDSettingDeconstructLattice = "DeconstructLattice"; + + /// + /// Tests RCD construction and deconstruction, as well as selecting options from the radial menu. + /// + [Test] + public async Task RCDConstructionDeconstructionTest() + { + // Place some tiles around the player so that we have space to build. + var pNorth = new EntityCoordinates(SPlayer, new Vector2(0, 1)); + var pSouth = new EntityCoordinates(SPlayer, new Vector2(0, -1)); + var pEast = new EntityCoordinates(SPlayer, new Vector2(1, 0)); + var pWest = new EntityCoordinates(SPlayer, new Vector2(-1, 0)); + + // Use EntityCoordinates relative to the grid because the player turns around when interacting. + pNorth = Transform.WithEntityId(pNorth, MapData.Grid); + pSouth = Transform.WithEntityId(pSouth, MapData.Grid); + pEast = Transform.WithEntityId(pEast, MapData.Grid); + pWest = Transform.WithEntityId(pWest, MapData.Grid); + + await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pNorth), MapData.Grid); + await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pSouth), MapData.Grid); + await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pEast), MapData.Grid); + await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid); + + Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}."); + Assert.That(settingWall.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingAirlock, out var settingAirlock), $"RCDPrototype not found: {RCDSettingAirlock}."); + Assert.That(settingAirlock.Prototype, Is.Not.Null, $"RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingPlating, out var settingPlating), $"RCDPrototype not found: {RCDSettingPlating}."); + Assert.That(settingPlating.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingFloorSteel, out var settingFloorSteel), $"RCDPrototype not found: {RCDSettingFloorSteel}."); + Assert.That(settingFloorSteel.Prototype, Is.Not.Null, "RCDPrototype has a null spawning prototype."); + Assert.That(ProtoMan.TryIndex(RCDSettingDeconstructTile, out var settingDeconstructTile), $"RCDPrototype not found: {RCDSettingDeconstructTile}."); + Assert.That(ProtoMan.TryIndex(RCDSettingDeconstructLattice, out var settingDeconstructLattice), $"RCDPrototype not found: {RCDSettingDeconstructLattice}."); + + var rcd = await PlaceInHands(RCDProtoId); + + // Give the RCD enough charges to do everything. + var sCharges = SEntMan.System(); + await Server.WaitPost(() => + { + sCharges.SetMaxCharges(ToServer(rcd), 10000); + sCharges.SetCharges(ToServer(rcd), 10000); + }); + var initialCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(10000), "RCD did not have the correct amount of charges."); + + // Make sure that picking it up did not open the UI. + Assert.That(IsUiOpen(RcdUiKey.Key), Is.False, "RCD UI was opened when picking it up."); + + // Switch to building walls. + await SetRcdProto(rcd, RCDSettingWall); + + // Build a wall next to the player. + await Interact(null, pNorth); + + // Check that there is exactly one wall. + await RunSeconds(settingWall.Delay + 1); // wait for the construction to finish + await AssertEntityLookup((settingWall.Prototype, 1)); + + // Check that the wall is in the correct tile. + var wallUid = await FindEntity(settingWall.Prototype); + var wallNetUid = FromServer(wallUid); + AssertLocation(wallNetUid, FromServer(pNorth)); + + // Check that the cost of the wall was subtracted from the current charges. + var newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingWall.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Try building another wall in the same spot. + await Interact(null, pNorth); + await RunSeconds(settingWall.Delay + 1); // wait for the construction to finish + + // Check that there is still exactly one wall. + await AssertEntityLookup((settingWall.Prototype, 1)); + + // Check that the failed construction did not cost us any charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(newCharges), "RCD has wrong amount of charges after failing to build something."); + + // Switch to building airlocks. + await SetRcdProto(rcd, RCDSettingAirlock); + + // Build an airlock next to the player. + await Interact(null, pSouth); + + // Check that there is exactly one airlock. + await RunSeconds(settingAirlock.Delay + 1); // wait for the construction to finish + await AssertEntityLookup( + (settingWall.Prototype, 1), + (settingAirlock.Prototype, 1) + ); + + // Check that the airlock is in the correct tile. + var airlockUid = await FindEntity(settingAirlock.Prototype); + var airlockNetUid = FromServer(airlockUid); + AssertLocation(airlockNetUid, FromServer(pSouth)); + + // Check that the cost of the airlock was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingAirlock.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to building plating. + await SetRcdProto(rcd, RCDSettingPlating); + + // Try building plating on existing plating. + await AssertTile(settingPlating.Prototype, FromServer(pEast)); + await Interact(null, pEast); + + // Check that the tile did not change. + await AssertTile(settingPlating.Prototype, FromServer(pEast)); + + // Check that the failed construction did not cost us any charges. + await RunSeconds(settingPlating.Delay + 1); // wait for the construction to finish + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges, Is.EqualTo(newCharges), "RCD has wrong amount of charges after failing to build something."); + + // Try building plating on top of lattice. + await AssertTile(Lattice, FromServer(pWest)); + await Interact(null, pWest); + await RunSeconds(settingPlating.Delay + 1); // wait for the construction to finish + + // Check that the tile is now plating. + await AssertTile(settingPlating.Prototype, FromServer(pWest)); + + // Check that the cost of the plating was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingPlating.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to building steel tiles. + await SetRcdProto(rcd, RCDSettingFloorSteel); + + // Try building a steel tile on top of plating. + await Interact(null, pEast); + + // Check that the tile is now a steel tile. + await RunSeconds(settingFloorSteel.Delay + 1); // wait for the construction to finish + await AssertTile(settingFloorSteel.Prototype, FromServer(pEast)); + + // Check that the cost of the plating was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingFloorSteel.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after building something."); + initialCharges = newCharges; + + // Switch to deconstruction mode. + await SetRcdProto(rcd, RCDSettingDeconstruct); + + // Deconstruct the wall. + Assert.That(SEntMan.TryGetComponent(wallUid, out var wallComp), "Wall entity did not have the RCDDeconstructableComponent."); + await Interact(wallUid, pNorth); + await RunSeconds(wallComp.Delay + 1); // wait for the deconstruction to finish + AssertDeleted(wallNetUid); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - wallComp.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the airlock. + Assert.That(SEntMan.TryGetComponent(airlockUid, out var airlockComp), "Wall entity did not have the RCDDeconstructableComponent."); + await Interact(airlockUid, pSouth); + await RunSeconds(airlockComp.Delay + 1); // wait for the deconstruction to finish + AssertDeleted(airlockNetUid); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - airlockComp.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the steel tile. + await Interact(null, pEast); + await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish + await AssertTile(PlatingRCD, FromServer(pEast)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructTile.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the plating. + await Interact(null, pWest); + await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish + await AssertTile(Lattice, FromServer(pWest)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructTile.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + initialCharges = newCharges; + + // Deconstruct the lattice. + await Interact(null, pWest); + await RunSeconds(settingDeconstructLattice.Delay + 1); // wait for the deconstruction to finish + await AssertTile(null, FromServer(pWest)); + + // Check that the cost of the deconstruction was subtracted from the current charges. + newCharges = sCharges.GetCurrentCharges(ToServer(rcd)); + Assert.That(initialCharges - settingDeconstructLattice.Cost, Is.EqualTo(newCharges), "RCD has wrong amount of charges after deconstructing something."); + + // Wait for the visual effect to disappear. + await RunSeconds(3); + + // Check that there are no entities left. + await AssertEntityLookup(); + } + + private async Task SetRcdProto(NetEntity rcd, ProtoId protoId) + { + await UseInHand(); + await RunTicks(3); + Assert.That(IsUiOpen(RcdUiKey.Key), Is.True, "RCD UI was not opened when using the RCD while holding it."); + + // Simulating a click on the right control for nested radial menus is very complicated. + // So we just manually send a networking message from the client to tell the server we selected an option. + // TODO: Write a separate test for clicking through a SimpleRadialMenu. + await SendBui(RcdUiKey.Key, new RCDSystemMessage(protoId), rcd); + await CloseBui(RcdUiKey.Key, rcd); + Assert.That(IsUiOpen(RcdUiKey.Key), Is.False, "RCD UI is still open."); + } +} diff --git a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs index 37c4b0c9b57..cb7e4016018 100644 --- a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs +++ b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { - public sealed class ContainerOcclusionTest + public sealed class ContainerOcclusionTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -34,7 +35,7 @@ public sealed class ContainerOcclusionTest [Test] public async Task TestA() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -69,14 +70,12 @@ await client.WaitAssertion(() => Assert.That(light.ContainerOccluded); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestB() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -112,14 +111,12 @@ await client.WaitAssertion(() => Assert.That(light.ContainerOccluded, Is.False); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestAb() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -157,8 +154,6 @@ await client.WaitAssertion(() => Assert.That(light.ContainerOccluded); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs index c52ef293e1e..9f0fb1d4995 100644 --- a/Content.IntegrationTests/Tests/ContrabandTest.cs +++ b/Content.IntegrationTests/Tests/ContrabandTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Contraband; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -5,12 +6,12 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ContrabandTest +public sealed class ContrabandTest : GameTest { [Test] public async Task EntityShowDepartmentsAndJobs() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; var protoMan = client.ResolveDependency(); var componentFactory = client.ResolveDependency(); @@ -41,7 +42,5 @@ await client.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs index bd5cac05dd1..522b53b5a9c 100644 --- a/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Damageable; @@ -39,14 +41,14 @@ public void TestDamageSpecifierOperations() Assert.That(difference, Is.EqualTo(output5)); } - private static readonly Dictionary Input1 = new() + private static readonly Dictionary, FixedPoint2> Input1 = new() { { "A", 1.5f }, { "B", 2 }, { "C", 3 } }; - private static readonly Dictionary Input2 = new() + private static readonly Dictionary, FixedPoint2> Input2 = new() { { "A", 1 }, { "B", 2 }, @@ -54,28 +56,28 @@ public void TestDamageSpecifierOperations() { "D", 0.05f } }; - private static readonly Dictionary Output1 = new() + private static readonly Dictionary, FixedPoint2> Output1 = new() { { "A", -1.5f }, { "B", -2 }, { "C", -3 } }; - private static readonly Dictionary Output2 = new() + private static readonly Dictionary, FixedPoint2> Output2 = new() { { "A", 0.75f }, { "B", 1 }, { "C", 1.5 } }; - private static readonly Dictionary Output3 = new() + private static readonly Dictionary, FixedPoint2> Output3 = new() { { "A", 3f }, { "B", 4 }, { "C", 6 } }; - private static readonly Dictionary Output4 = new() + private static readonly Dictionary, FixedPoint2> Output4 = new() { { "A", 0.5f }, { "B", 0 }, @@ -83,7 +85,7 @@ public void TestDamageSpecifierOperations() { "D", -0.05f } }; - private static readonly Dictionary Output5 = new() + private static readonly Dictionary, FixedPoint2> Output5 = new() { { "A", 0.25f }, { "B", 0 }, diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs index f610ab732e9..758bbe0a50b 100644 --- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs @@ -1,5 +1,8 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -10,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Damageable [TestFixture] [TestOf(typeof(DamageableComponent))] [TestOf(typeof(DamageableSystem))] - public sealed class DamageableTest + public sealed class DamageableTest : GameTest { private const string TestDamageableEntityId = "TestDamageableEntityId"; private const string TestGroup1 = "TestGroup1"; @@ -93,7 +96,7 @@ public sealed class DamageableTest [Test] public async Task TestDamageableComponents() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -145,46 +148,36 @@ await server.WaitPost(() => await server.WaitAssertion(() => { var uid = sDamageableEntity; - - // Check that the correct types are supported. - Assert.Multiple(() => - { - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type1.ID), Is.False); - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type2a.ID), Is.True); - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type2b.ID), Is.False); - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3a.ID), Is.True); - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3b.ID), Is.True); - Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3c.ID), Is.True); - }); + var ent = new Entity(uid, sDamageableComponent); // Check that damage is evenly distributed over a group if its a nice multiple var types = group3.DamageTypes; var damageToDeal = FixedPoint2.New(types.Count * 5); DamageSpecifier damage = new(group3, damageToDeal); - sDamageableSystem.TryChangeDamage(uid, damage, true); + sDamageableSystem.ChangeDamage(uid, damage, true); Assert.Multiple(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal)); - Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(damageToDeal)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(damageToDeal)); + Assert.That(sDamageableSystem.GetDamagePerGroup(ent)[group3.ID], Is.EqualTo(damageToDeal)); foreach (var type in types) { - Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage)); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict.TryGetValue(type, out typeDamage)); Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count)); } }); // Heal - sDamageableSystem.TryChangeDamage(uid, -damage); + sDamageableSystem.ChangeDamage(uid, -damage); Assert.Multiple(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); - Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetDamagePerGroup(ent)[group3.ID], Is.EqualTo(FixedPoint2.Zero)); foreach (var type in types) { - Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage)); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict.TryGetValue(type, out typeDamage)); Assert.That(typeDamage, Is.EqualTo(FixedPoint2.Zero)); } }); @@ -195,70 +188,73 @@ await server.WaitAssertion(() => Assert.That(types, Has.Count.EqualTo(3)); damage = new DamageSpecifier(group3, 14); - sDamageableSystem.TryChangeDamage(uid, damage, true); + sDamageableSystem.ChangeDamage(uid, damage, true); Assert.Multiple(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(14))); - Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(FixedPoint2.New(14))); - Assert.That(sDamageableComponent.Damage.DamageDict[type3a.ID], Is.EqualTo(FixedPoint2.New(4.66f))); - Assert.That(sDamageableComponent.Damage.DamageDict[type3b.ID], Is.EqualTo(FixedPoint2.New(4.67f))); - Assert.That(sDamageableComponent.Damage.DamageDict[type3c.ID], Is.EqualTo(FixedPoint2.New(4.67f))); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.New(14))); + Assert.That(sDamageableSystem.GetDamagePerGroup(ent)[group3.ID], Is.EqualTo(FixedPoint2.New(14))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3a.ID], Is.EqualTo(FixedPoint2.New(4.66f))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3b.ID], Is.EqualTo(FixedPoint2.New(4.67f))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3c.ID], Is.EqualTo(FixedPoint2.New(4.67f))); }); // Heal - sDamageableSystem.TryChangeDamage(uid, -damage); + sDamageableSystem.ChangeDamage(uid, -damage); Assert.Multiple(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); - Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetDamagePerGroup(ent)[group3.ID], Is.EqualTo(FixedPoint2.Zero)); foreach (var type in types) { - Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage)); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict.TryGetValue(type, out typeDamage)); Assert.That(typeDamage, Is.EqualTo(FixedPoint2.Zero)); } // Test that unsupported groups return false when setting/getting damage (and don't change damage) - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); }); damage = new DamageSpecifier(group1, FixedPoint2.New(10)) + new DamageSpecifier(type2b, FixedPoint2.New(10)); - sDamageableSystem.TryChangeDamage(uid, damage, true); + sDamageableSystem.ChangeDamage(uid, damage, true); Assert.Multiple(() => { - Assert.That(sDamageableComponent.DamagePerGroup.TryGetValue(group1.ID, out _), Is.False); - Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type1.ID, out typeDamage), Is.False); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetDamagePerGroup(ent).TryGetValue(group1.ID, out _), Is.False); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict.TryGetValue(type1.ID, out typeDamage), Is.False); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); }); - // Test SetAll function - sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 10); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(10 * sDamageableComponent.Damage.DamageDict.Count))); - sDamageableSystem.SetAllDamage(sDamageableEntity, sDamageableComponent, 0); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + // Test SetAll and ClearAll function + sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.New(10 * sDamageableSystem.GetAllDamage(uid).DamageDict.Count))); + sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 0); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); + sDamageableSystem.SetAllDamage((sDamageableEntity, sDamageableComponent), 10); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.New(10 * sDamageableSystem.GetAllDamage(uid).DamageDict.Count))); + sDamageableSystem.ClearAllDamage((sDamageableEntity, sDamageableComponent)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); // Test 'wasted' healing - sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5)); - sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3b, 7)); - sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -11)); + sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3a, 5)); + sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3b, 7)); + sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -11)); Assert.Multiple(() => { - Assert.That(sDamageableComponent.Damage.DamageDict[type3a.ID], Is.EqualTo(FixedPoint2.New(1.34))); - Assert.That(sDamageableComponent.Damage.DamageDict[type3b.ID], Is.EqualTo(FixedPoint2.New(3.33))); - Assert.That(sDamageableComponent.Damage.DamageDict[type3c.ID], Is.EqualTo(FixedPoint2.New(0))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3a.ID], Is.EqualTo(FixedPoint2.New(1.34))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3b.ID], Is.EqualTo(FixedPoint2.New(3.33))); + Assert.That(sDamageableSystem.GetAllDamage(uid).DamageDict[type3c.ID], Is.EqualTo(FixedPoint2.New(0))); }); // Test Over-Healing - sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100))); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100))); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); // Test that if no health change occurred, returns false - sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100)); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -100)); + Assert.That(sDamageableSystem.GetTotalDamage(ent), Is.EqualTo(FixedPoint2.Zero)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs b/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs index e2f841fc596..c4179135cdf 100644 --- a/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/MobThresholdsTest.cs @@ -1,40 +1,38 @@ +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Shared.Alert; using Content.Shared.Mobs.Components; namespace Content.IntegrationTests.Tests.Damageable; -public sealed class MobThresholdsTest +public sealed class MobThresholdsTest : GameTest { - /// - /// Inspects every entity prototype with a and makes - /// sure that every possible mob state is mapped to an . - /// + private static string[] _entitiesWithThresholds = GameDataScrounger.EntitiesWithComponent("MobThresholds"); + [Test] - public async Task ValidateMobThresholds() + [TestOf(typeof(MobThresholdsComponent))] + [TestCaseSource(nameof(_entitiesWithThresholds))] + [Description("Ensures every entity with mob thresholds has valid mob state configuration corresponding to some AlertPrototype.")] + public async Task ValidateMobThresholds(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; - var entMan = server.EntMan; var protoMan = server.ProtoMan; - var protos = pair.GetPrototypesWithComponent(); - Assert.Multiple(() => { - foreach (var (proto, comp) in protos) + var proto = protoMan.Index(protoKey); + var comp = (MobThresholdsComponent)proto.Components["MobThresholds"].Component; + + // See which mob states are mapped to alerts + var alertStates = comp.StateAlertDict.Keys; + // Check each mob state that this mob can be in + foreach (var (_, state) in comp.Thresholds) { - // See which mob states are mapped to alerts - var alertStates = comp.StateAlertDict.Keys; - // Check each mob state that this mob can be in - foreach (var (_, state) in comp.Thresholds) - { - // Make sure that an alert exists for each possible mob state - Assert.That(alertStates, Does.Contain(state), $"{proto.ID} does not have an alert state for mob state {state}"); - } + // Make sure that an alert exists for each possible mob state + Assert.That(alertStates, Does.Contain(state), $"{proto.ID} does not have an alert state for mob state {state}"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs b/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs index f0a594dbd46..b4a278529da 100644 --- a/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/StaminaComponentTest.cs @@ -1,29 +1,51 @@ +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; namespace Content.IntegrationTests.Tests.Damageable; -public sealed class StaminaComponentTest +public sealed class StaminaComponentTest : GameTest { + private static string[] _entitiesWithStamina = GameDataScrounger.EntitiesWithComponent("Stamina"); + [Test] - public async Task ValidatePrototypes() + [TestOf(typeof(StaminaComponent))] + [TestCaseSource(nameof(_entitiesWithStamina))] + [Description("Ensures every entity with Stamina has a valid stamina configuration.")] + public async Task ValidateStamina(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; - - var protos = pair.GetPrototypesWithComponent(); + var protoMan = server.ProtoMan; await server.WaitAssertion(() => { - Assert.Multiple(() => + using (Assert.EnterMultipleScope()) { - foreach (var (proto, comp) in protos) - { - Assert.That(comp.AnimationThreshold, Is.LessThan(comp.CritThreshold), - $"Animation threshold on {proto.ID} must be less than its crit threshold."); - } - }); - }); + var proto = protoMan.Index(protoKey); + var comp = (StaminaComponent)proto.Components["Stamina"].Component; - await pair.CleanReturnAsync(); + Assert.That(comp.AnimationThreshold, + Is.LessThan(comp.CritThreshold), + $"Animation threshold on {proto.ID} must be less than its crit threshold."); + + // TODO(Kaylie): "value is in range" serializer. Needs some serializationmanager improvements. + Assert.That(comp.Decay, Is.Positive.Or.Zero, "Negative decay results in nonsensical behavior."); + Assert.That(comp.Cooldown, Is.Positive.Or.Zero, "Negative cooldown results in nonsensical behavior"); + Assert.That(comp.BaseCritThreshold, Is.Positive); + Assert.That(comp.CritThreshold, Is.Positive); + Assert.That(comp.AfterCritDecayMultiplier, Is.Positive); + Assert.That(comp.ForceStandStamina, Is.Positive); + + // NUnit's analyzer is defective here. Cool. +#pragma warning disable NUnit2041 + Assert.That(comp.StunModifierThresholds.Keys, + Has.All.GreaterThanOrEqualTo(FixedPoint2.Zero).And.LessThanOrEqualTo(FixedPoint2.New(1.0f)), + "The stun thresholds are percentages and should be in the [0, 1.0] range."); +#pragma warning restore NUnit2041 + } + }); } } diff --git a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs index 49d54bbecfc..bbaf11a0524 100644 --- a/Content.IntegrationTests/Tests/DeleteInventoryTest.cs +++ b/Content.IntegrationTests/Tests/DeleteInventoryTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Inventory; @@ -7,14 +8,14 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class DeleteInventoryTest + public sealed class DeleteInventoryTest : GameTest { // Test that when deleting an entity with an InventoryComponent, // any equipped items also get deleted. [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var entMgr = server.ResolveDependency(); @@ -44,7 +45,6 @@ await server.WaitAssertion(() => // Assert that child item was also deleted. Assert.That(item.Deleted, Is.True); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs index 0da07ad5a16..a83891b8fcc 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs @@ -1,10 +1,11 @@ -using Content.Server.Destructible.Thresholds.Triggers; +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible.Thresholds.Triggers; using Content.Shared.FixedPoint; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -13,12 +14,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DamageGroupTrigger))] [TestOf(typeof(AndTrigger))] - public sealed class DestructibleDamageGroupTest + public sealed class DestructibleDamageGroupTest : GameTest { [Test] public async Task AndTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -91,7 +92,7 @@ await server.WaitAssertion(() => Assert.That(threshold.Trigger, Is.InstanceOf()); }); - var trigger = (AndTrigger) threshold.Trigger; + var trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -117,7 +118,7 @@ await server.WaitAssertion(() => sDamageableSystem.TryChangeDamage(sDestructibleEntity, bruteDamage * -10); Assert.Multiple(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(20))); + Assert.That(sDamageableSystem.GetTotalDamage(sDestructibleEntity), Is.EqualTo(FixedPoint2.New(20))); // No new thresholds reached, healing should not trigger it Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); @@ -132,7 +133,7 @@ await server.WaitAssertion(() => sTestThresholdListenerSystem.ThresholdsReached.Clear(); // Heal both classes of damage to 0 - sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0); + sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent)); // No new thresholds reached, healing should not trigger it Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); @@ -162,7 +163,7 @@ await server.WaitAssertion(() => Assert.That(threshold.Trigger, Is.InstanceOf()); }); - trigger = (AndTrigger) threshold.Trigger; + trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -176,7 +177,7 @@ await server.WaitAssertion(() => threshold.TriggersOnce = true; // Heal brute and burn back to 0 - sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0); + sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent)); // No new thresholds reached from healing Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); @@ -193,7 +194,6 @@ await server.WaitAssertion(() => // No new thresholds reached as triggers once is set to true and it already triggered before Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs index ccd459668b2..47f44ad4969 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs @@ -1,6 +1,9 @@ -using Content.Server.Destructible.Thresholds.Triggers; +using Content.IntegrationTests.Fixtures; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible.Thresholds.Triggers; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -10,12 +13,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DamageTypeTrigger))] [TestOf(typeof(AndTrigger))] - public sealed class DestructibleDamageTypeTest + public sealed class DestructibleDamageTypeTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -86,7 +89,7 @@ await server.WaitAssertion(() => Assert.That(threshold.Trigger, Is.InstanceOf()); }); - var trigger = (AndTrigger) threshold.Trigger; + var trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -154,7 +157,7 @@ await server.WaitAssertion(() => Assert.That(threshold.Trigger, Is.InstanceOf()); }); - trigger = (AndTrigger) threshold.Trigger; + trigger = (AndTrigger)threshold.Trigger; Assert.Multiple(() => { @@ -186,7 +189,6 @@ await server.WaitAssertion(() => // No new thresholds reached as triggers once is set to true and it already triggered before Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs index 4e169e7dfa5..0aa0449682d 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs @@ -1,8 +1,9 @@ using System.Linq; -using Content.Server.Destructible.Thresholds; +using Content.IntegrationTests.Fixtures; using Content.Server.Destructible.Thresholds.Behaviors; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.Destructible.Thresholds; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -10,12 +11,12 @@ namespace Content.IntegrationTests.Tests.Destructible { - public sealed class DestructibleDestructionTest + public sealed class DestructibleDestructionTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -59,7 +60,7 @@ await server.WaitAssertion(() => Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); }); - var spawnEntitiesBehavior = (SpawnEntitiesBehavior) threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior); + var spawnEntitiesBehavior = (SpawnEntitiesBehavior)threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior); Assert.Multiple(() => { @@ -89,7 +90,6 @@ await server.WaitAssertion(() => Assert.That(found, Is.True, $"Unable to find {SpawnedEntityId} nearby for destructible test; found {entitiesInRange.Count} entities."); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs index af86e406efd..da56a43a4ac 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs @@ -1,13 +1,16 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Destructible; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Content.Shared.Destructible; +using Content.Shared.Destructible.Thresholds.Triggers; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; @@ -17,12 +20,12 @@ namespace Content.IntegrationTests.Tests.Destructible [TestFixture] [TestOf(typeof(DestructibleComponent))] [TestOf(typeof(DamageThreshold))] - public sealed class DestructibleThresholdActivationTest + public sealed class DestructibleThresholdActivationTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -99,9 +102,9 @@ await server.WaitAssertion(() => // Check that it matches the YAML prototype Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - var soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - var spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - var actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + var soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + var spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + var actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; Assert.Multiple(() => { @@ -124,7 +127,7 @@ await server.WaitAssertion(() => Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); // Set damage to 0 - sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0); + sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent)); // Damage for 100, up to 100 sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 10, true); @@ -164,9 +167,9 @@ await server.WaitAssertion(() => // Check that it matches the YAML prototype Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; // Check that it matches the YAML prototype Assert.Multiple(() => @@ -185,7 +188,7 @@ await server.WaitAssertion(() => sTestThresholdListenerSystem.ThresholdsReached.Clear(); // Heal all damage - sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0); + sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent)); // Damage up to 50 sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true); @@ -193,7 +196,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { // Check that the total damage matches - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50))); + Assert.That(sDamageableSystem.GetTotalDamage(sDestructibleEntity), Is.EqualTo(FixedPoint2.New(50))); // Both thresholds should have triggered Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Has.Exactly(2).Items); @@ -201,11 +204,11 @@ await server.WaitAssertion(() => // Verify the first one, should be the lowest one (20) msg = sTestThresholdListenerSystem.ThresholdsReached[0]; - var trigger = (DamageTrigger) msg.Threshold.Trigger; + var trigger = (DamageTrigger)msg.Threshold.Trigger; Assert.Multiple(() => { Assert.That(trigger, Is.Not.Null); - Assert.That(trigger.Damage, Is.EqualTo(20)); + Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(20))); }); threshold = msg.Threshold; @@ -215,20 +218,20 @@ await server.WaitAssertion(() => // Verify the second one, should be the highest one (50) msg = sTestThresholdListenerSystem.ThresholdsReached[1]; - trigger = (DamageTrigger) msg.Threshold.Trigger; + trigger = (DamageTrigger)msg.Threshold.Trigger; Assert.Multiple(() => { Assert.That(trigger, Is.Not.Null); - Assert.That(trigger.Damage, Is.EqualTo(50)); + Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(50))); }); threshold = msg.Threshold; Assert.That(threshold.Behaviors, Has.Count.EqualTo(3)); - soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0]; - spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1]; - actsThreshold = (DoActsBehavior) threshold.Behaviors[2]; + soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0]; + spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1]; + actsThreshold = (DoActsBehavior)threshold.Behaviors[2]; // Check that it matches the YAML prototype Assert.Multiple(() => @@ -247,10 +250,10 @@ await server.WaitAssertion(() => sTestThresholdListenerSystem.ThresholdsReached.Clear(); // Heal the entity completely - sDamageableSystem.SetAllDamage(sDestructibleEntity, sDamageableComponent, 0); + sDamageableSystem.ClearAllDamage((sDestructibleEntity, sDamageableComponent)); // Check that the entity has 0 damage - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + Assert.That(sDamageableSystem.GetTotalDamage(sDestructibleEntity), Is.EqualTo(FixedPoint2.Zero)); // Set both thresholds to only trigger once foreach (var destructibleThreshold in sDestructibleComponent.Thresholds) @@ -265,7 +268,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { // Check that the total damage matches - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50))); + Assert.That(sDamageableSystem.GetTotalDamage(sDestructibleEntity), Is.EqualTo(FixedPoint2.New(50))); // No thresholds should have triggered as they were already triggered before, and they are set to only trigger once Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); @@ -281,13 +284,12 @@ await server.WaitAssertion(() => Assert.Multiple(() => { // Check that the total damage matches - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50))); + Assert.That(sDamageableSystem.GetTotalDamage(sDestructibleEntity), Is.EqualTo(FixedPoint2.New(50))); // They shouldn't have been triggered by changing TriggersOnce Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs index 6ce6d5d78e0..7a2a880bae5 100644 --- a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs +++ b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; -using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Server.DeviceLinking.Systems; using Content.Shared.DeviceLinking; -using Content.Shared.Prototypes; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.DeviceLinking; -public sealed class DeviceLinkingTest +public sealed class DeviceLinkingTest : GameTest { private const string PortTesterProtoId = "DeviceLinkingSinkPortTester"; @@ -24,71 +22,61 @@ public sealed class DeviceLinkingTest - Output "; - /// - /// Spawns every entity that has a - /// and sends a signal to every port to make sure nothing causes an error. - /// + private static string[] _entitiesWithDeviceLinkSink = GameDataScrounger.EntitiesWithComponent("DeviceLinkSink"); + [Test] - public async Task AllDeviceLinkSinksWorkTest() + [TestOf(typeof(DeviceLinkSinkComponent))] + [TestCaseSource(nameof(_entitiesWithDeviceLinkSink))] + [Description("Ensures all devices that can sink signals will not cause exceptions when signaled.")] + public async Task DeviceLinkSinkAllPortsTest(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; + var protoMan = server.ProtoMan; var compFact = server.ResolveDependency(); var mapMan = server.ResolveDependency(); var mapSys = server.System(); var deviceLinkSys = server.System(); - var prototypes = server.ProtoMan.EnumeratePrototypes(); - await server.WaitAssertion(() => { - Assert.Multiple(() => + using (Assert.EnterMultipleScope()) { - foreach (var proto in prototypes) - { - if (proto.Abstract || pair.IsTestPrototype(proto)) - continue; + var proto = protoMan.Index(protoKey); + Assert.That(proto.TryGetComponent(out var protoSinkComp, compFact)); - if (!proto.TryGetComponent(out var protoSinkComp, compFact)) - continue; - - foreach (var port in protoSinkComp.Ports) - { - // Create a map for each entity/port combo so they can't interfere - mapSys.CreateMap(out var mapId); - var grid = mapMan.CreateGridEntity(mapId); - mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1)); - var coord = new EntityCoordinates(grid.Owner, 0, 0); + foreach (var port in protoSinkComp!.Ports) + { + // Create a map for each entity/port combo so they can't interfere + mapSys.CreateMap(out var mapId); + var grid = mapMan.CreateGridEntity(mapId); + mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1)); + var coord = new EntityCoordinates(grid.Owner, 0, 0); - // Spawn the sink entity - var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord); - // Get the actual sink component, since the one we got from the prototype doesn't have its owner set up - Assert.That(server.EntMan.TryGetComponent(sinkEnt, out var sinkComp), - $"{proto.ID} does not have a DeviceLinkSinkComponent!"); + // Spawn the sink entity + var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord); + // Get the actual sink component, since the one we got from the prototype isn't initialized. + var sinkComp = server.EntMan.GetComponent(sinkEnt); - // Spawn the tester - var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord); - Assert.That(server.EntMan.TryGetComponent(sourceEnt, out var sourceComp), - $"Tester prototype does not have a DeviceLinkSourceComponent!"); + // Spawn the tester + var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord); + var sourceComp = server.EntMan.GetComponent(sourceEnt); - // Create a link from the tester's output to the target port on the sink - deviceLinkSys.SaveLinks(null, - sourceEnt, - sinkEnt, - [("Output", port.Id)], - sourceComp, - sinkComp); + // Create a link from the tester's output to the target port on the sink + deviceLinkSys.SaveLinks(null, + sourceEnt, + sinkEnt, + [("Output", port.Id)], + sourceComp, + sinkComp); - // Send a signal to the port - Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); }, - $"Exception thrown while triggering port {port.Id} of sink device {proto.ID}"); + // Send a signal to the port + Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); }, + $"Exception thrown while triggering port {port.Id} of the sink device."); - mapSys.DeleteMap(mapId); - } + mapSys.DeleteMap(mapId); } - }); + } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs index fdc0e1a4d4a..6ecdffc58c9 100644 --- a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs +++ b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Shared.DeviceNetwork; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.DeviceNetwork [TestOf(typeof(DeviceNetworkComponent))] [TestOf(typeof(WiredNetworkComponent))] [TestOf(typeof(WirelessNetworkComponent))] - public sealed class DeviceNetworkTest + public sealed class DeviceNetworkTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -50,7 +51,7 @@ public sealed class DeviceNetworkTest [Test] public async Task NetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -104,13 +105,12 @@ await server.WaitAssertion(() => { Assert.That(payload, Is.EquivalentTo(deviceNetTestSystem.LastPayload)); }); - await pair.CleanReturnAsync(); } [Test] public async Task WirelessNetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -188,14 +188,12 @@ await server.WaitAssertion(() => { Assert.That(payload, Is.Not.EqualTo(deviceNetTestSystem.LastPayload).AsCollection); }); - - await pair.CleanReturnAsync(); } [Test] public async Task WiredNetworkDeviceSendAndReceive() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -271,8 +269,6 @@ await server.WaitAssertion(() => { Assert.That(payload, Is.EqualTo(deviceNetTestSystem.LastPayload).AsCollection); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs index 52b669b09da..f7b20819c24 100644 --- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs +++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs @@ -1,6 +1,7 @@ #nullable enable annotations using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Disposal.Unit; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Disposal [TestOf(typeof(DisposalHolderComponent))] [TestOf(typeof(DisposalEntryComponent))] [TestOf(typeof(DisposalUnitComponent))] - public sealed class DisposalUnitTest + public sealed class DisposalUnitTest : GameTest { [Reflect(false)] private sealed class DisposalUnitTestSystem : EntitySystem @@ -145,7 +146,7 @@ private static void Flush(EntityUid unitEntity, DisposalUnitComponent unit, bool [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -240,8 +241,6 @@ await server.WaitAssertion(() => // Re-pressurizing Flush(disposalUnit, unitComponent, false, disposalSystem); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs index 45c384f86c7..d9619076e0f 100644 --- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs +++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.DoAfter; +using Content.Shared.Interaction; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Reflection; @@ -10,7 +13,7 @@ namespace Content.IntegrationTests.Tests.DoAfter { [TestFixture] [TestOf(typeof(DoAfterComponent))] - public sealed partial class DoAfterServerTest + public sealed partial class DoAfterServerTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -33,7 +36,7 @@ public override DoAfterEvent Clone() [Test] public async Task TestSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); var refMan = server.ResolveDependency(); @@ -53,28 +56,25 @@ await server.WaitPost(() => } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFinished() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); - var entityManager = server.ResolveDependency(); + var entityManager = server.EntMan; var timing = server.ResolveDependency(); - var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem(); + var doAfterSystem = entityManager.System(); 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, tickTime / 2, ev, null) { Broadcast = true }; + var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true }; #pragma warning disable NUnit2045 // Interdependent assertions. Assert.That(doAfterSystem.TryStartDoAfter(args)); Assert.That(ev.Cancelled, Is.False); @@ -83,32 +83,24 @@ await server.WaitPost(() => await server.WaitRunTicks(1); Assert.That(ev.Cancelled, Is.False); - - await pair.CleanReturnAsync(); } [Test] public async Task TestCancelled() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; - var entityManager = server.ResolveDependency(); + var entityManager = server.EntMan; var timing = server.ResolveDependency(); - var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem(); + var doAfterSystem = entityManager.System(); var ev = new TestDoAfterEvent(); await server.WaitPost(() => { - var tickTime = 1.0f / timing.TickRate; - var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace); - var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true }; + var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true }; - if (!doAfterSystem.TryStartDoAfter(args, out var id)) - { - Assert.Fail(); - return; - } + Assert.That(doAfterSystem.TryStartDoAfter(args, out var id)); Assert.That(!ev.Cancelled); doAfterSystem.Cancel(id); @@ -118,8 +110,66 @@ await server.WaitPost(() => await server.WaitRunTicks(3); Assert.That(ev.Cancelled); + } + + /// + /// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result + /// includes the correct interacting entities. + /// + [Test] + public async Task TestGetInteractingEntities() + { + var pair = Pair; + var server = pair.Server; + var entityManager = server.EntMan; + var timing = server.ResolveDependency(); + var doAfterSystem = entityManager.System(); + var interactionSystem = entityManager.System(); + var ev = new TestDoAfterEvent(); + + EntityUid mob = default; + EntityUid target = default; + + EntityUid mob2 = default; + EntityUid mob3 = default; + EntityUid target2 = default; - await pair.CleanReturnAsync(); + 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(); + 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); + }); } } } diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs index 69fe66039b8..af03de169f5 100644 --- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs +++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Doors.Systems; using Content.Shared.Doors.Components; using Robust.Shared.GameObjects; @@ -11,7 +12,7 @@ namespace Content.IntegrationTests.Tests.Doors { [TestFixture] [TestOf(typeof(AirlockComponent))] - public sealed class AirlockTest + public sealed class AirlockTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -54,7 +55,7 @@ public sealed class AirlockTest [Test] public async Task OpenCloseDestroyTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -104,16 +105,12 @@ await server.WaitAssertion(() => entityManager.DeleteEntity(airlock); }); }); - - server.RunTicks(5); - - await pair.CleanReturnAsync(); } [Test] public async Task AirlockBlockTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -179,7 +176,6 @@ await server.WaitAssertion(() => { Assert.That(Math.Abs(xformSystem.GetWorldPosition(airlockPhysicsDummy).X - 1), Is.GreaterThan(0.01f)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/DummyIconTest.cs b/Content.IntegrationTests/Tests/DummyIconTest.cs index 62197bb3197..dcb5d315dcf 100644 --- a/Content.IntegrationTests/Tests/DummyIconTest.cs +++ b/Content.IntegrationTests/Tests/DummyIconTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Client.ResourceManagement; using Robust.Shared.Prototypes; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class DummyIconTest + public sealed class DummyIconTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; var prototypeManager = client.ResolveDependency(); var resourceCache = client.ResolveDependency(); @@ -32,7 +33,6 @@ await client.WaitAssertion(() => proto.ID); } }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs index f9db0641636..3a72a195fdc 100644 --- a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs +++ b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs @@ -132,7 +132,7 @@ public async Task TestDeleteWhileEmbedded() "Target has unexpected EmbeddedObjects count."); // Wait for the cooldown between throws - await RunSeconds(Hands.ThrowCooldown.Seconds); + await RunSeconds(Hands!.ThrowCooldown.Seconds); // Throw the second projectile await ThrowItem(); diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 9b0e7729f5b..13293b7d465 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Numerics; using System.Text; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Robust.Shared; using Robust.Shared.Audio.Components; using Robust.Shared.Configuration; @@ -16,17 +18,28 @@ namespace Content.IntegrationTests.Tests { [TestFixture] [TestOf(typeof(EntityUid))] - public sealed class EntityTest + public sealed class EntityTest : GameTest { private static readonly ProtoId SpawnerCategory = "Spawner"; + public override PoolSettings PoolSettings => new() + { + Connected = true, + Dirty = true + }; + + public static PoolSettings Disconnected => new() + { + Dirty = true, + }; + [Test] + [PairConfig(nameof(Disconnected))] public async Task SpawnAndDeleteAllEntitiesOnDifferentMaps() { // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round // is minimal relative to the rest of the test. - var settings = new PoolSettings { Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var entityMan = server.ResolveDependency(); @@ -79,17 +92,14 @@ await server.WaitPost(() => Assert.That(entityMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } [Test] + [PairConfig(nameof(Disconnected))] public async Task SpawnAndDeleteAllEntitiesInTheSameSpot() { - // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round - // is minimal relative to the rest of the test. - var settings = new PoolSettings { Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; + Assert.That(pair.Client.Session, Is.Null); var server = pair.Server; var map = await pair.CreateTestMap(); @@ -134,8 +144,6 @@ await server.WaitPost(() => Assert.That(entityMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } /// @@ -145,10 +153,7 @@ await server.WaitPost(() => [Test] public async Task SpawnAndDirtyAllEntities() { - // This test dirties the pair as it simply deletes ALL entities when done. Overhead of restarting the round - // is minimal relative to the rest of the test. - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -182,7 +187,7 @@ await server.WaitPost(() => } }); - await pair.RunTicksSync(15); + await pair.RunUntilSynced(); // Make sure the client actually received the entities // 500 is completely arbitrary. Note that the client & sever entity counts aren't expected to match. @@ -209,8 +214,6 @@ await server.WaitPost(() => Assert.That(sEntMan.EntityCount, Is.Zero); }); - - await pair.CleanReturnAsync(); } /// @@ -230,8 +233,7 @@ await server.WaitPost(() => [Test] public async Task SpawnAndDeleteEntityCountTest() { - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var mapSys = pair.Server.System(); var server = pair.Server; var client = pair.Client; @@ -317,8 +319,6 @@ await Assert.MultipleAsync(async () => BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan)); } }); - - await pair.CleanReturnAsync(); } private static string BuildDiffString(IEnumerable oldEnts, IEnumerable newEnts, IEntityManager entMan) @@ -385,14 +385,11 @@ public async Task AllComponentsOneToOneDeleteTest() "StationData", // errors when removed mid-round "StationJobs", "Actor", // We aren't testing actor components, those need their player session set. - "BlobFloorPlanBuilder", // Implodes if unconfigured. - "DebrisFeaturePlacerController", // Above. - "LoadedChunk", // Worldgen chunk loading malding. "BiomeSelection", // Whaddya know, requires config. "ActivatableUI", // Requires enum key }; - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); var componentFactory = server.ResolveDependency(); @@ -445,8 +442,6 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs b/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs index 54ff61c0c4b..7a486e68394 100644 --- a/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Explosion/ExplosionPrototypeTest.cs @@ -1,28 +1,45 @@ +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Utility; using Content.Shared.Explosion; namespace Content.IntegrationTests.Tests.Explosion; -public sealed class ExplosionPrototypeTest +public sealed class ExplosionPrototypeTest : GameTest { + private static string[] _explosionKinds = GameDataScrounger.PrototypesOfKind(); + [Test] - public async Task ValidateExplosionPrototypes() + [TestOf(typeof(ExplosionPrototype))] + [TestCaseSource(nameof(_explosionKinds))] + [Description("Ensures various properties of ExplosionPrototype are correctly configured.")] + public async Task Validate(string protoKey) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; - var entMan = server.EntMan; var protoMan = server.ProtoMan; - var protos = protoMan.EnumeratePrototypes(); + var proto = protoMan.Index(protoKey); - Assert.Multiple(() => + using (Assert.EnterMultipleScope()) { - foreach (var proto in protos) - { - Assert.That(proto._tileBreakChance, Is.Not.Empty, $"Empty tile break chance definitions for explosion prototype: {proto.ID}"); - Assert.That(proto._tileBreakChance, Has.Length.EqualTo(proto._tileBreakIntensity.Length), $"Malformed tile break chance definitions for explosion prototype: {proto.ID}"); - } - }); + Assert.That(proto._tileBreakChance, Is.Not.Empty, $"Tile break chances cannot be empty."); + // this diagnostic is broken. +#pragma warning disable NUnit2041 + Assert.That(proto._tileBreakChance, + Has.All.GreaterThanOrEqualTo(0.0f).And.LessThanOrEqualTo(1.0f), + "Tile break chances are probabilities and must be in the range [0, 1]"); +#pragma warning restore NUnit2041 + Assert.That(proto._tileBreakChance, + Has.Length.EqualTo(proto._tileBreakIntensity.Length), + $"Tile break chances don't match the tile break intensities."); - await pair.CleanReturnAsync(); + Assert.That(proto.FireStacks, Is.Null.Or.Positive); + Assert.That(proto.Temperature, Is.Null.Or.Positive); + Assert.That(proto.TileBreakRerollReduction, Is.Positive.Or.Zero); + Assert.That(proto.SmallSoundIterationThreshold, Is.Positive.Or.Zero); + Assert.That(proto.MaxCombineDistance, Is.Positive.Or.Zero); + Assert.That(proto.IntensityPerState, Is.Positive); + Assert.That(proto.FireStates, Is.Positive); + } } } diff --git a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs index 37e777fa8cf..142ec21ddbe 100644 --- a/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs +++ b/Content.IntegrationTests/Tests/FillLevelSpriteTest.cs @@ -1,5 +1,8 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; +using Content.Shared.Prototypes; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -10,17 +13,20 @@ namespace Content.IntegrationTests.Tests; /// Tests to see if any entity prototypes specify solution fill level sprites that don't exist. /// [TestFixture] -public sealed class FillLevelSpriteTest +public sealed class FillLevelSpriteTest : GameTest { private static readonly string[] HandStateNames = ["left", "right"]; + private static readonly string[] EquipStateNames = ["back", "suitstorage"]; [Test] public async Task FillLevelSpritesExist() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; var protoMan = client.ResolveDependency(); var componentFactory = client.ResolveDependency(); + var entMan = client.ResolveDependency(); + var spriteSystem = client.System(); await client.WaitAssertion(() => { @@ -31,41 +37,70 @@ await client.WaitAssertion(() => .OrderBy(p => p.ID) .ToList(); - foreach (var proto in protos) + Assert.Multiple(() => { - Assert.That(proto.TryGetComponent(out var visuals, componentFactory)); - Assert.That(proto.TryGetComponent(out var sprite, componentFactory)); - - var rsi = sprite.BaseRSI; - - // Test base sprite fills - if (!string.IsNullOrEmpty(visuals.FillBaseName)) + foreach (var proto in protos) { - for (var i = 1; i <= visuals.MaxFillLevels; i++) + Assert.That(proto.TryGetComponent(out var visuals, componentFactory)); + Assert.That(proto.TryGetComponent(out var sprite, componentFactory)); + if (!proto.HasComponent(componentFactory)) { - var state = $"{visuals.FillBaseName}{i}"; - Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with - MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!"); + Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no AppearanceComponent."); } - } - // Test inhand sprite fills - if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName)) - { - for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++) + // Test base sprite fills + if (!string.IsNullOrEmpty(visuals.FillBaseName) && visuals.MaxFillLevels > 0) { - foreach (var handname in HandStateNames) + var entity = entMan.Spawn(proto.ID); + if (!spriteSystem.LayerMapTryGet(entity, SolutionContainerLayers.Fill, out var fillLayerId, false)) + { + Assert.Fail(@$"{proto.ID} has SolutionContainerVisualsComponent but no fill layer map."); + } + if (!spriteSystem.TryGetLayer(entity, fillLayerId, out var fillLayer, false)) { - var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}"; + Assert.Fail(@$"{proto.ID} somehow lost a layer."); + } + var rsi = fillLayer.ActualRsi; + + for (var i = 1; i <= visuals.MaxFillLevels; i++) + { + var state = $"{visuals.FillBaseName}{i}"; Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with - InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!"); + MaxFillLevels = {visuals.MaxFillLevels}, but {rsi.Path} doesn't have state {state}!"); } + } + // Test inhand sprite fills + if (!string.IsNullOrEmpty(visuals.InHandsFillBaseName) && visuals.InHandsMaxFillLevels > 0) + { + var rsi = sprite.BaseRSI; + for (var i = 1; i <= visuals.InHandsMaxFillLevels; i++) + { + foreach (var handname in HandStateNames) + { + var state = $"inhand-{handname}{visuals.InHandsFillBaseName}{i}"; + Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with + InHandsMaxFillLevels = {visuals.InHandsMaxFillLevels}, but {rsi.Path} doesn't have state {state}!"); + } + } + } + + // Test equipped sprite fills + if (!string.IsNullOrEmpty(visuals.EquippedFillBaseName) && visuals.EquippedMaxFillLevels > 0) + { + var rsi = sprite.BaseRSI; + for (var i = 1; i <= visuals.EquippedMaxFillLevels; i++) + { + foreach (var equipName in EquipStateNames) + { + var state = $"equipped-{equipName}{visuals.EquippedFillBaseName}{i}"; + Assert.That(rsi.TryGetState(state, out _), @$"{proto.ID} has SolutionContainerVisualsComponent with + EquippedMaxFillLevels = {visuals.EquippedMaxFillLevels}, but {rsi.Path} doesn't have state {state}!"); + } + } } } - } + }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs index 1afed38966f..3d9ff61a2d3 100644 --- a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs @@ -7,12 +7,13 @@ using Robust.Shared.Prototypes; using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; namespace Content.IntegrationTests.Tests.Fluids; [TestFixture] [TestOf(typeof(AbsorbentComponent))] -public sealed class AbsorbentTest +public sealed class AbsorbentTest : GameTest { private const string UserDummyId = "UserDummy"; private const string AbsorbentDummyId = "AbsorbentDummy"; @@ -73,7 +74,7 @@ public record TestSolutionCase( [TestCaseSource(nameof(TestCasesToRun))] public async Task AbsorbentOnRefillableTest(TestSolutionCase testCase) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -123,15 +124,12 @@ await server.WaitAssertion(() => Assert.That(VolumeOfPrototypeInComposition(refillableComposition, NonEvaporablePrototypeId), Is.EqualTo(testCase.ExpectedRefillableSolution.VolumeOfNonEvaporable)); }); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [TestCaseSource(nameof(TestCasesToRunOnSmallRefillable))] public async Task AbsorbentOnSmallRefillableTest(TestSolutionCase testCase) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -180,9 +178,6 @@ await server.WaitAssertion(() => Assert.That(VolumeOfPrototypeInComposition(refillableComposition, NonEvaporablePrototypeId), Is.EqualTo(testCase.ExpectedRefillableSolution.VolumeOfNonEvaporable)); }); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } private static FixedPoint2 VolumeOfPrototypeInComposition(Dictionary composition, string prototypeId) diff --git a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs index d6f9bf35986..c073020e9c5 100644 --- a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Fluids.EntitySystems; using Content.Server.Spreader; using Content.Shared.Chemistry.Components; @@ -14,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Fluids; [TestFixture] [TestOf(typeof(SpreaderSystem))] -public sealed class FluidSpill +public sealed class FluidSpill : GameTest { private static PuddleComponent? GetPuddle(IEntityManager entityManager, Entity mapGrid, Vector2i pos) { @@ -36,7 +37,7 @@ public sealed class FluidSpill [Test] public async Task SpillCorner() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -110,7 +111,5 @@ await server.WaitAssertion(() => } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs index ee2d0cb1f7a..71fbefa241f 100644 --- a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Fluids.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Coordinates; @@ -10,12 +11,12 @@ namespace Content.IntegrationTests.Tests.Fluids { [TestFixture] [TestOf(typeof(PuddleComponent))] - public sealed class PuddleTest + public sealed class PuddleTest : GameTest { [Test] public async Task TilePuddleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -32,15 +33,12 @@ await server.WaitAssertion(() => Assert.That(spillSystem.TrySpillAt(coordinates, solution, out _), Is.True); }); - await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [Test] public async Task SpaceNoPuddleTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -69,8 +67,6 @@ await server.WaitAssertion(() => Assert.That(spillSystem.TrySpillAt(coordinates, solution, out _), Is.False); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/FollowerSystemTest.cs b/Content.IntegrationTests/Tests/FollowerSystemTest.cs index f4447426c77..464b3306f20 100644 --- a/Content.IntegrationTests/Tests/FollowerSystemTest.cs +++ b/Content.IntegrationTests/Tests/FollowerSystemTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.Follower; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests; [TestFixture, TestOf(typeof(FollowerSystem))] -public sealed class FollowerSystemTest +public sealed class FollowerSystemTest : GameTest { /// /// This test ensures that deleting a map while an entity follows another doesn't throw any exceptions. @@ -15,7 +16,7 @@ public sealed class FollowerSystemTest [Test] public async Task FollowerMapDeleteTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -44,6 +45,5 @@ await server.WaitPost(() => entMan.DeleteEntity(mapSys.GetMap(map)); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index dae3203f9ff..97681ad4695 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Cuffs; using Content.Shared.Cuffs.Components; using Content.Shared.Hands.Components; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking [TestFixture] [TestOf(typeof(CuffableComponent))] [TestOf(typeof(HandcuffComponent))] - public sealed class HandCuffTest + public sealed class HandCuffTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -40,7 +41,7 @@ public sealed class HandCuffTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; EntityUid human; @@ -98,8 +99,6 @@ await server.WaitAssertion(() => cuffableSys.TryAddNewCuffs(human, human, secondCuffs, cuffed); Assert.That(cuffed.CuffedHandCount, Is.EqualTo(4), "Player doesn't have correct amount of hands cuffed"); }); - - await pair.CleanReturnAsync(); } private static void AddHand(NetEntity to, IServerConsoleHost host) diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs index 5c732455011..3473f1bc9c2 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/EntityPrototypeComponentsTest.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using Content.IntegrationTests.Fixtures; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.Utility; @@ -11,12 +12,12 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components { [TestFixture] [TestOf(typeof(Server.Entry.IgnoredComponents))] - public sealed class EntityPrototypeComponentsTest + public sealed class EntityPrototypeComponentsTest : GameTest { [Test] public async Task PrototypesHaveKnownComponents() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -26,7 +27,7 @@ public async Task PrototypesHaveKnownComponents() .ToList() .AsParallel() .Where(filePath => filePath.Extension == "yml" && - !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) + !filePath.Filename.StartsWith('.')) .ToArray(); var cComponentFactory = client.ResolveDependency(); @@ -34,6 +35,7 @@ public async Task PrototypesHaveKnownComponents() var unknownComponentsClient = new List<(string entityId, string component)>(); var unknownComponentsServer = new List<(string entityId, string component)>(); + var doubleIgnoredComponents = new List<(string entityId, string component)>(); var entitiesValidated = 0; var componentsValidated = 0; @@ -72,28 +74,33 @@ public async Task PrototypesHaveKnownComponents() var componentType = component.GetNode("type").AsString(); var clientAvailability = cComponentFactory.GetComponentAvailability(componentType); + var serverAvailability = sComponentFactory.GetComponentAvailability(componentType); - if (clientAvailability == ComponentAvailability.Unknown) + var entityId = node.GetNode("id").AsString(); + + if ((clientAvailability, serverAvailability) is + (ComponentAvailability.Ignore, ComponentAvailability.Ignore)) { - var entityId = node.GetNode("id").AsString(); - unknownComponentsClient.Add((entityId, componentType)); + doubleIgnoredComponents.Add((entityId, componentType)); + continue; } - var serverAvailability = sComponentFactory.GetComponentAvailability(componentType); + // NOTE: currently, the client's component factory is configured to ignore /all/ + // non-registered components, meaning this case will never succeed. This is here + // mainly for future proofing plus any downstreams that were brave enough to not + // ignore all unknown components on clientside. + if (clientAvailability == ComponentAvailability.Unknown) + unknownComponentsClient.Add((entityId, componentType)); if (serverAvailability == ComponentAvailability.Unknown) - { - var entityId = node.GetNode("id").AsString(); unknownComponentsServer.Add((entityId, componentType)); - } } } } } - if (unknownComponentsClient.Count + unknownComponentsServer.Count == 0) + if (unknownComponentsClient.Count + unknownComponentsServer.Count + doubleIgnoredComponents.Count == 0) { - await pair.CleanReturnAsync(); Assert.Pass($"Validated {entitiesValidated} entities with {componentsValidated} components in {paths.Length} files."); return; } @@ -112,17 +119,23 @@ public async Task PrototypesHaveKnownComponents() $"SERVER: Unknown component {component} in prototype {entityId}\n"); } + foreach (var (entityId, component) in doubleIgnoredComponents) + { + message.Append( + $"Component {component} in prototype {entityId} is ignored by both client and serverV\n"); + } + Assert.Fail(message.ToString()); } [Test] public async Task IgnoredComponentsExistInTheCorrectPlaces() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var client = pair.Client; var serverComponents = server.ResolveDependency(); - var ignoredServerNames = Server.Entry.IgnoredComponents.List; + var ignoredServerNames = Content.Server.Entry.IgnoredComponents.List; var clientComponents = client.ResolveDependency(); var failureMessages = ""; @@ -138,7 +151,6 @@ public async Task IgnoredComponentsExistInTheCorrectPlaces() } } Assert.That(failureMessages, Is.Empty); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs index 6c103bf6aef..71c37076eec 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Client.UserInterface.Systems.Alerts.Controls; using Content.Client.UserInterface.Systems.Alerts.Widgets; +using Content.IntegrationTests.Fixtures; using Content.Shared.Alert; using Robust.Client.UserInterface; using Robust.Server.Player; @@ -10,16 +11,18 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs { [TestFixture] [TestOf(typeof(AlertsComponent))] - public sealed class AlertsComponentTests + public sealed class AlertsComponentTests : GameTest { + public override PoolSettings PoolSettings => new() + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task AlertsTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -107,8 +110,6 @@ await client.WaitAssertion(() => var expectedIDs = new[] { "HumanHealth", "Debug2" }; Assert.That(alertIDs, Is.SupersetOf(expectedIDs)); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs index b215584c57a..4a059eb1cca 100644 --- a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Antag; using Content.Server.Antag.Components; using Content.Server.GameTicking; @@ -14,17 +15,19 @@ namespace Content.IntegrationTests.Tests.GameRules; // Once upon a time, players in the lobby weren't ever considered eligible for antag roles. // Lets not let that happen again. [TestFixture] -public sealed class AntagPreferenceTest +public sealed class AntagPreferenceTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + InLobby = true + }; + [Test] public async Task TestLobbyPlayersValid() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -71,6 +74,5 @@ public async Task TestLobbyPlayersValid() Assert.That(pool.Count, Is.EqualTo(0)); await server.WaitPost(() => server.EntMan.DeleteEntity(uid)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs index b9a02339fba..b8efb64b8bc 100644 --- a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; using Content.Shared.CCVar; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class FailAndStartPresetTest +public sealed class FailAndStartPresetTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -52,19 +53,21 @@ public sealed class FailAndStartPresetTest - type: TestRule "; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }; + /// /// Test that a nuke ops gamemode can start after failing to start once. /// [Test] public async Task FailAndStartTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -115,7 +118,6 @@ public async Task FailAndStartTest() server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, true); server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "secret"); server.System().Run = false; - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index 4f92fd4e55d..f9074f7dc60 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Body.Components; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; @@ -10,7 +11,8 @@ using Content.Server.RoundEnd; using Content.Server.Shuttles.Components; using Content.Shared.CCVar; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.GameTicking; using Content.Shared.Hands.Components; @@ -29,24 +31,27 @@ namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class NukeOpsTest +public sealed class NukeOpsTest : GameTest { private static readonly ProtoId SyndicateFaction = "Syndicate"; private static readonly ProtoId NanotrasenFaction = "NanoTrasen"; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }; + + /// /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded. /// [Test] public async Task TryStopNukeOpsFromConstantlyFailing() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -58,6 +63,7 @@ public async Task TryStopNukeOpsFromConstantlyFailing() var invSys = server.System(); var factionSys = server.System(); var roundEndSys = server.System(); + var damageSys = server.System(); server.CfgMan.SetCVar(CCVars.GridFill, true); @@ -226,16 +232,17 @@ void CheckDummy(int i) Assert.That(total, Is.GreaterThan(3)); // Check the nukie commander passed basic training and figured out how to breathe. - var totalSeconds = 30; - var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds); - var increment = 5; - var resp = entMan.GetComponent(player); - var damage = entMan.GetComponent(player); - for (var tick = 0; tick < totalTicks; tick += increment) + if (entMan.TryGetComponent(player, out var resp)) { - await pair.RunTicksSync(increment); - Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); - Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + var totalSeconds = 30; + var totalTicks = (int)Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds); + var increment = 5; + for (var tick = 0; tick < totalTicks; tick += increment) + { + await pair.RunTicksSync(increment); + Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); + Assert.That(damageSys.GetTotalDamage(player), Is.EqualTo(FixedPoint2.Zero)); + } } // Check that the round does not end prematurely when agents are deleted in the outpost @@ -257,6 +264,5 @@ await server.WaitAssertion(() => }); ticker.SetGamePreset((GamePresetPrototype?) null); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index c805d04a712..0bbf2fae4fb 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; @@ -9,12 +10,14 @@ namespace Content.IntegrationTests.Tests.GameRules { [TestFixture] [TestOf(typeof(MaxTimeRestartRuleSystem))] - public sealed class RuleMaxTimeRestartTest + public sealed class RuleMaxTimeRestartTest : GameTest { + public override PoolSettings PoolSettings => new() { InLobby = true }; + [Test] public async Task RestartTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); + var pair = Pair; var server = pair.Server; Assert.That(server.EntMan.Count(), Is.Zero); @@ -64,8 +67,6 @@ await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 5d7ae8efbf4..521b043c68d 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -1,19 +1,22 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class SecretStartsTest +public sealed class SecretStartsTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings { Dirty = true }; + /// /// Tests that when secret is started, all of the game rules it successfully adds are also started. /// [Test] public async Task TestSecretStarts() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true }); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -38,7 +41,5 @@ await server.WaitAssertion(() => // End all rules gameTicker.ClearGameRules(); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index bda931397b2..1eef1ab0a12 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -7,19 +8,21 @@ namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class StartEndGameRulesTest +public sealed class StartEndGameRulesTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Dirty = true, + DummyTicker = false + }; + /// /// Tests that all game rules can be added/started/ended at the same time without exceptions. /// [Test] public async Task TestAllConcurrent() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); var gameTicker = server.ResolveDependency().GetEntitySystem(); @@ -47,7 +50,5 @@ await server.WaitAssertion(() => gameTicker.ClearGameRules(); Assert.That(!gameTicker.GetAddedGameRules().Any()); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs index 97fe1c87627..318c4b6b425 100644 --- a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Antag.Components; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; @@ -17,23 +18,25 @@ namespace Content.IntegrationTests.Tests.GameRules; [TestFixture] -public sealed class TraitorRuleTest +public sealed class TraitorRuleTest : GameTest { private const string TraitorGameRuleProtoId = "Traitor"; private const string TraitorAntagRoleName = "Traitor"; private static readonly ProtoId SyndicateFaction = "Syndicate"; private static readonly ProtoId NanotrasenFaction = "NanoTrasen"; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true, + }; + [Test] public async Task TestTraitorObjectives() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Dirty = true, - DummyTicker = false, - Connected = true, - InLobby = true, - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var entMan = server.EntMan; @@ -123,9 +126,6 @@ await server.WaitPost(() => $"MaxDifficulty exceeded! Objectives: {string.Join(", ", mindComp.Objectives.Select(o => FormatObjective(o, entMan)))}"); Assert.That(mindComp.Objectives, Is.Not.Empty, $"No objectives assigned!"); - - - await pair.CleanReturnAsync(); } private static string FormatObjective(Entity entity, IEntityManager entMan) diff --git a/Content.IntegrationTests/Tests/GameTestTests/ContraintTests.cs b/Content.IntegrationTests/Tests/GameTestTests/ContraintTests.cs new file mode 100644 index 00000000000..34a6d6dd9c0 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/ContraintTests.cs @@ -0,0 +1,86 @@ +#nullable enable +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.IntegrationTests.NUnit.Constraints; +using Content.IntegrationTests.NUnit.Operators; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +public sealed class ConstraintsTests : GameTest +{ + [Test] + [TestOf(typeof(CompExistsConstraint))] + [Description("Ensures that a freshly spawned entity matches a constraint stating it has MetaData.")] + [RunOnSide(Side.Server)] + public void CompPositive() + { + var ent = SSpawn(null); + + Assert.That(ent, Has.Comp(Server)); + } + + [Test] + [TestOf(typeof(CompOperator))] + [Description("Ensures that NUnit property access works on Comp constraints.")] + [RunOnSide(Side.Server)] + public void CompPropertyAccess() + { + var ent = SSpawn(null); + + Assert.That(ent, + Has + .Comp(Server) + .Property(nameof(MetaDataComponent.EntityDeleted)) + .EqualTo(false) + ); + } + + [Test] + [TestOf(typeof(CompExistsConstraint))] + [Description("Ensures that a freshly spawned entity does not match a constraint stating it has some odd component.")] + [RunOnSide(Side.Server)] + public void CompNegative() + { + var ent = SSpawn(null); + + // Arbitrary pick. + Assert.That(ent, Has.No.Comp(Server)); + } + + [Test] + [TestOf(typeof(LifeStageConstraint))] + [Description("Ensures that a freshly deleted entity is deleted to constraints.")] + [RunOnSide(Side.Server)] + public void DeletedPositive() + { + var ent = SSpawn(null); + + SDeleteNow(ent); + + Assert.That(ent, Is.Deleted(Server)); + } + + [Test] + [TestOf(typeof(LifeStageConstraint))] + [RunOnSide(Side.Server)] + [Description("Entities that never existed are currently considered deleted by constraints.")] + public void DeletedNeverExisted() + { + // We'll never spawn this many ents in tests without it taking all damn day. + var ent = new EntityUid(int.MaxValue / 2); + + Assert.That(ent, Is.Deleted(Server), "Entites that never existed still count as deleted."); + } + + [Test] + [TestOf(typeof(LifeStageConstraint))] + [Description("Entities that are alive do not count as deleted.")] + [RunOnSide(Side.Server)] + public void DeletedNegative() + { + var ent = SSpawn(null); + + Assert.That(ent, Is.Not.Deleted(Server)); + } +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs b/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs new file mode 100644 index 00000000000..5ce2b486fd5 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/DependencyTests.cs @@ -0,0 +1,54 @@ +#nullable enable +using System.Collections.Generic; +using Content.Client.GameTicking.Managers; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.IntegrationTests.NUnit.Constraints; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[TestOf(typeof(GameTest))] +[TestOf(typeof(SidedDependencyAttribute))] +public sealed class DependencyTests : GameTest +{ + [SidedDependency(Side.Server)] private readonly SharedGameTicker _sGameTicker = null!; + [SidedDependency(Side.Client)] private readonly SharedGameTicker _cGameTicker = null!; + [SidedDependency(Side.Server)] private readonly EntityQuery _sXformQuery = default!; + + [Test] + [Description("Asserts that sided dependencies actually grab from the right sides.")] + public void DependenciesRespectSides() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(!ReferenceEquals(SEntMan, CEntMan), "Server and client entity managers should be distinct"); + Assert.That(SEntMan, Is.EqualTo(Server.EntMan).Using(ReferenceEqualityComparer.Instance)); + Assert.That(CEntMan, Is.EqualTo(Client.EntMan).Using(ReferenceEqualityComparer.Instance)); + } + } + + [Test] + [Description("Asserts that system dependencies actually grab from the right sides.")] + public void SystemDependenciesRespectSides() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(!ReferenceEquals(_sGameTicker, _cGameTicker), + "Server and client gametickers should be distinct"); + Assert.That(_sGameTicker, Is.TypeOf()); + Assert.That(_cGameTicker, Is.TypeOf()); + } + } + + [Test] + [Description("Asserts that query dependencies function")] + public async Task QueryDependencies() + { + var ent = await Spawn(null); + + Assert.That(_sXformQuery.HasComp(ent), Is.True); + } +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/DisconnectedDependencyTest.cs b/Content.IntegrationTests/Tests/GameTestTests/DisconnectedDependencyTest.cs new file mode 100644 index 00000000000..d71c56c3135 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/DisconnectedDependencyTest.cs @@ -0,0 +1,46 @@ +#nullable enable +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC.Exceptions; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[TestOf(typeof(GameTest))] +[TestOf(typeof(SidedDependencyAttribute))] +public sealed class DisconnectedDependencyTest : GameTest +{ + [Test] + [PairConfig(nameof(PsDisconnected))] + [Description($""" + Ensures that GameTest can be started up even when the client {nameof(EntitySystemManager)} isn't initialized yet. + No body is necessary, if this fails then the bug is firmly in GameTest itself. + """)] + public void EnsureGameTestSetupWorksDisconnected() + { + // Nothin' + } + + [Test] + [PairConfig(nameof(PsDisconnected))] + [Description(""" + Ensures dependency injection that relies on client side systems fails as expected when the client is detached. + """)] + public void ClientSystemDependencyFails() + { + var creature = new Creature(); + + Assert.Throws(() => + { + InjectDependencies(creature); + }); + } + + private sealed class Creature + { +#pragma warning disable CS0414 // Field is assigned but its value is never used + [SidedDependency(Side.Client)] private readonly MapSystem _mapSys = null!; +#pragma warning restore CS0414 // Field is assigned but its value is never used + } +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarTest.cs b/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarTest.cs new file mode 100644 index 00000000000..942254807fb --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarTest.cs @@ -0,0 +1,53 @@ +#nullable enable +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Robust.Shared.Configuration; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[TestOf(typeof(GameTest))] +[TestOf(typeof(EnsureCVarAttribute))] +[Description("Ensures EnsureCVar actually sets CVars as expected.")] +[EnsureCVar(Side.Server, typeof(EnsureCVarsTestCVars), nameof(EnsureCVarsTestCVars.Foo), true)] +public sealed class EnsureCVarTest : GameTest +{ + [SidedDependency(Side.Server)] private readonly IConfigurationManager _sCfg = default!; + [SidedDependency(Side.Client)] private readonly IConfigurationManager _cCfg = default!; + + [Test] + [Description("Ensure Foo is set and Bar is not.")] + public void FooIsSet() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Foo), Is.True); + Assert.That(_cCfg.GetCVar(EnsureCVarsTestCVars.Foo), + Is.EqualTo(EnsureCVarsTestCVars.Foo.DefaultValue), + "Foo is not replicated and should not be set on the client."); + + Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Bar), + Is.EqualTo(EnsureCVarsTestCVars.Bar.DefaultValue)); + } + } + + [Test] + [EnsureCVar(Side.Server, typeof(EnsureCVarsTestCVars), nameof(EnsureCVarsTestCVars.Bar), 42)] + [Description("Ensure Foo and Bar are set.")] + public void BarIsSet() + { + var props = TestContext.CurrentContext.Test.Properties; + using (Assert.EnterMultipleScope()) + { + Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Bar), + Is.EqualTo(42)); + Assert.That(_cCfg.GetCVar(EnsureCVarsTestCVars.Bar), + Is.EqualTo(EnsureCVarsTestCVars.Bar.DefaultValue), + "Bar is not replicated and should not be set on the client."); + + Assert.That(props[EnsureCVarAttribute.ServerEnsuredCVarsProperty].Count(), + Is.EqualTo(1), + "Expected EnsureCVar to appropriately mark its target test."); + } + } +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarsTestCVars.cs b/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarsTestCVars.cs new file mode 100644 index 00000000000..3d7afccd57e --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/EnsureCVarsTestCVars.cs @@ -0,0 +1,14 @@ +#nullable enable +using Robust.Shared.Configuration; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[CVarDefs] +public sealed class EnsureCVarsTestCVars +{ + public static readonly CVarDef Foo = + CVarDef.Create("tests.ensure_cvars.foo", false, CVar.SERVER); + + public static readonly CVarDef Bar = + CVarDef.Create("tests.ensure_cvars.bar", 3, CVar.SERVER); +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/IGameTestPairConfigModifierAttributeTests.cs b/Content.IntegrationTests/Tests/GameTestTests/IGameTestPairConfigModifierAttributeTests.cs new file mode 100644 index 00000000000..080c143b8a8 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/IGameTestPairConfigModifierAttributeTests.cs @@ -0,0 +1,23 @@ +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[TestOf(typeof(GameTest))] +[TestOf(typeof(IGameTestPairConfigModifier))] +public sealed class IGameTestPairConfigModifierAttributeTests : GameTest +{ + [Test] + public void Control() + { + Assert.That(Pair.Settings.Connected, Is.True); + } + + [Test] + [PairConfig(nameof(PsDisconnected))] + [Description("Ensures pair settings apply.")] + public void PairConfigWorks() + { + Assert.That(Pair.Settings.Connected, Is.False); + } +} diff --git a/Content.IntegrationTests/Tests/GameTestTests/RunOnSideTests.cs b/Content.IntegrationTests/Tests/GameTestTests/RunOnSideTests.cs new file mode 100644 index 00000000000..1a3de265144 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameTestTests/RunOnSideTests.cs @@ -0,0 +1,43 @@ +#nullable enable +using System.Threading; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; + +namespace Content.IntegrationTests.Tests.GameTestTests; + +[TestOf(typeof(GameTest))] +[TestOf(typeof(RunOnSideAttribute))] +[Description("Asserts that RunOnSide actually does as expected and runs the test on the given side.")] +public sealed class RunOnSideTests : GameTest +{ + [Test] + [Description("Ensures that the default scenario is the test thread.")] + public void Control() + { + Assert.That(Thread.CurrentThread, Is.Not.EqualTo(ServerThread).And.Not.EqualTo(ClientThread)); + } + + [Test] + [RunOnSide(Side.Server)] + public void TestServerSide() + { + Assert.That(Thread.CurrentThread, Is.EqualTo(ServerThread)); + } + + [Test] + [RunOnSide(Side.Client)] + public void TestClientSide() + { + Assert.That(Thread.CurrentThread, Is.EqualTo(ClientThread)); + } + + [Test] + [RunOnSide(Side.Server)] + [Description("Ensures that RunOnSide appropriately adds a property.")] + [Ignore("TestContext on the game threads is broken.")] + [TrackingIssue("https://github.com/space-wizards/RobustToolbox/issues/6449")] + public void TestProperty() + { + Assert.That(TestContext.CurrentContext.Test.Properties.Get(RunOnSideAttribute.RunOnSideProperty), Is.Not.Null); + } +} diff --git a/Content.IntegrationTests/Tests/Gibbing/GibTest.cs b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs new file mode 100644 index 00000000000..23be8f028ab --- /dev/null +++ b/Content.IntegrationTests/Tests/Gibbing/GibTest.cs @@ -0,0 +1,35 @@ +#nullable enable +using Content.IntegrationTests.Fixtures; +using Content.Shared.Gibbing; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Body; + +[TestFixture] +public sealed class GibTest : GameTest +{ + [Test] + public async Task TestGib() + { + var pair = Pair; + var (server, client) = (pair.Server, pair.Client); + var map = await pair.CreateTestMap(); + + EntityUid target = default; + + await server.WaitAssertion(() => target = server.EntMan.Spawn("MobHuman", map.MapCoords)); + await pair.WaitCommand($"setoutfit {server.EntMan.GetNetEntity(target)} CaptainGear"); + + await pair.RunTicksSync(5); + var nuid = pair.ToClientUid(target); + Assert.That(client.EntMan.EntityExists(nuid)); + + await server.WaitAssertion(() => server.System().Gib(target)); + + await pair.RunTicksSync(5); + await pair.WaitCommand("dirty"); + await pair.RunTicksSync(5); + + Assert.That(!client.EntMan.EntityExists(nuid)); + } +} diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs index 0951e7e2608..0ef52a12a66 100644 --- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs +++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Gravity; using Content.Shared.Alert; using Content.Shared.Gravity; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Gravity [TestFixture] [TestOf(typeof(GravitySystem))] [TestOf(typeof(GravityGeneratorComponent))] - public sealed class WeightlessStatusTests + public sealed class WeightlessStatusTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -38,7 +39,7 @@ public sealed class WeightlessStatusTests [Test] public async Task WeightlessStatusTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityManager = server.ResolveDependency(); @@ -86,8 +87,6 @@ await server.WaitAssertion(() => }); await pair.RunTicksSync(10); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs index 8257035de66..10804ea201b 100644 --- a/Content.IntegrationTests/Tests/GravityGridTest.cs +++ b/Content.IntegrationTests/Tests/GravityGridTest.cs @@ -1,4 +1,4 @@ -using Content.Server.Gravity; +using Content.IntegrationTests.Fixtures; using Content.Server.Power.Components; using Content.Shared.Gravity; using Robust.Shared.GameObjects; @@ -12,7 +12,7 @@ namespace Content.IntegrationTests.Tests /// making sure that gravity is applied to the correct grids. [TestFixture] [TestOf(typeof(GravityGeneratorComponent))] - public sealed class GravityGridTest + public sealed class GravityGridTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -32,7 +32,7 @@ public sealed class GravityGridTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -97,8 +97,6 @@ await server.WaitAssertion(() => Assert.That(entityMan.GetComponent(grid2).Enabled, Is.False); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs b/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs index dec2c40c0ac..98b7056d00f 100644 --- a/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs +++ b/Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Client.Guidebook; using Content.Client.Guidebook.Richtext; +using Content.IntegrationTests.Fixtures; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -13,7 +14,7 @@ namespace Content.IntegrationTests.Tests.Guidebook; /// [TestFixture] [TestOf(typeof(DocumentParsingManager))] -public sealed class DocumentParsingTest +public sealed class DocumentParsingTest : GameTest { public string TestDocument = @"multiple @@ -45,7 +46,7 @@ some text with a nested control [Test] public async Task ParseTestDocument() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; await client.WaitIdleAsync(); var parser = client.ResolveDependency(); @@ -133,8 +134,6 @@ await client.WaitPost(() => subTest2.Params.TryGetValue("k", out val); Assert.That(val, Is.EqualTo(@"<>\>=""=<-_?*3.0//")); - - await pair.CleanReturnAsync(); } public sealed class TestControl : Control, IDocumentTag diff --git a/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs b/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs index ae64eace9a2..016b06abe8f 100644 --- a/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs +++ b/Content.IntegrationTests/Tests/Guidebook/GuideEntryPrototypeTests.cs @@ -1,9 +1,11 @@ using Content.Client.Guidebook; using Content.Client.Guidebook.Richtext; +using Content.IntegrationTests.Fixtures; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; -using System.Linq; +using Content.IntegrationTests.Utility; using Content.Shared.Guidebook; +using Robust.Shared.Localization; namespace Content.IntegrationTests.Tests.Guidebook; @@ -11,32 +13,29 @@ namespace Content.IntegrationTests.Tests.Guidebook; [TestOf(typeof(GuidebookSystem))] [TestOf(typeof(GuideEntryPrototype))] [TestOf(typeof(DocumentParsingManager))] -public sealed class GuideEntryPrototypeTests +public sealed class GuideEntryPrototypeTests : GameTest { + private static string[] _guideEntries = GameDataScrounger.PrototypesOfKind(); + [Test] - public async Task ValidatePrototypeContents() + [TestCaseSource(nameof(_guideEntries))] + [Description("Ensures a given guidebook entry is valid, checking the document/etc.")] + public async Task Validate(string protoKey) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; await client.WaitIdleAsync(); var protoMan = client.ResolveDependency(); var resMan = client.ResolveDependency(); var parser = client.ResolveDependency(); - var prototypes = protoMan.EnumeratePrototypes().ToList(); + var proto = protoMan.Index(protoKey); - foreach (var proto in prototypes) + await client.WaitAssertion(() => { - await client.WaitAssertion(() => - { - using var reader = resMan.ContentFileReadText(proto.Text); - var text = reader.ReadToEnd(); - Assert.That(parser.TryAddMarkup(new Document(), text), $"Failed to parse guidebook: {proto.Id}"); - }); - - // Avoid styleguide update limit - await client.WaitRunTicks(1); - } + using var reader = resMan.ContentFileReadText(proto.Text); + var text = reader.ReadToEnd(); - await pair.CleanReturnAsync(); + Assert.That(parser.TryAddMarkup(new Document(), text), $"Failed to parse the guide entry's document."); + }); } } diff --git a/Content.IntegrationTests/Tests/Hands/HandTests.cs b/Content.IntegrationTests/Tests/Hands/HandTests.cs index d5cf75c4634..95f053a1cb7 100644 --- a/Content.IntegrationTests/Tests/Hands/HandTests.cs +++ b/Content.IntegrationTests/Tests/Hands/HandTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Hands; [TestFixture] -public sealed class HandTests +public sealed class HandTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -25,14 +26,16 @@ public sealed class HandTests "; + public override PoolSettings PoolSettings => new() + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task TestPickupDrop() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -69,17 +72,12 @@ await server.WaitPost(() => Assert.That(sys.GetActiveItem((player, hands)), Is.Null); await server.WaitPost(() => mapSystem.DeleteMap(data.MapId)); - await pair.CleanReturnAsync(); } [Test] public async Task TestPickUpThenDropInContainer() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); await pair.RunTicksSync(5); @@ -134,6 +132,5 @@ await server.WaitPost(() => Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform))); await server.WaitPost(() => mapSystem.DeleteMap(map.MapId)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs new file mode 100644 index 00000000000..817558b4269 --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Component that is used by to store any information about received events. +/// +[RegisterComponent] +public sealed partial class TestListenerComponent : Component +{ + public Dictionary> Events = new(); +} diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs new file mode 100644 index 00000000000..2481cef03f6 --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Generic system that listens for and records any received events of a given type. +/// +public abstract class TestListenerSystem : EntitySystem where TEvent : notnull +{ + public override void Initialize() + { + // TODO + // supporting broadcast events requires cleanup on test finish, which will probably require changes to the + // test pair/pool manager and would conflict with #36797 + SubscribeLocalEvent(OnDirectedEvent); + } + + protected virtual void OnDirectedEvent(Entity ent, ref TEvent args) + { + ent.Comp.Events.GetOrNew(args.GetType()).Add(args); + } + + public int Count(EntityUid uid, Func? predicate = null) + { + return GetEvents(uid, predicate).Count(); + } + + public void Clear(EntityUid uid) + { + CompOrNull(uid)?.Events.Remove(typeof(TEvent)); + } + + public IEnumerable GetEvents(EntityUid uid, Func? predicate = null) + { + var events = CompOrNull(uid)?.Events.GetValueOrDefault(typeof(TEvent)); + if (events == null) + return []; + + return events.Cast().Where(e => predicate?.Invoke(e) ?? true); + } +} diff --git a/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs b/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs new file mode 100644 index 00000000000..2fc1ce6401c --- /dev/null +++ b/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs @@ -0,0 +1,113 @@ +#nullable enable +using Content.IntegrationTests.Tests.Movement; +using Content.Shared.Holosign; +using Content.Shared.PowerCell; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Spawners; + +namespace Content.IntegrationTests.Tests.Holosign; + +/// +/// Tests for different devices using . +/// +[TestOf(typeof(HolosignProjectorComponent))] +public sealed class HolosignProjectorTest : MovementTest +{ + private static readonly EntProtoId HoloBarrierProjectorProtoId = "HoloprojectorSecurity"; + private static readonly EntProtoId HoloSignProjectorProtoId = "Holoprojector"; + + /// + /// Tests the janitors holosign projector. + /// + [Test] + public async Task HoloSignTest() + { + var projector = await PlaceInHands(HoloSignProjectorProtoId); + var projectorComp = Comp(projector); + var signProtoId = projectorComp.SignProto; + + // No holosigns before using the item. + await AssertEntityLookup((WallPrototype, 2)); + + var powerCellSystem = SEntMan.System(); + var initialUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(initialUses, Is.GreaterThan(0), "Holoprojector spawned without usable charges."); + + // Click on the tile next to the player. + await Interact(null, TargetCoords); + + // We should have one charge less. + var remainingUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(remainingUses, Is.EqualTo(initialUses - 1), "Holoprojector did not use the right amount of charge when used."); + + // We should have spawned exactly one holosign. + await AssertEntityLookup( + (signProtoId, 1), + (WallPrototype, 2)); + + // Try spawn more holosigns than we have charge. + for (var i = 0; i < initialUses; i++) + { + await Interact(null, TargetCoords); + } + + // The total should be the same as the initial charges. + await AssertEntityLookup( + (signProtoId, initialUses), + (WallPrototype, 2)); + + // We should have no charges left. + remainingUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(remainingUses, Is.Zero, "Holoprojector did not use up all charges."); + } + + /// + /// Tests the security holo barrier projector and the barrier. + /// + [Test] + public async Task HoloBarrierTest() + { + var projector = await PlaceInHands(HoloBarrierProjectorProtoId); + var holoBarrierProtoId = Comp(projector).SignProto; + // No holobarriers before using the item. + await AssertEntityLookup((WallPrototype, 2)); + + // Click on the tile next to the player. + await Interact(null, TargetCoords); + + // We should have spawned exactly one holobarrier. + await AssertEntityLookup( + (holoBarrierProtoId, 1), + (WallPrototype, 2)); + Target = FromServer(await FindEntity(holoBarrierProtoId)); + var timeRemaining = Comp(Target).Lifetime; + + // Check that the barrier is at the location we clicked at. + AssertLocation(Target, TargetCoords); + + // Try moving past the barrier. + Assert.That(Delta(), Is.GreaterThan(0.5), "Player was not located west of the holobarrier."); + await Move(DirectionFlag.East, 0.5f); + Assert.That(Delta(), Is.GreaterThan(0.5), "Player was able to walk through a holobarrier."); + + // Try to climb the barrier. + await Interact(Target, TargetCoords, altInteract: true); + + // We should be able to move past the barrier now. + await Move(DirectionFlag.East, 0.5f); + Assert.That(Delta(), Is.LessThan(-0.5), "Player was not able to climb over a holobarrier."); + + // We should not be able to walk back without climbing again. + await Move(DirectionFlag.West, 0.5f); + Assert.That(Delta(), Is.LessThan(-0.5), "Player was able to walk through a holobarrier."); + + // Wait until the barrier despawns. + await RunSeconds(timeRemaining); + AssertDeleted(Target); + + // We should be able to walk back now. + await Move(DirectionFlag.West, 0.5f); + Assert.That(DeltaCoordinates(), Is.GreaterThan(0.5), "Player was able to walk past a deleted holobarrier."); + } +} diff --git a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs index 929a2311599..accdd4667c0 100644 --- a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs +++ b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Inventory; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests // i.e. the interaction between uniforms and the pocket/ID slots. // and also how big items don't fit in pockets. [TestFixture] - public sealed class HumanInventoryUniformSlotsTest + public sealed class HumanInventoryUniformSlotsTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -55,7 +56,7 @@ public sealed class HumanInventoryUniformSlotsTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); var coordinates = testMap.GridCoords; @@ -130,8 +131,6 @@ await server.WaitAssertion(() => mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } private static bool IsDescendant(EntityUid descendant, EntityUid parent, IEntityManager entManager) diff --git a/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs b/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs new file mode 100644 index 00000000000..24d8da479c6 --- /dev/null +++ b/Content.IntegrationTests/Tests/Humanoid/HideableHumanoidLayersTest.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Inventory; +using Robust.Client.GameObjects; + +namespace Content.IntegrationTests.Tests.Humanoid; + +[TestOf(typeof(SharedHideableHumanoidLayersSystem))] +public sealed class HideableHumanoidLayersTest : InteractionTest +{ + protected override string PlayerPrototype => "MobVulpkanin"; + + [Test] + public async Task BasicHiding() + { + await SpawnTarget("ClothingMaskGas"); + await Pickup(); // equip mask + await UseInHand(); + + await Server.WaitAssertion(() => + { + var hideableHumanoidLayers = SEntMan.GetComponent(SPlayer); + Assert.That(hideableHumanoidLayers.HiddenLayers, Does.ContainKey(HumanoidVisualLayers.Snout).WithValue(SlotFlags.MASK)); + }); + + await Server.WaitAssertion(() => + { + SEntMan.DeleteEntity(STarget); // de-equip mask + + var hideableHumanoidLayers = SEntMan.GetComponent(SPlayer); + Assert.That(hideableHumanoidLayers.HiddenLayers, Does.Not.ContainKey(HumanoidVisualLayers.Snout)); + }); + } + + [Test] + public async Task DependentHiding() + { + await Server.WaitAssertion(() => + { + var visualBody = SEntMan.System(); + visualBody.ApplyMarkings(SPlayer, new() + { + ["Head"] = new() + { + [HumanoidVisualLayers.SnoutCover] = new List() { new("VulpSnoutNose", 1) }, + }, + }); + }); + + await SpawnTarget("ClothingMaskGas"); + await Pickup(); // equip mask + await UseInHand(); + + await RunTicks(20); + + await Client.WaitAssertion(() => + { + var spriteSystem = CEntMan.System(); + var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout"); + var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose"); + var spriteComp = CEntMan.GetComponent(CPlayer); + + Assert.That(spriteComp[snoutIndex].Visible, Is.False); + Assert.That(spriteComp[snoutCoverIndex].Visible, Is.False); + }); + + await Server.WaitAssertion(() => + { + SEntMan.DeleteEntity(STarget); // de-equip mask + }); + + await RunTicks(20); + + await Client.WaitAssertion(() => + { + var spriteSystem = CEntMan.System(); + var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout"); + var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose"); + var spriteComp = CEntMan.GetComponent(CPlayer); + + Assert.That(spriteComp[snoutIndex].Visible, Is.True); + Assert.That(spriteComp[snoutCoverIndex].Visible, Is.True); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs new file mode 100644 index 00000000000..4ee65c9c32e --- /dev/null +++ b/Content.IntegrationTests/Tests/Humanoid/HideablePrototypeValidation.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Body; +using Content.Shared.Clothing.Components; +using Content.Shared.Humanoid; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Humanoid; + +[TestFixture] +public sealed class HideablePrototypeValidation : GameTest +{ + [Test] + public async Task NoOrgansWithoutClothing() + { + var pair = Pair; + var requirements = new Dictionary>(); + foreach (var (proto, component) in pair.GetPrototypesWithComponent()) + { + foreach (var layer in component.HideableLayers) + { + requirements[layer] = requirements.GetValueOrDefault(layer) ?? []; + requirements[layer].Add(proto.ID); + } + } + + var provided = new HashSet(); + foreach (var (_, component) in pair.GetPrototypesWithComponent()) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (component.Slots is { } slots) + { + provided.UnionWith(slots); + } + provided.UnionWith(component.Layers.Keys); +#pragma warning restore CS0618 // Type or member is obsolete + } + + using var scope = Assert.EnterMultipleScope(); + foreach (var (key, requirement) in requirements) + { + Assert.That(provided, Does.Contain(key), $"No clothing will hide {key} that can be hidden on {string.Join(", ", requirement.Select(it => it.Id))}"); + } + } + + [Test] + public async Task NoClothingWithoutOrgans() + { + var pair = Pair; + + var requirements = new Dictionary>(); + foreach (var (proto, component) in pair.GetPrototypesWithComponent()) + { +#pragma warning disable CS0618 // Type or member is obsolete + foreach (var layer in component.Layers.Keys.Concat(component.Slots ?? [])) +#pragma warning restore CS0618 // Type or member is obsolete + { + requirements[layer] = requirements.GetValueOrDefault(layer) ?? []; + requirements[layer].Add(proto.ID); + } + } + + var provided = new HashSet(); + foreach (var (_, component) in pair.GetPrototypesWithComponent()) + { + provided.UnionWith(component.HideableLayers); + } + + using var scope = Assert.EnterMultipleScope(); + foreach (var (key, requirement) in requirements) + { + Assert.That(provided, Does.Contain(key), $"No organ will hide {key} that can be hidden by {string.Join(", ", requirement.Select(it => it.Id))}"); + } + } +} diff --git a/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs new file mode 100644 index 00000000000..48eb521c5e7 --- /dev/null +++ b/Content.IntegrationTests/Tests/Humanoid/HumanoidProfileTests.cs @@ -0,0 +1,48 @@ +using Content.IntegrationTests.Fixtures; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Content.Shared.Speech.Components; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Humanoid; + +[TestFixture] +[TestOf(typeof(HumanoidProfileSystem))] +public sealed class HumanoidProfileTests : GameTest +{ + private static readonly ProtoId Vox = "Vox"; + + [Test] + public async Task EnsureValidLoading() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var entityManager = server.ResolveDependency(); + var humanoidProfile = entityManager.System(); + var human = entityManager.Spawn("MobHuman"); + humanoidProfile.ApplyProfileTo(human, new HumanoidCharacterProfile() + .WithSex(Sex.Female) + .WithAge(67) + .WithGender(Gender.Neuter) + .WithSpecies(Vox)); + var humanoidComponent = entityManager.GetComponent(human); + var voiceComponent = entityManager.GetComponent(human); + + Assert.That(humanoidComponent.Age, Is.EqualTo(67)); + Assert.That(humanoidComponent.Sex, Is.EqualTo(Sex.Female)); + Assert.That(humanoidComponent.Gender, Is.EqualTo(Gender.Neuter)); + Assert.That(humanoidComponent.Species, Is.EqualTo(Vox)); + + Assert.That(voiceComponent.Sounds, Is.Not.Null, message: "the MobHuman spawned by this test needs to have sex-specific sound set"); + Assert.That(voiceComponent.Sounds![Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds)); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Imperial/DeimonFly/RouletteSystemsTest.cs b/Content.IntegrationTests/Tests/Imperial/DeimonFly/RouletteSystemsTest.cs new file mode 100644 index 00000000000..cd867167100 --- /dev/null +++ b/Content.IntegrationTests/Tests/Imperial/DeimonFly/RouletteSystemsTest.cs @@ -0,0 +1,328 @@ +using System.Linq; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.DeimonFly.BuckshotRoulette; +using Content.Shared.Imperial.DeimonFly.RussianRoulette; +using Content.Shared.Medical.Healing; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Components; +using Content.Shared.Traits.Assorted; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.Wieldable.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Imperial.DeimonFly; + +[TestOf(typeof(RussianRouletteGunComponent))] +[TestOf(typeof(BuckshotRouletteShotgunComponent))] +public sealed class RouletteSystemsTest : InteractionTest +{ + protected override string PlayerPrototype => HumanPrototype; + + private const string HumanPrototype = "MobHuman"; + private const string RussianRouletteRevolver = "WeaponRevolverFinalBet"; + private const string UnrevivableRussianRouletteRevolver = "WeaponRevolverFinalBetUnrevivable"; + private const string DealerShotgun = "WeaponShotgunBuckshotRoulette"; + private const string LiveShell = "ShellShotgunBuckshotRouletteLive"; + private const string BlankShell = "ShellShotgunBuckshotRouletteBlank"; + private const string HandSaw = "BuckshotRouletteHandSaw"; + private const string MedicalKit = "BuckshotRouletteMedicalKit"; + + [Test] + public async Task RussianRouletteSpawnsOneCartridgeAndDamagesOnlyShooter() + { + await AddAtmosphere(); + var target = await SpawnTarget(HumanPrototype); + var revolver = await PlaceInHands(RussianRouletteRevolver); + var revolverUid = ToServer(revolver); + var provider = SEntMan.GetComponent(revolverUid); + + await Server.WaitAssertion(() => + { + var loadedSlots = provider.AmmoSlots + .Select((uid, index) => (uid, index)) + .Where(entry => entry.uid != null) + .ToList(); + + Assert.That(loadedSlots, Has.Count.EqualTo(1), + "Револьвер русской рулетки должен появляться ровно с одним патроном."); + Assert.That(provider.Chambers.Count(chamber => chamber == true), Is.EqualTo(1), + "Только одна камора должна содержать боевой патрон."); + Assert.That(provider.Chambers[loadedSlots[0].index], Is.True, + "Состояние выбранной каморы не соответствует помещённому в неё патрону."); + Assert.That(SEntMan.GetComponent(loadedSlots[0].uid!.Value).EntityPrototype?.ID, + Is.EqualTo("CartridgeMagnumFinalBet")); + Assert.That(SGun.GetAmmoCount(revolverUid), Is.EqualTo(1)); + + // Убираем случайность только из самого выстрела: + // генерация выше по-прежнему проверяется как случайная камора. + provider.CurrentIndex = loadedSlots[0].index; + SEntMan.Dirty(revolverUid, provider); + }); + + await RunSeconds(2f); + await AttemptShoot(target); + await RunTicks(3); + + Assert.Multiple(() => + { + Assert.That(GetDamageOfType(SPlayer, "Piercing"), Is.EqualTo(FixedPoint2.New(200)), + "Подтверждённый выстрел должен нанести стрелку ровно 200 урона."); + Assert.That(GetDamageOfType(ToServer(target), "Piercing"), Is.EqualTo(FixedPoint2.Zero), + "Безвредный внешний снаряд не должен наносить урон выбранной цели."); + Assert.That(SEntMan.GetComponent(SPlayer).CurrentState, Is.EqualTo(MobState.Dead)); + Assert.That(SEntMan.HasComponent(SPlayer), Is.False, + "Обычная версия револьвера не должна запрещать воскрешение."); + Assert.That(SEntMan.HasComponent(revolverUid), Is.False, + "Ивентовый револьвер не должен засчитываться как служебный пистолет для цели кражи."); + }); + } + + [Test] + public async Task UnrevivableRussianRouletteMarksOnlyADeadShooter() + { + await AddAtmosphere(); + var target = await SpawnTarget(HumanPrototype); + await PlaceLoadedRevolver(UnrevivableRussianRouletteRevolver); + + await RunSeconds(2f); + await AttemptShoot(target); + await RunTicks(3); + + Assert.Multiple(() => + { + Assert.That(SEntMan.GetComponent(SPlayer).CurrentState, Is.EqualTo(MobState.Dead)); + Assert.That(SEntMan.HasComponent(SPlayer), Is.True, + "Безвозвратная версия должна запрещать воскрешение убитого стрелка."); + }); + } + + [Test] + public async Task UnrevivableRussianRouletteDoesNotMarkASurvivor() + { + await AddAtmosphere(); + var target = await SpawnTarget(HumanPrototype); + await PlaceLoadedRevolver(UnrevivableRussianRouletteRevolver); + + await Server.WaitPost(() => + { + var thresholds = SEntMan.System(); + thresholds.SetMobStateThreshold(SPlayer, FixedPoint2.New(300), MobState.Dead); + }); + + await RunSeconds(2f); + await AttemptShoot(target); + await RunTicks(3); + + Assert.Multiple(() => + { + Assert.That(SEntMan.GetComponent(SPlayer).CurrentState, + Is.EqualTo(MobState.Critical)); + Assert.That(SEntMan.HasComponent(SPlayer), Is.False, + "Временная метка должна сниматься, если 200 урона не убили нестандартную сущность."); + }); + } + + [TestCase(LiveShell, BuckshotRouletteFireMode.Target, 0, 34, + TestName = "Buckshot live shell damages target")] + [TestCase(BlankShell, BuckshotRouletteFireMode.Target, 0, 0, + TestName = "Buckshot blank shell damages nobody in target mode")] + [TestCase(LiveShell, BuckshotRouletteFireMode.Self, 34, 0, + TestName = "Buckshot live shell damages shooter in self mode")] + [TestCase(BlankShell, BuckshotRouletteFireMode.Self, 0, 0, + TestName = "Buckshot blank shell damages nobody in self mode")] + public async Task BuckshotShellAndFireModeAreRespected( + string shellPrototype, + BuckshotRouletteFireMode mode, + int expectedShooterDamage, + int expectedTargetDamage) + { + await AddAtmosphere(); + var target = await SpawnTarget(HumanPrototype); + var shotgun = await PlaceInHands(DealerShotgun); + var shotgunUid = ToServer(shotgun); + var roulette = SEntMan.GetComponent(shotgunUid); + + await Server.WaitPost(() => + { + roulette.FireMode = mode; + SEntMan.Dirty(shotgunUid, roulette); + }); + await LoadShell(shotgunUid, shellPrototype); + + await UseInHand(); + Assert.That(SEntMan.GetComponent(shotgunUid).Wielded, Is.True, + "Дробовик должен быть взят в две руки перед выстрелом."); + + await RunSeconds(2f); + await AttemptShoot(target); + await RunSeconds(0.5f); + + Assert.Multiple(() => + { + Assert.That(GetDamageOfType(SPlayer, "Cellular"), + Is.EqualTo(FixedPoint2.New(expectedShooterDamage))); + Assert.That(GetDamageOfType(ToServer(target), "Cellular"), + Is.EqualTo(FixedPoint2.New(expectedTargetDamage))); + Assert.That(SGun.GetAmmoCount(shotgunUid), Is.Zero, + "Использованный боевой или холостой патрон должен покинуть дробовик."); + }); + } + + [Test] + public async Task SawedTargetShotDoublesDamage() + { + await AddAtmosphere(); + var shotgun = await SpawnTarget(DealerShotgun); + var shotgunUid = ToServer(shotgun); + var roulette = SEntMan.GetComponent(shotgunUid); + + await InteractUsing(HandSaw); + await Pickup(shotgun); + await LoadShell(shotgunUid, LiveShell); + await Server.WaitPost(() => + { + roulette.FireMode = BuckshotRouletteFireMode.Target; + SEntMan.Dirty(shotgunUid, roulette); + }); + + await UseInHand(); + var target = await SpawnTarget(HumanPrototype); + await RunSeconds(2f); + await AttemptShoot(target); + await RunSeconds(0.5f); + + Assert.Multiple(() => + { + Assert.That(GetDamageOfType(SPlayer, "Cellular"), Is.EqualTo(FixedPoint2.Zero)); + Assert.That(GetDamageOfType(ToServer(target), "Cellular"), Is.EqualTo(FixedPoint2.New(68)), + "Пила должна удваивать урон боевого патрона в режиме стрельбы по цели."); + Assert.That(SGun.GetAmmoCount(shotgunUid), Is.Zero); + Assert.That(roulette.DoubleNextShot, Is.False); + Assert.That(roulette.BarrelRestorationPending, Is.True); + }); + } + + [Test] + public async Task MedicalKitHealsOnlyCellularDamage() + { + var kit = await SpawnTarget(MedicalKit); + var healing = SEntMan.GetComponent(ToServer(kit)); + + Assert.Multiple(() => + { + Assert.That(healing.Damage.DamageDict["Cellular"], Is.EqualTo(FixedPoint2.New(-34))); + Assert.That(healing.Damage.DamageDict.Count(entry => entry.Value != FixedPoint2.Zero), Is.EqualTo(1), + "Аптечка не должна наследовать лечение обычных травм от HardTraumapack1."); + }); + } + + [Test] + public async Task SawedSelfShotRestoresBarrelAfterCriticalDrop() + { + await AddAtmosphere(); + var shotgun = await SpawnTarget(DealerShotgun); + var shotgunUid = ToServer(shotgun); + var roulette = SEntMan.GetComponent(shotgunUid); + var damage = SEntMan.System(); + + // Используем настоящую пилу, чтобы тест проверял не только вручную выставленное состояние компонента. + await InteractUsing(HandSaw); + Assert.Multiple(() => + { + Assert.That(roulette.DoubleNextShot, Is.True); + Assert.That(roulette.BarrelVisualState, Is.EqualTo(BuckshotRouletteBarrelVisualState.Sawed)); + }); + + await Pickup(shotgun); + await LoadShell(shotgunUid, LiveShell); + await Server.WaitPost(() => + { + roulette.FireMode = BuckshotRouletteFireMode.Self; + SEntMan.Dirty(shotgunUid, roulette); + + // 68 предварительного урона гарантируют переход в крит от усиленного выстрела на 68. + var preliminaryDamage = new Content.Shared.Damage.DamageSpecifier(roulette.SelfDamage) * 2f; + damage.TryChangeDamage(SPlayer, preliminaryDamage, ignoreResistances: true); + }); + Assert.That(SEntMan.GetComponent(SPlayer).CurrentState, Is.EqualTo(MobState.Alive)); + + await UseInHand(); + Assert.That(SEntMan.GetComponent(shotgunUid).Wielded, Is.True); + + var target = await SpawnTarget(HumanPrototype); + await RunSeconds(2f); + await AttemptShoot(target); + await RunTicks(3); + + Assert.Multiple(() => + { + Assert.That(SEntMan.GetComponent(SPlayer).CurrentState, Is.EqualTo(MobState.Critical), + "Усиленный самовыстрел должен перевести предварительно раненого человека в крит."); + Assert.That(HandSys.GetActiveItem((SPlayer, Hands)), Is.Null, + "Переход в крит должен выбросить дробовик из рук."); + Assert.That(roulette.DoubleNextShot, Is.False); + Assert.That(roulette.BarrelRestorationPending, Is.False, + "Синхронное выпадение при переходе в крит должно сразу запустить восстановление."); + Assert.That(roulette.BarrelVisualState, Is.EqualTo(BuckshotRouletteBarrelVisualState.Restoring)); + Assert.That(GetDamageOfType(ToServer(target), "Cellular"), Is.EqualTo(FixedPoint2.Zero)); + }); + + await RunSeconds((float) roulette.BarrelRestoreDuration.TotalSeconds + 0.2f); + + Assert.Multiple(() => + { + Assert.That(roulette.BarrelVisualState, Is.EqualTo(BuckshotRouletteBarrelVisualState.Intact)); + Assert.That(roulette.BarrelRestoreAt, Is.Null); + }); + } + + private async Task LoadShell(EntityUid shotgunUid, EntProtoId shellPrototype) + { + await Server.WaitAssertion(() => + { + var provider = SEntMan.GetComponent(shotgunUid); + var coordinates = SEntMan.GetComponent(shotgunUid).Coordinates; + var shell = SEntMan.SpawnEntity(shellPrototype, coordinates); + + Assert.That(SGun.TryBallisticInsert( + (shotgunUid, provider), + shell, + SPlayer, + suppressInsertionSound: true), + $"Не удалось загрузить {shellPrototype} в тестовый дробовик."); + }); + await RunTicks(1); + } + + private async Task PlaceLoadedRevolver(EntProtoId prototype) + { + var revolver = await PlaceInHands(prototype); + var revolverUid = ToServer(revolver); + + await Server.WaitPost(() => + { + var provider = SEntMan.GetComponent(revolverUid); + provider.CurrentIndex = provider.AmmoSlots.FindIndex(slot => slot != null); + Assert.That(provider.CurrentIndex, Is.GreaterThanOrEqualTo(0)); + SEntMan.Dirty(revolverUid, provider); + }); + + return revolverUid; + } + + private FixedPoint2 GetDamageOfType(EntityUid uid, string damageType) + { + var component = SEntMan.GetComponent(uid); + var damage = SEntMan.System().GetPositiveDamage((uid, component)); + return damage.DamageDict.TryGetValue(damageType, out var amount) + ? amount + : FixedPoint2.Zero; + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index 6ac40e92a1e..e4bd4615c58 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -1,5 +1,6 @@ #nullable enable annotations using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Interaction; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click { [TestFixture] [TestOf(typeof(InteractionSystem))] - public sealed class InteractionSystemTests + public sealed class InteractionSystemTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -40,7 +41,7 @@ public sealed class InteractionSystemTests [Test] public async Task InteractionTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -101,13 +102,12 @@ await server.WaitAssertion(() => }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionObstructionTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -168,13 +168,12 @@ await server.WaitAssertion(() => }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionInRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -234,14 +233,13 @@ await server.WaitAssertion(() => }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InteractionOutOfRangeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -300,13 +298,12 @@ await server.WaitAssertion(() => }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } [Test] public async Task InsideContainerInteractionBlockTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -388,7 +385,6 @@ await server.WaitAssertion(() => }); testInteractionSystem.ClearHandlers(); - await pair.CleanReturnAsync(); } public sealed class TestInteractionSystem : EntitySystem diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs index 801433ae72b..966f13675d5 100644 --- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs +++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Shared.Interaction; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Interaction { [TestFixture] [TestOf(typeof(SharedInteractionSystem))] - public sealed class InRangeUnobstructed + public sealed class InRangeUnobstructed : GameTest { private const string HumanId = "MobHuman"; @@ -27,7 +28,7 @@ public sealed class InRangeUnobstructed [Test] public async Task EntityEntityTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -109,8 +110,6 @@ await server.WaitAssertion(() => Assert.That(interactionSys.InRangeUnobstructed(mapCoordinates, origin, InteractionRangeDivided15Times3)); }); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs index 8917ba7ead5..1aac18f3a4f 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs @@ -11,7 +11,9 @@ public abstract partial class InteractionTest protected const string Floor = "FloorSteel"; protected const string FloorItem = "FloorTileItemSteel"; protected const string Plating = "Plating"; + protected const string PlatingRCD = "PlatingRCD"; protected const string Lattice = "Lattice"; + protected const string PlatingBrass = "PlatingBrass"; // Structures protected const string Airlock = "Airlock"; diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs index ca7445c359a..6823e1ac97f 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs @@ -49,6 +49,9 @@ public EntitySpecifier(string prototype, int quantity, bool converted = false) 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); @@ -93,7 +96,7 @@ protected async Task SpawnEntity(EntitySpecifier spec, EntityCoordina await Server.WaitPost(() => { uid = SEntMan.SpawnEntity(stackProto.Spawn, coords); - Stack.SetCount(uid, spec.Quantity); + Stack.SetCount((uid, null), spec.Quantity); }); return uid; } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index c835a36ed5b..0f853979017 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Reflection; using Content.Client.Construction; +using Content.IntegrationTests.Tests.Helpers; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; using Content.Server.Gravity; @@ -22,6 +23,8 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; +using Robust.Shared.Reflection; +using Robust.UnitTesting; using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent; namespace Content.IntegrationTests.Tests.Interaction; @@ -29,6 +32,8 @@ namespace Content.IntegrationTests.Tests.Interaction; // This partial class defines various methods that are useful for performing & validating interactions public abstract partial class InteractionTest { + private Dictionary _listenerCache = new(); + /// /// Begin constructing an entity. /// @@ -86,16 +91,18 @@ await Server.WaitPost(() => } /// - /// Spawn an entity at the target coordinates and set it as the target. + /// Spawn an entity at the given coordinates and set it as the target. + /// If no coordinates are given it will default to /// [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))] #pragma warning disable CS8774 // Member must have a non-null value when exiting. - protected async Task SpawnTarget(string prototype) + protected async Task SpawnTarget(string prototype, NetCoordinates? coords = null) { + coords ??= TargetCoords; Target = NetEntity.Invalid; await Server.WaitPost(() => { - Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords))); + Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value))); }); await RunTicks(5); @@ -105,14 +112,16 @@ await Server.WaitPost(() => #pragma warning restore CS8774 // Member must have a non-null value when exiting. /// - /// Spawn an entity entity at the target coordinates without setting it as the target. + /// Spawn an entity entity at the given coordinates without setting it as the target. + /// If no coordinates are given it will default to /// - protected async Task Spawn(string prototype) + protected async Task Spawn(string prototype, NetCoordinates? coords = null) { + coords ??= TargetCoords; var entity = NetEntity.Invalid; await Server.WaitPost(() => { - entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords))); + entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(coords.Value))); }); await RunTicks(5); @@ -169,6 +178,12 @@ protected async Task PlaceInHands(string id, int quantity = 1, bool e /// Whether or not to automatically enable any toggleable items protected async Task PlaceInHands(EntitySpecifier entity, bool enableToggleable = true) { + if (Hands == null) + { + Assert.Fail("No HandsComponent"); + return default; + } + if (Hands.ActiveHandId == null) { Assert.Fail("No active hand"); @@ -210,6 +225,12 @@ protected async Task Pickup(NetEntity? entity = null, bool deleteHeld = true) { entity ??= Target; + if (Hands == null) + { + Assert.Fail("No HandsComponent"); + return; + } + if (Hands.ActiveHandId == null) { Assert.Fail("No active hand"); @@ -390,6 +411,33 @@ protected async Task Interact(params EntitySpecifier[] specifiers) } } + /// + /// Simulates a drag and drop mouse interaction from one entity to another. + /// + protected async Task DragDrop(NetEntity source, NetEntity target) + { + // ScreenCoordinates diff needs to be larger than DragDropSystem.Deadzone for the drag drop to initiate + var screenX = CDragDropSys.Deadzone + 1f; + + // Start drag + await SetKey(EngineKeyFunctions.Use, + BoundKeyState.Down, + NetPosition(source), + source, + screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main)); + + await RunTicks(3); + + // End drag + await SetKey(EngineKeyFunctions.Use, + BoundKeyState.Up, + NetPosition(target), + target, + screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main)); + + await RunTicks(3); + } + /// /// Throw the currently held entity. Defaults to targeting the current /// @@ -461,11 +509,11 @@ protected async Task AttemptShoot(NetCoordinates? target = null, bool assert = t var wasInCombatMode = IsInCombatMode(); await SetCombatMode(true); - Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!"); + Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!"); await Server.WaitAssertion(() => { - var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget); + var success = SGun.AttemptShoot(SPlayer, gun, actualTarget); if (assert) Assert.That(success, "Gun failed to shoot."); }); @@ -500,11 +548,11 @@ protected async Task AttemptShoot(NetEntity? target = null, bool assert = true) var wasInCombatMode = IsInCombatMode(); await SetCombatMode(true); - Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!"); + Assert.That(SGun.TryGetGun(SPlayer, out var gun), "Player was not holding a gun!"); await Server.WaitAssertion(() => { - var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget)); + var success = SGun.AttemptShoot(SPlayer, gun, Position(actualTarget!.Value), ToServer(actualTarget)); if (assert) Assert.That(success, "Gun failed to shoot."); }); @@ -714,7 +762,7 @@ await Server.WaitPost(() => tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile; }); - Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId)); + Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId), $"Expected tile at NetCoordinates {coords}: {TileMan[targetTile.TypeId].Name}. But was: {TileMan[tile.TypeId].Name}"); } protected void AssertGridCount(int value) @@ -730,6 +778,153 @@ protected void AssertGridCount(int value) Assert.That(count, Is.EqualTo(value)); } + /// + /// Check that some entity is close to a certain coordinate location. + /// + /// The entity to check the location for. Defaults to + /// The coordinates the entity should be at. + /// The maximum allowed distance from the target coords + protected void AssertLocation(NetEntity? target, NetCoordinates coords, float radius = 0.01f) + { + target ??= Target; + Assert.That(target, Is.Not.Null, "No target specified"); + Assert.That(Position(target!.Value).TryDelta(SEntMan, Transform, ToServer(coords), out var delta), "Could not calculate distance between coordinates."); + Assert.That(delta.Length(), Is.LessThanOrEqualTo(radius), $"{SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} was not at the intended location. Distance: {delta}, allowed distance: {radius}"); + } + + #endregion + + #region EventListener + + /// + /// Asserts that running the given action causes an event to be fired directed at the specified entity (defaults to ). + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertFiresEvent(Func act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + var sys = GetListenerSystem(); + + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return; + } + + if (clear) + sys.Clear(uid.Value); + else + count += sys.Count(uid.Value); + + await Server.WaitPost(() => SEntMan.EnsureComponent(uid.Value)); + await act(); + AssertEvent(uid, count: count); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertPostFiresEvent(Action act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitPost(act), uid, count, clear); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertAssertionFiresEvent(Action act, + EntityUid? uid = null, + int count = 1, + bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitAssertion(act), uid, count, clear); + } + + /// + /// Asserts that the specified event has been fired some number of times at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events were directed + /// How many new events are expected + /// A predicate that can be used to filter the recorded events + protected void AssertEvent(EntityUid? uid = null, int count = 1, Func? predicate = null) + where TEvent : notnull + { + Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count)); + } + + /// + /// Gets all the events of the specified type that have been fired at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only gets for server-side events. + /// + /// The entity at which the events were directed + /// A predicate that can be used to filter the returned events + protected IEnumerable GetEvents(EntityUid? uid = null, Func? predicate = null) + where TEvent : notnull + { + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return []; + } + + Assert.That(SEntMan.HasComponent(uid), $"Entity must have {nameof(TestListenerComponent)}"); + return GetListenerSystem().GetEvents(uid.Value, predicate); + } + + protected TestListenerSystem GetListenerSystem() + where TEvent : notnull + { + if (_listenerCache.TryGetValue(typeof(TEvent), out var listener)) + return (TestListenerSystem)listener; + + var type = Server.Resolve().GetAllChildren>().Single(); + if (!SEntMan.EntitySysManager.TryGetEntitySystem(type, out var systemObj)) + { + // There has to be a listener system that is manually defined. Event subscriptions are locked once + // finalized, so we can't really easily create new subscriptions on the fly. + // TODO find a better solution + throw new InvalidOperationException($"Event {typeof(TEvent).Name} has no associated listener system!"); + } + + var system = (TestListenerSystem)systemObj; + _listenerCache[typeof(TEvent)] = system; + return system; + } + + /// + /// Clears all recorded events of the given type. + /// + protected void ClearEvents(EntityUid uid) where TEvent : notnull + => GetListenerSystem().Clear(uid); + #endregion #region Entity lookups @@ -860,7 +1055,7 @@ protected async Task FindEntity( /// List of currently active DoAfters on the player. /// protected IEnumerable ActiveDoAfters - => DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed); + => DoAfters?.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed) ?? []; #region Component @@ -974,9 +1169,9 @@ protected async Task RunSeconds(float seconds) /// /// Sends a bui message using the given bui key. /// - protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? _ = null) + protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, NetEntity? target = null) { - if (!TryGetBui(key, out var bui)) + if (!TryGetBui(key, out var bui, target)) return; await Client.WaitPost(() => bui.SendMessage(msg)); @@ -988,9 +1183,9 @@ protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? /// /// Sends a bui message using the given bui key. /// - protected async Task CloseBui(Enum key, EntityUid? _ = null) + protected async Task CloseBui(Enum key, NetEntity? target = null) { - if (!TryGetBui(key, out var bui)) + if (!TryGetBui(key, out var bui, target)) return; await Client.WaitPost(() => bui.Close()); @@ -1412,15 +1607,25 @@ protected async Task Move(DirectionFlag dir, float seconds) protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent); protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent); protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid)); - protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid)); + protected EntityUid ToClient(EntityUid suid) => CEntMan.GetEntity(SEntMan.GetNetEntity(suid)); protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid)); - protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid)); + protected EntityUid? ToClient(EntityUid? suid) => CEntMan.GetEntity(SEntMan.GetNetEntity(suid)); protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords); protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords); protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords); protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords); + protected NetEntity FromServer(EntityUid suid) => SEntMan.GetNetEntity(suid); + protected NetEntity FromClient(EntityUid cuid) => CEntMan.GetNetEntity(cuid); + protected NetEntity? FromServer(EntityUid? suid) => SEntMan.GetNetEntity(suid); + protected NetEntity? FromClient(EntityUid? cuid) => CEntMan.GetNetEntity(cuid); + + protected NetCoordinates FromServer(EntityCoordinates scoords) => SEntMan.GetNetCoordinates(scoords); + protected NetCoordinates FromClient(EntityCoordinates ccoords) => CEntMan.GetNetCoordinates(ccoords); + protected NetCoordinates? FromServer(EntityCoordinates? scoords) => SEntMan.GetNetCoordinates(scoords); + protected NetCoordinates? FromClient(EntityCoordinates? ccoords) => CEntMan.GetNetCoordinates(ccoords); + #endregion #region Metadata & Transforms @@ -1433,6 +1638,7 @@ protected async Task Move(DirectionFlag dir, float seconds) protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid)); protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates; + protected NetCoordinates NetPosition(NetEntity uid) => FromServer(Position(uid)); #endregion } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index e523be2bfc3..5231055c694 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -3,6 +3,7 @@ using Content.Client.Construction; using Content.Client.Examine; using Content.Client.Gameplay; +using Content.Client.Interaction; using Content.IntegrationTests.Pair; using Content.Server.Hands.Systems; using Content.Server.Stack; @@ -24,6 +25,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; using Robust.UnitTesting; namespace Content.IntegrationTests.Tests.Interaction; @@ -39,10 +41,20 @@ namespace Content.IntegrationTests.Tests.Interaction; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public abstract partial class InteractionTest { + /// + /// The prototype that will be spawned for the player entity at . + /// This is not a full humanoid and only has one hand by default. + /// protected virtual string PlayerPrototype => "InteractionTestMob"; + /// + /// The map path to load for the integration test. + /// If null an empty map with a single 1x1 plating grid will be generated. + /// + protected virtual ResPath? TestMapPath => null; + protected TestPair Pair = default!; - protected TestMapData MapData => Pair.TestMap!; + protected TestMapData MapData = default!; protected RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server; protected RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client; @@ -123,10 +135,11 @@ public abstract partial class InteractionTest protected InteractionTestSystem CTestSystem = default!; protected ISawmill CLogger = default!; protected SharedUserInterfaceSystem CUiSys = default!; + protected DragDropSystem CDragDropSys = default!; // player components - protected HandsComponent Hands = default!; - protected DoAfterComponent DoAfters = default!; + protected HandsComponent? Hands; + protected DoAfterComponent? DoAfters; public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds; @@ -156,10 +169,13 @@ public abstract partial class InteractionTest - type: CombatMode "; + protected static PoolSettings Default => new() { Connected = true, Dirty = true }; + protected virtual PoolSettings Settings => Default; + [SetUp] public virtual async Task Setup() { - Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, Dirty = true }); + Pair = await PoolManager.GetServerClient(Settings, new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); // server dependencies SEntMan = Server.ResolveDependency(); @@ -194,9 +210,13 @@ public virtual async Task Setup() CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); CUiSys = CEntMan.System(); + CDragDropSys = CEntMan.System(); // Setup map. - await Pair.CreateTestMap(); + if (TestMapPath == null) + MapData = await Pair.CreateTestMap(); + else + MapData = await Pair.LoadTestMap(TestMapPath.Value); PlayerCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), MapData.MapUid)); TargetCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)), MapData.MapUid)); @@ -211,19 +231,19 @@ public virtual async Task Setup() ServerSession = sPlayerMan.GetSessionById(ClientSession.UserId); // Spawn player entity & attach - EntityUid? old = default; + NetEntity? old = default; await Server.WaitPost(() => { // Fuck you mind system I want an hour of my life back // Mind system is a time vampire SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind); - old = cPlayerMan.LocalEntity; + CEntMan.TryGetNetEntity(cPlayerMan.LocalEntity, out old); SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)); Player = SEntMan.GetNetEntity(SPlayer); Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer); - Hands = SEntMan.GetComponent(SPlayer); - DoAfters = SEntMan.GetComponent(SPlayer); + Hands = SEntMan.GetComponentOrNull(SPlayer); + DoAfters = SEntMan.GetComponentOrNull(SPlayer); }); // Check player got attached. @@ -234,8 +254,8 @@ await Server.WaitPost(() => // Delete old player entity. await Server.WaitPost(() => { - if (old != null) - SEntMan.DeleteEntity(old.Value); + if (SEntMan.TryGetEntity(old, out var uid)) + SEntMan.DeleteEntity(uid); }); // Change UI state to in-game. diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs new file mode 100644 index 00000000000..54417b6c0b5 --- /dev/null +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTestTests.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Map.Components; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Interaction; + +/// +/// Makes sure that interaction test helper methods are working as intended. +/// +public sealed class InteractionTestTests : InteractionTest +{ + protected override ResPath? TestMapPath => new("Maps/Test/empty.yml"); + + /// + /// Tests that map loading is working correctly. + /// + [Test] + public void MapLoadingTest() + { + // Make sure that there is only one grid. + var grids = SEntMan.AllEntities().ToList(); + Assert.That(grids, Has.Count.EqualTo(1), "Test map did not have exactly one grid."); + Assert.That(grids, Does.Contain(MapData.Grid), "MapData did not contain the loaded grid."); + + // Make sure we loaded the right map. + // This name is defined in empty.yml + Assert.That(SEntMan.GetComponent(MapData.MapUid).EntityName, Is.EqualTo("Empty Debug Map")); + } +} + diff --git a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs index d1535368736..d2f67e98308 100644 --- a/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs +++ b/Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; using Content.Server.Station.Systems; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.Internals; [TestFixture] [TestOf(typeof(InternalsSystem))] -public sealed class AutoInternalsTests +public sealed class AutoInternalsTests : GameTest { [Test] public async Task TestInternalsAutoActivateInSpaceForStationSpawn() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -31,14 +32,12 @@ await server.WaitAssertion(() => server.EntMan.DeleteEntity(dummy); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestInternalsAutoActivateInSpaceForEntitySpawn() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -55,8 +54,6 @@ await server.WaitAssertion(() => server.EntMan.DeleteEntity(dummy); }); - - await pair.CleanReturnAsync(); } [TestPrototypes] diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs index 39761ad0898..c841e233656 100644 --- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs +++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Stunnable; using Content.Shared.Inventory; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class InventoryHelpersTest + public sealed class InventoryHelpersTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -39,7 +40,7 @@ public sealed class InventoryHelpersTest [Test] public async Task SpawnItemInSlotTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntities = server.ResolveDependency(); @@ -87,8 +88,6 @@ await server.WaitAssertion(() => #pragma warning restore NUnit2045 sEntities.DeleteEntity(human); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs index c335f8d6c8a..f0a5c042372 100644 --- a/Content.IntegrationTests/Tests/Lathe/LatheTest.cs +++ b/Content.IntegrationTests/Tests/Lathe/LatheTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Lathe; using Content.Shared.Materials; using Content.Shared.Prototypes; @@ -11,12 +12,12 @@ namespace Content.IntegrationTests.Tests.Lathe; [TestFixture] -public sealed class LatheTest +public sealed class LatheTest : GameTest { [Test] public async Task TestLatheRecipeIngredientsFitLathe() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapData = await pair.CreateTestMap(); @@ -111,14 +112,12 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task AllLatheRecipesValidTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var proto = server.ProtoMan; @@ -131,7 +130,5 @@ public async Task AllLatheRecipesValidTest() Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs index b75b81ab3ca..bd35c1f08e4 100644 --- a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs +++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -11,12 +12,12 @@ namespace Content.IntegrationTests.Tests.Linter; /// Verify that the yaml linter successfully validates static fields /// [TestFixture] -public sealed class StaticFieldValidationTest +public sealed class StaticFieldValidationTest : GameTest { [Test] public async Task TestStaticFieldValidation() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoMan = pair.Server.ProtoMan; var protos = new Dictionary>(); @@ -49,8 +50,6 @@ public async Task TestStaticFieldValidation() Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos), Has.Count.EqualTo(2)); - - await pair.CleanReturnAsync(); } [TestPrototypes] diff --git a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs index 60501a781fc..fb48797616d 100644 --- a/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs +++ b/Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs @@ -1,114 +1,114 @@ using Content.Client.Lobby; +using Content.IntegrationTests.Fixtures; using Content.Server.Preferences.Managers; +using Content.Shared.Humanoid; using Content.Shared.Preferences; using Robust.Client.State; -using Robust.Shared.Network; -namespace Content.IntegrationTests.Tests.Lobby +namespace Content.IntegrationTests.Tests.Lobby; + +[TestFixture] +[TestOf(typeof(ClientPreferencesManager))] +[TestOf(typeof(ServerPreferencesManager))] +public sealed class CharacterCreationTest : GameTest { - [TestFixture] - [TestOf(typeof(ClientPreferencesManager))] - [TestOf(typeof(ServerPreferencesManager))] - public sealed class CharacterCreationTest + public override PoolSettings PoolSettings => new() { InLobby = true }; + + [Test] + public async Task CreateDeleteCreateTest() { - [Test] - public async Task CreateDeleteCreateTest() + var pair = Pair; + var server = pair.Server; + var client = pair.Client; + var user = pair.Client.User!.Value; + var clientPrefManager = client.Resolve(); + var serverPrefManager = server.Resolve(); + + Assert.That(client.Resolve().CurrentState, Is.TypeOf()); + await client.WaitPost(() => clientPrefManager.SelectCharacter(0)); + await pair.RunTicksSync(5); + + var clientCharacters = clientPrefManager.Preferences?.Characters; + Assert.That(clientCharacters, Is.Not.Null); + Assert.That(clientCharacters, Has.Count.EqualTo(1)); + + HumanoidCharacterProfile profile = null; + await client.WaitPost(() => { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); - var server = pair.Server; - var client = pair.Client; - - var clientNetManager = client.ResolveDependency(); - var clientStateManager = client.ResolveDependency(); - var clientPrefManager = client.ResolveDependency(); - - var serverPrefManager = server.ResolveDependency(); - - - // Need to run them in sync to receive the messages. - await pair.RunTicksSync(1); - - await PoolManager.WaitUntil(client, () => clientStateManager.CurrentState is LobbyState, 600); - - Assert.That(clientNetManager.ServerChannel, Is.Not.Null); - - var clientNetId = clientNetManager.ServerChannel.UserId; - HumanoidCharacterProfile profile = null; - - await client.WaitAssertion(() => - { - clientPrefManager.SelectCharacter(0); - - var clientCharacters = clientPrefManager.Preferences?.Characters; - Assert.That(clientCharacters, Is.Not.Null); - Assert.Multiple(() => - { - Assert.That(clientCharacters, Has.Count.EqualTo(1)); - - Assert.That(clientStateManager.CurrentState, Is.TypeOf()); - }); - - profile = HumanoidCharacterProfile.Random(); - clientPrefManager.CreateCharacter(profile); + profile = HumanoidCharacterProfile.Random(); + clientPrefManager.CreateCharacter(profile); + }); + await pair.RunTicksSync(5); - clientCharacters = clientPrefManager.Preferences?.Characters; + clientCharacters = clientPrefManager.Preferences?.Characters; + Assert.That(clientCharacters, Is.Not.Null); + Assert.That(clientCharacters, Has.Count.EqualTo(2)); + AssertEqual(clientCharacters[1], profile); - Assert.That(clientCharacters, Is.Not.Null); - Assert.That(clientCharacters, Has.Count.EqualTo(2)); - Assert.That(clientCharacters[1].MemberwiseEquals(profile)); - }); + await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60); - await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60); + var serverCharacters = serverPrefManager.GetPreferences(user).Characters; + Assert.That(serverCharacters, Has.Count.EqualTo(2)); + AssertEqual(serverCharacters[1], profile); - await server.WaitAssertion(() => - { - var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters; + await client.WaitAssertion(() => clientPrefManager.DeleteCharacter(1)); + await pair.RunTicksSync(5); + Assert.That(clientPrefManager.Preferences?.Characters.Count, Is.EqualTo(1)); + await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 1, maxTicks: 60); + Assert.That(serverPrefManager.GetPreferences(user).Characters.Count, Is.EqualTo(1)); - Assert.That(serverCharacters, Has.Count.EqualTo(2)); - Assert.That(serverCharacters[1].MemberwiseEquals(profile)); - }); + await client.WaitIdleAsync(); - await client.WaitAssertion(() => - { - clientPrefManager.DeleteCharacter(1); - - var clientCharacters = clientPrefManager.Preferences?.Characters.Count; - Assert.That(clientCharacters, Is.EqualTo(1)); - }); - - await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 1, maxTicks: 60); - - await server.WaitAssertion(() => - { - var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters.Count; - Assert.That(serverCharacters, Is.EqualTo(1)); - }); - - await client.WaitIdleAsync(); - - await client.WaitAssertion(() => - { - profile = HumanoidCharacterProfile.Random(); - - clientPrefManager.CreateCharacter(profile); - - var clientCharacters = clientPrefManager.Preferences?.Characters; + await client.WaitAssertion(() => + { + profile = HumanoidCharacterProfile.Random(); + clientPrefManager.CreateCharacter(profile); + }); + await pair.RunTicksSync(5); + + clientCharacters = clientPrefManager.Preferences?.Characters; + Assert.That(clientCharacters, Is.Not.Null); + Assert.That(clientCharacters, Has.Count.EqualTo(2)); + AssertEqual(clientCharacters[1], profile); + + await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(user).Characters.Count == 2, maxTicks: 60); + serverCharacters = serverPrefManager.GetPreferences(user).Characters; + Assert.That(serverCharacters, Has.Count.EqualTo(2)); + AssertEqual(serverCharacters[1], profile); + } - Assert.That(clientCharacters, Is.Not.Null); - Assert.That(clientCharacters, Has.Count.EqualTo(2)); - Assert.That(clientCharacters[1].MemberwiseEquals(profile)); - }); + private void AssertEqual(HumanoidCharacterProfile a, HumanoidCharacterProfile b) + { + if (a.MemberwiseEquals(b)) + return; - await PoolManager.WaitUntil(server, () => serverPrefManager.GetPreferences(clientNetId).Characters.Count == 2, maxTicks: 60); + Assert.Multiple(() => + { + Assert.That(a.Name, Is.EqualTo(b.Name)); + Assert.That(a.Age, Is.EqualTo(b.Age)); + Assert.That(a.Sex, Is.EqualTo(b.Sex)); + Assert.That(a.Gender, Is.EqualTo(b.Gender)); + Assert.That(a.Species, Is.EqualTo(b.Species)); + Assert.That(a.PreferenceUnavailable, Is.EqualTo(b.PreferenceUnavailable)); + Assert.That(a.SpawnPriority, Is.EqualTo(b.SpawnPriority)); + Assert.That(a.FlavorText, Is.EqualTo(b.FlavorText)); + Assert.That(a.JobPriorities, Is.EquivalentTo(b.JobPriorities)); + Assert.That(a.AntagPreferences, Is.EquivalentTo(b.AntagPreferences)); + Assert.That(a.TraitPreferences, Is.EquivalentTo(b.TraitPreferences)); + Assert.That(a.Loadouts, Is.EquivalentTo(b.Loadouts)); + AssertEqual(a.Appearance, b.Appearance); + Assert.Fail("Profile not equal"); + }); + } - await server.WaitAssertion(() => - { - var serverCharacters = serverPrefManager.GetPreferences(clientNetId).Characters; + private void AssertEqual(HumanoidCharacterAppearance a, HumanoidCharacterAppearance b) + { + if (a.Equals(b)) + return; - Assert.That(serverCharacters, Has.Count.EqualTo(2)); - Assert.That(serverCharacters[1].MemberwiseEquals(profile)); - }); - await pair.CleanReturnAsync(); - } + Assert.That(a.EyeColor, Is.EqualTo(b.EyeColor)); + Assert.That(a.SkinColor, Is.EqualTo(b.SkinColor)); + Assert.That(a.Markings, Is.EquivalentTo(b.Markings)); + Assert.Fail("Appearance not equal"); } } diff --git a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs index 3dc2075887a..e59bc269ad7 100644 --- a/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs +++ b/Content.IntegrationTests/Tests/Lobby/ServerReloginTest.cs @@ -1,20 +1,23 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.CCVar; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Lobby; -public sealed class ServerReloginTest +public sealed class ServerReloginTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + Connected = true, + DummyTicker = false + }; + [Test] public async Task Relogin() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; var originalMaxPlayers = 0; @@ -62,7 +65,5 @@ await server.WaitAssertion(() => //Put the cvar back, so other tests can still use this server serverConfig.SetCVar(CCVars.SoftMaxPlayers, originalMaxPlayers); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs b/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs index 69d44fd08b3..1b8fa165432 100644 --- a/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs +++ b/Content.IntegrationTests/Tests/Localization/EntityPrototypeLocalizationTest.cs @@ -1,9 +1,10 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.Localization; using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Localization; -public sealed class EntityPrototypeLocalizationTest +public sealed class EntityPrototypeLocalizationTest : GameTest { /// /// An explanation of why LocIds should not be used for entity prototype names/descriptions. @@ -18,7 +19,7 @@ public sealed class EntityPrototypeLocalizationTest [Test] public async Task TestNoManualEntityLocStrings() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; var locMan = server.ResolveDependency(); @@ -44,7 +45,5 @@ public async Task TestNoManualEntityLocStrings() } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs index 05f98c3d198..14aeaf648cc 100644 --- a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Dataset; using Robust.Shared.Localization; using Robust.Shared.Prototypes; @@ -6,12 +7,12 @@ namespace Content.IntegrationTests.Tests.Localization; [TestFixture] -public sealed class LocalizedDatasetPrototypeTest +public sealed class LocalizedDatasetPrototypeTest : GameTest { [Test] public async Task ValidProtoIdsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -36,7 +37,5 @@ public async Task ValidProtoIdsTest() Assert.That(localizationMan.HasString(nextId), Is.False, $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but a localized string exists with ID {nextId}! Does count need to be raised?"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/LogErrorTest.cs b/Content.IntegrationTests/Tests/LogErrorTest.cs deleted file mode 100644 index b8d8dba7cec..00000000000 --- a/Content.IntegrationTests/Tests/LogErrorTest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Robust.Shared.Configuration; -using Robust.Shared.Log; -using Robust.UnitTesting; - -namespace Content.IntegrationTests.Tests; - -public sealed class LogErrorTest -{ - /// - /// This test ensures that error logs cause tests to fail. - /// - [Test] - public async Task TestLogErrorCausesTestFailure() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); - var server = pair.Server; - var client = pair.Client; - - var cfg = server.ResolveDependency(); - var logmill = server.ResolveDependency().RootSawmill; - - // Default cvar is properly configured - Assert.That(cfg.GetCVar(RTCVars.FailureLogLevel), Is.EqualTo(LogLevel.Error)); - - // Warnings don't cause tests to fail. - await server.WaitPost(() => logmill.Warning("test")); - - // But errors do - await server.WaitPost(() => Assert.Throws(() => logmill.Error("test"))); - await client.WaitPost(() => Assert.Throws(() => logmill.Error("test"))); - - await pair.CleanReturnAsync(); - } -} diff --git a/Content.IntegrationTests/Tests/MachineBoardTest.cs b/Content.IntegrationTests/Tests/MachineBoardTest.cs index e1533bbb8d7..9554357eced 100644 --- a/Content.IntegrationTests/Tests/MachineBoardTest.cs +++ b/Content.IntegrationTests/Tests/MachineBoardTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Construction.Components; using Content.Shared.Construction.Components; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests; -public sealed class MachineBoardTest +public sealed class MachineBoardTest : GameTest { /// /// A list of machine boards that can be ignored by this test. @@ -32,7 +33,7 @@ public sealed class MachineBoardTest [Test] public async Task TestMachineBoardHasValidMachine() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -60,8 +61,6 @@ await server.WaitAssertion(() => }); } }); - - await pair.CleanReturnAsync(); } /// @@ -71,7 +70,7 @@ await server.WaitAssertion(() => [Test] public async Task TestComputerBoardHasValidComputer() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ResolveDependency(); @@ -100,8 +99,6 @@ await server.WaitAssertion(() => }); } }); - - await pair.CleanReturnAsync(); } /// @@ -111,7 +108,7 @@ await server.WaitAssertion(() => [Test] public async Task TestValidateBoardComponentRequirements() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -136,7 +133,5 @@ await server.WaitAssertion(() => }); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs index 6d48a668a55..43fea6d6c0c 100644 --- a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs +++ b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Content.Client.Weapons.Ranged.Components; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -9,12 +10,12 @@ namespace Content.IntegrationTests.Tests; /// Tests all entity prototypes with the MagazineVisualsComponent. /// [TestFixture] -public sealed class MagazineVisualsSpriteTest +public sealed class MagazineVisualsSpriteTest : GameTest { [Test] public async Task MagazineVisualsSpritesExist() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var client = pair.Client; var toTest = new List<(int, string)>(); var protos = pair.GetPrototypesWithComponent(); @@ -67,7 +68,5 @@ await client.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs index be8bad229b4..0bf637cf194 100644 --- a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs +++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -5,15 +6,18 @@ namespace Content.IntegrationTests.Tests.Mapping; [TestFixture] -public sealed class MappingTests +public sealed class MappingTests : GameTest { + public override PoolSettings PoolSettings => + new() { Dirty = true, Connected = true, DummyTicker = false }; + /// /// Checks that the mapping command creates paused & uninitialized maps. /// [Test] public async Task MappingTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true, Connected = true, DummyTicker = false }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; @@ -97,6 +101,5 @@ await server.WaitPost(() => Assert.That(server.MetaData(ent).EntityPaused, Is.True); await server.WaitPost(() => entMan.DeleteEntity(map)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/MappingEditorTest.cs b/Content.IntegrationTests/Tests/MappingEditorTest.cs index bd930aef9ee..987fe7ad735 100644 --- a/Content.IntegrationTests/Tests/MappingEditorTest.cs +++ b/Content.IntegrationTests/Tests/MappingEditorTest.cs @@ -1,19 +1,17 @@ using Content.Client.Gameplay; using Content.Client.Mapping; +using Content.IntegrationTests.Fixtures; using Robust.Client.State; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class MappingEditorTest +public sealed class MappingEditorTest : GameTest { [Test] public async Task StopHardCodingWidgetsJesusChristTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true - }); + var pair = Pair; var client = pair.Client; var state = client.ResolveDependency(); @@ -35,7 +33,5 @@ await client.WaitPost(() => state.RequestStateChange(); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs b/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs new file mode 100644 index 00000000000..af5c6c20183 --- /dev/null +++ b/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs @@ -0,0 +1,229 @@ +using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Markings; + +[TestFixture] +[TestOf(typeof(MarkingManager))] +public sealed class MarkingManagerTests : GameTest +{ + [TestPrototypes] + private const string Prototypes = @" +- type: markingsGroup + id: Testing + +- type: markingsGroup + id: TestingOther + +- type: markingsGroup + id: TestingOptionalEyes + limits: + enum.HumanoidVisualLayers.Eyes: + limit: 1 + required: false + +- type: markingsGroup + id: TestingRequiredEyes + limits: + enum.HumanoidVisualLayers.Eyes: + limit: 1 + required: true + default: [ EyesMarking ] + +- type: marking + id: SingleColorMarking + bodyPart: Eyes + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] + coloring: + default: + type: + !type:EyeColoring + +- type: marking + id: MenOnlyMarking + bodyPart: Eyes + sexRestriction: Male + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] + +- type: marking + id: TestingOnlyMarking + bodyPart: Eyes + groupWhitelist: [ Testing ] + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] + +- type: marking + id: TestingMenOnlyMarking + bodyPart: Eyes + sexRestriction: Male + groupWhitelist: [ Testing ] + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] + +- type: marking + id: EyesMarking + bodyPart: Eyes + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] + +- type: marking + id: ChestMarking + bodyPart: Chest + sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }] +"; + + [Test] + public async Task HairConvesion() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var markingManager = server.ResolveDependency(); + + var markings = new List() { new("HumanHairLongBedhead2", new List() { Color.Red }) }; + + var converted = markingManager.ConvertMarkings(markings, "Human"); + + Assert.That(converted, Does.ContainKey(new ProtoId("Head"))); + Assert.That(converted["Head"], Does.ContainKey(HumanoidVisualLayers.Hair)); + var hairMarkings = converted["Head"][HumanoidVisualLayers.Hair]; + Assert.That(hairMarkings, Has.Count.EqualTo(1)); + Assert.That(hairMarkings[0].MarkingId, Is.EqualTo("HumanHairLongBedhead2")); + Assert.That(hairMarkings[0].MarkingColors[0], Is.EqualTo(Color.Red)); + }); + } + + [Test] + public async Task LimitsFilling() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var markingManager = server.ResolveDependency(); + var dict = new Dictionary>(); + + markingManager.EnsureValidLimits(dict, "TestingRequiredEyes", new() { HumanoidVisualLayers.Eyes }, null, null); + Assert.That(dict, Does.ContainKey(HumanoidVisualLayers.Eyes)); + Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); + Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("EyesMarking")); + }); + } + + [Test] + public async Task LimitsTruncations() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var markingManager = server.ResolveDependency(); + var dict = new Dictionary>() + { + [HumanoidVisualLayers.Eyes] = new() + { + new("EyesMarking", 0), + new("MenOnlyMarking", 0), + }, + }; + + markingManager.EnsureValidLimits(dict, "TestingOptionalEyes", new() { HumanoidVisualLayers.Eyes }, null, null); + Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); + Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking")); + }); + } + + [Test] + public async Task EnsureValidGroupAndSex() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var markingManager = server.ResolveDependency(); + var dictFactory = static () => new Dictionary>() + { + [HumanoidVisualLayers.Eyes] = new() + { + new("MenOnlyMarking", 0), + new("TestingOnlyMarking", 0), + new("TestingMenOnlyMarking", 0), + } + }; + + var menMarkings = dictFactory(); + markingManager.EnsureValidGroupAndSex(menMarkings, "TestingOther", Sex.Male); + + Assert.That(menMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); + Assert.That(menMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking")); + + var testingMarkings = dictFactory(); + markingManager.EnsureValidGroupAndSex(testingMarkings, "Testing", Sex.Female); + + Assert.That(testingMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1)); + Assert.That(testingMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("TestingOnlyMarking")); + + var testingMenMarkings = dictFactory(); + markingManager.EnsureValidGroupAndSex(testingMenMarkings, "Testing", Sex.Male); + + Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(3)); + Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking")); + Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][1].MarkingId, Is.EqualTo("TestingOnlyMarking")); + Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][2].MarkingId, Is.EqualTo("TestingMenOnlyMarking")); + }); + } + + [Test] + public async Task EnsureValidColors() + { + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + await server.WaitAssertion(() => + { + var markingManager = server.ResolveDependency(); + + var dict = new Dictionary>() + { + [HumanoidVisualLayers.Eyes] = new() + { + new("SingleColorMarking", 0), + new("SingleColorMarking", new List() { Color.Red }), + new("SingleColorMarking", 2), + new("SingleColorMarking", new List() { Color.Green }), + } + }; + + markingManager.EnsureValidColors(dict); + + var eyeMarkings = dict[HumanoidVisualLayers.Eyes]; + + // ensure all colors are the correct length + Assert.That(eyeMarkings[0].MarkingColors, Has.Count.EqualTo(1)); + Assert.That(eyeMarkings[1].MarkingColors, Has.Count.EqualTo(1)); + Assert.That(eyeMarkings[2].MarkingColors, Has.Count.EqualTo(1)); + Assert.That(eyeMarkings[3].MarkingColors, Has.Count.EqualTo(1)); + + // and make sure we didn't shuffle our colors around + Assert.That(eyeMarkings[1].MarkingColors[0], Is.EqualTo(Color.Red)); + Assert.That(eyeMarkings[3].MarkingColors[0], Is.EqualTo(Color.Green)); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs b/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs new file mode 100644 index 00000000000..026d2ecf3b0 --- /dev/null +++ b/Content.IntegrationTests/Tests/Markings/MarkingsViewModelTests.cs @@ -0,0 +1,136 @@ +using Content.Client.Humanoid; +using Content.IntegrationTests.Pair; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Commands; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.UnitTesting; + +namespace Content.IntegrationTests.Tests.Markings; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class MarkingTestAttribute : TestAttribute, IWrapTestMethod +{ + private sealed class MarkingTestCommand(TestCommand inner) : DelegatingTestCommand(inner) + { + public override TestResult Execute(TestExecutionContext context) + { + var fixture = innerCommand.Test.Fixture as MarkingsViewModelTests; + fixture!.Client.WaitAssertion(() => + { + context.CurrentResult = innerCommand.Execute(context); + }) + .Wait(); + return context.CurrentResult; + } + } + + public TestCommand Wrap(TestCommand command) + { + return new MarkingTestCommand(command); + } +} + +[TestFixture] +[TestOf(typeof(MarkingsViewModel))] +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public sealed class MarkingsViewModelTests +{ + public ProtoId TestSpecies = "Moth"; + public ProtoId Head = "Head"; + public ProtoId Torso = "Torso"; + public ProtoId MothAntennasCharred = "MothAntennasCharred"; + public ProtoId MothChestCharred = "MothChestCharred"; + public ProtoId MothChestDeathhead = "MothChestDeathhead"; + public ProtoId MothChestFan = "MothChestFan"; + public ProtoId LizardHornsCurled = "LizardHornsCurled"; + public ProtoId MothAntennasDefault = "MothAntennasDefault"; + + public TestPair Pair = default!; + public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client; + public MarkingsViewModel Model = default!; + public MarkingManager Manager = default!; + + [SetUp] + public async Task SetUp() + { + Pair = await PoolManager.GetServerClient(testContext: new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out)); + await Client.WaitPost(() => + { + Model = new MarkingsViewModel(); + Manager = Client.ResolveDependency(); + + Model.OrganData = Manager.GetMarkingData(TestSpecies); + Model.OrganProfileData = Manager.GetProfileData(TestSpecies, Sex.Male, Color.White, Color.White); + Model.ValidateMarkings(); + }); + } + + [TearDown] + public async Task TearDown() + { + await Pair.CleanReturnAsync(); + } + + [MarkingTest] + public void MarkingSelection() + { + Assert.That(Model.TrySelectMarking(Head, HumanoidVisualLayers.HeadTop, MothAntennasCharred), Is.True, "You should be able to select a marking in a limit-1 category if another marking is selected"); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)!, Has.Count.EqualTo(1), "The markings model should respect the limits when selecting markings"); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)![0].MarkingId, Is.EqualTo(MothAntennasCharred), "The markings model should have replaced the default marking with charred antennae"); + + Assert.That(Model.TrySelectMarking(Torso, HumanoidVisualLayers.Chest, MothChestCharred), Is.True); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)!, Has.Count.EqualTo(1)); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)![0].MarkingId, Is.EqualTo(MothChestCharred)); + + Assert.That(Model.TrySelectMarking(Torso, HumanoidVisualLayers.Chest, MothChestDeathhead), Is.True); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)!, Has.Count.EqualTo(2)); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)![1].MarkingId, Is.EqualTo(MothChestDeathhead)); + + Assert.That(Model.TrySelectMarking(Torso, HumanoidVisualLayers.Chest, MothChestFan), Is.False); + Assert.That(Model.TrySelectMarking(Head, HumanoidVisualLayers.HeadTop, LizardHornsCurled), Is.False); + + Model.EnforceLimits = false; + Assert.That(Model.TrySelectMarking(Torso, HumanoidVisualLayers.Chest, MothChestFan), Is.True); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)!, Has.Count.EqualTo(3)); + Assert.That(Model.SelectedMarkings(Torso, HumanoidVisualLayers.Chest)![2].MarkingId, Is.EqualTo(MothChestFan)); + } + + [MarkingTest] + public void MarkingDeselection() + { + Assert.That(Model.TryDeselectMarking(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault), Is.False); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)!, Has.Count.EqualTo(1)); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)![0].MarkingId, Is.EqualTo(MothAntennasDefault)); + + Model.EnforceLimits = false; + + Assert.That(Model.TryDeselectMarking(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault), Is.True); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)!, Has.Count.EqualTo(0)); + } + + [MarkingTest] + public void MarkingColors() + { + Model.TrySetMarkingColor(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault, 0, Color.AliceBlue); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)![0].MarkingColors[0], Is.EqualTo(Color.AliceBlue)); + } + + [MarkingTest] + public void MarkingColorRestoration() + { + Model.EnforceLimits = false; + Model.TrySetMarkingColor(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault, 0, Color.AliceBlue); + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)![0].MarkingColors[0], Is.EqualTo(Color.AliceBlue)); + + Assert.That(Model.TryDeselectMarking(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault), Is.True); + Assert.That(Model.TrySelectMarking(Head, HumanoidVisualLayers.HeadTop, MothAntennasDefault), Is.True); + + Assert.That(Model.SelectedMarkings(Head, HumanoidVisualLayers.HeadTop)![0].MarkingColors[0], Is.EqualTo(Color.AliceBlue)); + } +} diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs index 082baf7e643..460146357ca 100644 --- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs +++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Cargo.Systems; using Content.Server.Construction.Completions; using Content.Server.Construction.Components; @@ -26,27 +27,17 @@ namespace Content.IntegrationTests.Tests; /// create them. /// [TestFixture] -public sealed class MaterialArbitrageTest +public sealed class MaterialArbitrageTest : GameTest { - // These recipes are currently broken and need fixing. You should not be adding to these sets. - private readonly HashSet _destructionArbitrageIgnore = - [ - "BaseChemistryEmptyVial", "DrinkShotGlass", "SodiumLightTube", "DrinkGlassCoupeShaped", - "LedLightBulb", "ExteriorLightTube", "LightTube", "DrinkGlass", "DimLightBulb", "LightBulb", "LedLightTube", - "SheetRGlass1", "ChemistryEmptyBottle01", "WarmLightBulb", - ]; - - private readonly HashSet _compositionArbitrageIgnore = - [ - "FoodPlateSmall", "AirTank", "FoodPlateTin", "FoodPlateMuffinTin", "WeaponCapacitorRechargerCircuitboard", - "WeaponCapacitorRechargerCircuitboard", "BorgChargerCircuitboard", "BorgChargerCircuitboard", "FoodPlate", - "CellRechargerCircuitboard", "CellRechargerCircuitboard", - ]; + // These sets are for selectively excluding recipes from arbitrage. + // You should NOT be adding to these. They exist here for downstreams and potential future issues. + private readonly HashSet _destructionArbitrageIgnore = []; + private readonly HashSet _compositionArbitrageIgnore = []; [Test] public async Task NoMaterialArbitrage() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -449,7 +440,6 @@ await Assert.MultipleAsync(async () => }); await server.WaitPost(() => mapSystem.DeleteMap(testMap.MapId)); - await pair.CleanReturnAsync(); async Task GetSpawnedPrice(Dictionary ents) { @@ -469,7 +459,8 @@ async Task GetPrice(string id) await server.WaitPost(() => { var ent = entManager.SpawnEntity(id, testMap.GridCoords); - stackSys.SetCount(ent, 1); + if (entManager.TryGetComponent(ent, out var stackComp)) + stackSys.SetCount((ent, stackComp), 1); priceCache[id] = price = pricing.GetPrice(ent, false); entManager.DeleteEntity(ent); }); diff --git a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs index 30800f358e2..43c7dab2b0b 100644 --- a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs +++ b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Server.Stack; using Content.Shared.Stacks; using Content.Shared.Materials; @@ -14,12 +15,12 @@ namespace Content.IntegrationTests.Tests.Materials [TestFixture] [TestOf(typeof(StackSystem))] [TestOf(typeof(MaterialPrototype))] - public sealed class MaterialPrototypeSpawnsStackMaterialTest + public sealed class MaterialPrototypeSpawnsStackMaterialTest : GameTest { [Test] public async Task MaterialPrototypeSpawnsStackMaterial() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -54,14 +55,12 @@ await server.WaitAssertion(() => $"{proto.ID} material has no stack prototype"); if (stackProto != null) - Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn)); + Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn.Id)); } }); mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs new file mode 100644 index 00000000000..18361a9d901 --- /dev/null +++ b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs @@ -0,0 +1,102 @@ +#nullable enable +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Medical; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Medical; + +/// +/// Tests for defibrilators. +/// +[TestOf(typeof(DefibrillatorComponent))] +public sealed class DefibrillatorTest : InteractionTest +{ + // We need two hands to use a defbrillator. + protected override string PlayerPrototype => "MobHuman"; + + private static readonly EntProtoId DefibrillatorProtoId = "Defibrillator"; + private static readonly EntProtoId TargetProtoId = "MobHuman"; + private static readonly ProtoId BluntDamageTypeId = "Blunt"; + + /// + /// Kills a target mob, heals them and then revives them with a defibrillator. + /// + [Test] + public async Task KillAndReviveTest() + { + var damageableSystem = SEntMan.System(); + var mobThresholdsSystem = SEntMan.System(); + + // Don't let the player and target suffocate. + await AddAtmosphere(); + + await SpawnTarget(TargetProtoId); + + var targetMobState = Comp(); + var targetDamageable = Comp(); + + // Check that the target has no damage and is not crit or dead. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Alive), "Target mob was not alive when spawned."); + Assert.That(damageableSystem.GetTotalDamage(STarget!.Value), Is.EqualTo(FixedPoint2.Zero), "Target mob was damaged when spawned."); + }); + + // Get the damage needed to kill or crit the target. + var critThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Critical); + var deathThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Dead); + var critDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), (critThreshold + deathThreshold) / 2); + var deathDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), deathThreshold); + + // Kill the target by applying blunt damage. + await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), deathDamage)); + await RunTicks(3); + + // Check that the target is dead. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob did not die from deadly damage amount."); + Assert.That(damageableSystem.GetTotalDamage(STarget!.Value), Is.EqualTo(deathThreshold), "Target mob had the wrong total damage amount after being killed."); + }); + + // Spawn a defib and activate it. + var defib = await PlaceInHands(DefibrillatorProtoId, enableToggleable: true); + var cooldown = Comp(defib).ZapDelay; + + // Wait for the cooldown. + await RunSeconds((float)cooldown.TotalSeconds); + + // ZAP! + await Interact(); + + // Check that the target is still dead since it is over the crit threshold. + // And it should have taken some extra damage. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob was revived despite being over the death damage threshold."); + Assert.That(damageableSystem.GetTotalDamage(STarget!.Value), Is.GreaterThan(deathThreshold), "Target mob did not take damage from being defibrillated."); + }); + + // Set the damage halfway between the crit and death thresholds so that the target can be revived. + await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), critDamage)); + await RunTicks(3); + + // Check that the target is still dead. + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob revived on its own."); + + // ZAP! + await RunSeconds((float)cooldown.TotalSeconds); + await Interact(); + + // The target should be revived, but in crit. + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Critical), "Target mob was not revived from being defibrillated."); + } +} diff --git a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs index 32fcf9c1ade..7369fece356 100644 --- a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs +++ b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Shared.Ghost; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class GhostRoleTests +public sealed class GhostRoleTests : GameTest { private const string GhostRoleProtoId = "GhostRoleTestEntity"; private const string TestMobProtoId = "GhostRoleTestMob"; @@ -33,6 +34,13 @@ public sealed class GhostRoleTests - type: MobState # MobState is required for correct determination of if the player can return to body or not """; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + DummyTicker = false, + Connected = true + }; + /// /// This is a simple test that just checks if a player can take a ghost role and then regain control of their /// original entity without encountering errors. @@ -43,12 +51,7 @@ public async Task TakeRoleAndReturn(bool adminGhost) { var ghostCommand = adminGhost ? "aghost" : "ghost"; - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true - }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -210,7 +213,6 @@ await server.WaitPost(() => if (!adminGhost) { // End of the normal player ghost role test - await pair.CleanReturnAsync(); return; } @@ -242,7 +244,5 @@ await server.WaitPost(() => // Check that there is are no lingereing ghosts Assert.That(entMan.Count(), Is.Zero); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs index 3a860267e55..26991c08add 100644 --- a/Content.IntegrationTests/Tests/Minds/GhostTests.cs +++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Pair; using Content.Shared.Ghost; using Content.Shared.Mind; @@ -12,7 +13,7 @@ namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class GhostTests +public sealed class GhostTests : GameTest { private struct GhostTestData { @@ -45,17 +46,20 @@ public GhostTestData() } } + // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult. + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + private async Task SetupData() { var data = new GhostTestData { - // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult. - Pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }) + // ..Just use the gametest pair, please. + Pair = Pair, }; data.SEntMan = data.Pair.Server.ResolveDependency(); @@ -126,8 +130,6 @@ public async Task TestGridGhostOnDelete() // Ensure the position is the same var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; Assert.That(ghostPosition, Is.EqualTo(oldPosition)); - - await data.Pair.CleanReturnAsync(); } /// @@ -154,8 +156,6 @@ public async Task TestGridGhostOnQueueDelete() // Ensure the position is the same var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; Assert.That(ghostPosition, Is.EqualTo(oldPosition)); - - await data.Pair.CleanReturnAsync(); } [Test] @@ -168,10 +168,6 @@ public async Task TestGhostGridNotTerminating() // Delete the grid await data.Server.WaitPost(() => data.SEntMan.DeleteEntity(data.MapData.Grid.Owner)); }); - - await data.Pair.RunTicksSync(5); - - await data.Pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs index ad4ddc26125..c207e7b4894 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs @@ -11,13 +11,7 @@ public sealed partial class MindTests [Test] public async Task DeleteAllThenGhost() { - var settings = new PoolSettings - { - Dirty = true, - DummyTicker = false, - Connected = true - }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; // Client is connected with a valid entity & mind Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity)); @@ -56,7 +50,5 @@ public async Task DeleteAllThenGhost() Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind)); var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value); Assert.That(xform.MapID, Is.EqualTo(mapId)); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs index 6f331888138..cf87b3ea76a 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs @@ -23,7 +23,7 @@ public sealed partial class MindTests [Test] public async Task TestDeleteVisiting() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -67,15 +67,13 @@ await server.WaitAssertion(() => // This used to throw so make sure it doesn't. await server.WaitPost(() => entMan.DeleteEntity(mind.OwnedEntity!.Value)); await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } // this is a variant of TestGhostOnDelete that just deletes the whole map. [Test] public async Task TestGhostOnDeleteMap() { - await using var pair = await SetupPair(dirty: true); + var pair = await SetupPair(dirty: true); var server = pair.Server; var testMap = await pair.CreateTestMap(); var testMap2 = await pair.CreateTestMap(); @@ -112,13 +110,11 @@ await server.WaitAssertion(() => Assert.That(entMan.EntityExists(attachedEntity), Is.True); Assert.That(attachedEntity, Is.Not.EqualTo(playerEnt)); Assert.That(entMan.HasComponent(attachedEntity)); - var transform = entMan.GetComponent(attachedEntity.Value); + var transform = entMan.GetComponent(attachedEntity!.Value); Assert.That(transform.MapID, Is.Not.EqualTo(MapId.Nullspace)); Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId)); #pragma warning restore NUnit2045 }); - - await pair.CleanReturnAsync(); } /// @@ -130,7 +126,7 @@ await server.WaitAssertion(() => public async Task TestGhostOnDelete() { // Client is needed to spawn session - await using var pair = await SetupPair(dirty: true); + var pair = await SetupPair(dirty: true); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -145,8 +141,6 @@ public async Task TestGhostOnDelete() await pair.RunTicksSync(5); Assert.That(entMan.HasComponent(player.AttachedEntity), "Player did not become a ghost"); - - await pair.CleanReturnAsync(); } /// @@ -162,7 +156,7 @@ public async Task TestGhostOnDelete() public async Task TestOriginalDeletedWhileGhostingKeepsGhost() { // Client is needed to spawn session - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -175,7 +169,7 @@ public async Task TestOriginalDeletedWhileGhostingKeepsGhost() Assert.That(player.AttachedEntity, Is.Not.Null); Assert.That(entMan.EntityExists(player.AttachedEntity)); #pragma warning restore NUnit2045 - var originalEntity = player.AttachedEntity.Value; + var originalEntity = player.AttachedEntity!.Value; EntityUid ghost = default!; await server.WaitAssertion(() => @@ -207,8 +201,6 @@ await server.WaitAssertion(() => Assert.That(mind.Comp.VisitingEntity, Is.Null); Assert.That(mind.Comp.OwnedEntity, Is.EqualTo(ghost)); }); - - await pair.CleanReturnAsync(); } /// @@ -220,7 +212,7 @@ await server.WaitAssertion(() => [Test] public async Task TestGhostToAghost() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); var playerMan = server.ResolveDependency(); @@ -248,10 +240,8 @@ public async Task TestGhostToAghost() var mindId = player.ContentData()?.Mind; Assert.That(mindId, Is.Not.Null); - var mind = entMan.GetComponent(mindId.Value); + var mind = entMan.GetComponent(mindId!.Value); Assert.That(mind.VisitingEntity, Is.Null); - - await pair.CleanReturnAsync(); } /// @@ -264,7 +254,7 @@ public async Task TestGhostToAghost() public async Task TestGhostDeletedSpawnsNewGhost() { // Client is needed to spawn session - await using var pair = await SetupPair(); + var pair = await SetupPair(); var server = pair.Server; var entMan = server.ResolveDependency(); @@ -308,7 +298,5 @@ await server.WaitAssertion(() => Assert.That(entMan.HasComponent(player.AttachedEntity!.Value)); #pragma warning restore NUnit2045 }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs index cebbed8ee8f..e22f6c1e4df 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs @@ -18,6 +18,14 @@ namespace Content.IntegrationTests.Tests.Minds; // This partial class contains misc helper functions for other tests. public sealed partial class MindTests { + // TODO GAMETEST: Rewrite this test to use improved GameTest pair management when I have an API for that figured out. + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + Dirty = true, + }; + /// /// Gets a server-client pair and ensures that the client is attached to a simple mind test entity. /// @@ -26,15 +34,9 @@ public sealed partial class MindTests /// the player's mind's current entity, likely because some previous test directly changed the players attached /// entity. /// - private static async Task SetupPair(bool dirty = false) + private async Task SetupPair(bool dirty = false) { - var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = dirty - }); - + var pair = Pair; var entMan = pair.Server.ResolveDependency(); var playerMan = pair.Server.ResolveDependency(); var mindSys = entMan.System(); diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs index db87797553f..fbe79fe044d 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs @@ -17,7 +17,7 @@ public sealed partial class MindTests [Test] public async Task TestGhostsCanReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -32,8 +32,6 @@ public async Task TestGhostsCanReconnect() Assert.That(entMan.HasComponent(mind.Comp.OwnedEntity)); Assert.That(mind.Comp.VisitingEntity, Is.Null); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -44,7 +42,7 @@ public async Task TestGhostsCanReconnect() [Test] public async Task TestDeletedCanReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -82,8 +80,6 @@ public async Task TestDeletedCanReconnect() Assert.That(mind.Comp.OwnedEntity, Is.Not.EqualTo(entity)); Assert.That(entMan.HasComponent(mind.Comp.OwnedEntity)); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -94,7 +90,7 @@ public async Task TestDeletedCanReconnect() [Test] public async Task TestVisitingGhostReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var entMan = pair.Server.ResolveDependency(); var mind = GetMind(pair); @@ -110,8 +106,6 @@ public async Task TestVisitingGhostReconnect() Assert.That(entMan.Deleted(original), Is.False); Assert.That(entMan.Deleted(ghost)); }); - - await pair.CleanReturnAsync(); } // This test will do the following: @@ -122,7 +116,7 @@ public async Task TestVisitingGhostReconnect() [Test] public async Task TestVisitingReconnect() { - await using var pair = await SetupPair(true); + var pair = await SetupPair(true); var entMan = pair.Server.ResolveDependency(); var mindSys = entMan.System(); var mind = GetMind(pair); @@ -150,8 +144,6 @@ await pair.Server.WaitAssertion(() => Assert.That(entMan.Deleted(visiting), Is.False); Assert.That(mind.Comp.CurrentEntity, Is.EqualTo(visiting)); }); - - await pair.CleanReturnAsync(); } // This test will do the following @@ -162,7 +154,7 @@ await pair.Server.WaitAssertion(() => [Test] public async Task TestReconnect() { - await using var pair = await SetupPair(); + var pair = await SetupPair(); var mind = GetMind(pair); Assert.That(mind.Comp.VisitingEntity, Is.Null); @@ -178,7 +170,5 @@ public async Task TestReconnect() Assert.That(newMind.Comp.VisitingEntity, Is.Null); Assert.That(newMind.Comp.OwnedEntity, Is.EqualTo(entity)); Assert.That(newMind.Id, Is.EqualTo(mind.Id)); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs index 1bda6fd4dba..08cfbaed5e3 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs @@ -1,10 +1,13 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -21,7 +24,7 @@ namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed partial class MindTests +public sealed partial class MindTests : GameTest { private static readonly ProtoId BluntDamageType = "Blunt"; @@ -54,7 +57,7 @@ public sealed partial class MindTests [Test] public async Task TestCreateAndTransferMindToNewEntity() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -73,14 +76,12 @@ await server.WaitAssertion(() => mindSystem.TransferTo(mind, entity, mind: mind); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind.Owner)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestReplaceMind() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -105,14 +106,12 @@ await server.WaitAssertion(() => Assert.That(mind.OwnedEntity, Is.Not.EqualTo(entity)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestEntityDeadWhenGibbed() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -147,7 +146,7 @@ await server.WaitAssertion(() => var damageable = entMan.GetComponent(entity); var prototype = protoMan.Index(BluntDamageType); - damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401))); + damageableSystem.SetDamage((entity, damageable), new DamageSpecifier(prototype, FixedPoint2.New(401))); Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId)); }); @@ -158,14 +157,12 @@ await server.WaitAssertion(() => var mind = entMan.GetComponent(mindId); Assert.That(mindSystem.IsCharacterDeadPhysically(mind)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestMindTransfersToOtherEntity() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -192,18 +189,12 @@ await server.WaitAssertion(() => Assert.That(mindSystem.GetMind(targetEntity), Is.EqualTo(mind)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestOwningPlayerCanBeChanged() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Connected = true, - DummyTicker = false - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -251,14 +242,12 @@ await server.WaitAssertion(() => }); await pair.RunTicksSync(5); - - await pair.CleanReturnAsync(); } [Test] public async Task TestAddRemoveHasRoles() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -321,15 +310,13 @@ await server.WaitAssertion(() => Assert.That(roleSystem.MindHasRole(mindId), Is.False); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestPlayerCanGhost() { // Client is needed to spawn session - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -397,19 +384,12 @@ await server.WaitAssertion(() => Assert.That(mId, Is.Not.EqualTo(mindId)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestGhostDoesNotInfiniteLoop() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -480,7 +460,5 @@ await server.WaitAssertion(() => Assert.That(player.AttachedEntity, Is.Not.Null); Assert.That(player.AttachedEntity!.Value, Is.EqualTo(ghost)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Minds/RoleTests.cs b/Content.IntegrationTests/Tests/Minds/RoleTests.cs index f0a7268a3d7..3204dc1c2fd 100644 --- a/Content.IntegrationTests/Tests/Minds/RoleTests.cs +++ b/Content.IntegrationTests/Tests/Minds/RoleTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Roles.Components; using Robust.Shared.GameObjects; using Robust.Shared.Reflection; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class RoleTests +public sealed class RoleTests : GameTest { /// /// Check that any prototype with a is properly configured @@ -14,7 +15,7 @@ public sealed class RoleTests [Test] public async Task ValidateRolePrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var jobComp = pair.Server.ResolveDependency().GetComponentName(); @@ -35,7 +36,6 @@ public async Task ValidateRolePrototypes() } }); - await pair.CleanReturnAsync(); } /// @@ -45,7 +45,7 @@ public async Task ValidateRolePrototypes() [Test] public async Task ValidateJobPrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var mindCompId = pair.Server.ResolveDependency().GetComponentName(); @@ -57,8 +57,6 @@ public async Task ValidateJobPrototypes() Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null); } }); - - await pair.CleanReturnAsync(); } /// @@ -68,7 +66,7 @@ public async Task ValidateJobPrototypes() [Test] public async Task ValidateRolesHaveMindRoleComp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var refMan = pair.Server.ResolveDependency(); var mindCompId = pair.Server.ResolveDependency().GetComponentName(); @@ -87,7 +85,5 @@ public async Task ValidateRolesHaveMindRoleComp() } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs new file mode 100644 index 00000000000..52432e631b0 --- /dev/null +++ b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs @@ -0,0 +1,152 @@ +using Content.IntegrationTests.Tests.Movement; +using Content.Server.NPC.HTN; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mousetrap; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Mousetrap; + +/// +/// Spawns a mouse and a mousetrap. +/// Makes the mouse cross the inactive mousetrap, then activates the trap and +/// makes the mouse try to cross back over it. +/// +/// +/// Yep, every time the tests run, a virtual mouse dies. Sorry. +/// +public sealed class MousetrapMouseMoveOverTest : MovementTest +{ + private static readonly EntProtoId MousetrapProtoId = "Mousetrap"; + private static readonly EntProtoId MouseProtoId = "MobMouse"; + protected override string PlayerPrototype => MouseProtoId.Id; // use a mouse as the player entity + + [Test] + public async Task MouseMoveOverTest() + { + // Make sure the mouse doesn't have any AI active + await Server.WaitPost(() => SEntMan.RemoveComponent(SPlayer)); + + // Spawn a mouse trap + await SpawnTarget(MousetrapProtoId); + Assert.That(Delta(), Is.GreaterThan(0.5), "Mouse and mousetrap not in expected positions."); + + Assert.That(HasComp(), + $"{MousetrapProtoId} does not have a MousetrapComponent. If you're refactoring, please update this test!"); + + Assert.That(TryComp(out var itemToggleComp), + $"{MousetrapProtoId} does not have a ItemToggleComponent. If you're refactoring, please update this test!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap started active."); + + // The mouse is spawned by the test before the atmosphere is added, so it has some barotrauma damage already + // TODO: fix this since it can have an impact on integration tests + var damageSystem = Server.System(); + var startingDamage = damageSystem.GetTotalDamage(SPlayer); + + Assert.That(SEntMan.TryGetComponent(SPlayer, out var mouseMobStateComp), + $"{MouseProtoId} does not have a MobStateComponent."); + Assert.That(mouseMobStateComp.CurrentState, Is.EqualTo(MobState.Alive), "Mouse was not alive when spawned."); + + // Move mouse over the trap + await Move(DirectionFlag.East, 1f); + + Assert.That(Delta(), Is.LessThan(0.5), "Mouse did not move over mousetrap."); + + // Walking over an inactive trap does not trigger it + Assert.That(damageSystem.GetTotalDamage(SPlayer), Is.LessThanOrEqualTo(startingDamage), "Mouse took damage from inactive trap!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was activated."); + + // Activate the trap + var itemToggleSystem = Server.System(); + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + + await Move(DirectionFlag.West, 1f); + Assert.That(Delta(), Is.LessThan(0.1), "Mouse moved past active mousetrap."); + + // Walking over an active trap triggers it + Assert.That(damageSystem.GetTotalDamage(SPlayer), Is.GreaterThan(startingDamage), "Mouse did not take damage from active trap!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was not deactivated after triggering."); + Assert.That(mouseMobStateComp.CurrentState, Is.EqualTo(MobState.Dead), "Mouse was not killed by trap."); + } +} + +/// +/// Spawns a mousetrap and makes the player walk over it without shoes. +/// Gives the player some shoes and makes them walk back over the trap. +/// +public sealed class MousetrapHumanMoveOverTest : MovementTest +{ + private static readonly EntProtoId MousetrapProtoId = "Mousetrap"; + private const string ShoesProtoId = "InteractionTestShoes"; + + [TestPrototypes] + private static readonly string TestPrototypes = $@" + - type: entity + parent: ClothingShoesBase + id: {ShoesProtoId} + components: + - type: Sprite + sprite: Clothing/Shoes/Boots/workboots.rsi + "; + + [Test] + public async Task HumanMoveOverTest() + { + await SpawnTarget(MousetrapProtoId); + + Assert.That(Delta(), Is.GreaterThan(0.5), "Player and mousetrap not in expected positions."); + + Assert.That(HasComp(), + $"{MousetrapProtoId} does not have a MousetrapComponent. If you're refactoring, please update this test!"); + + Assert.That(TryComp(out var itemToggleComp), + $"{MousetrapProtoId} does not have a ItemToggleComponent. If you're refactoring, please update this test!"); + + // Activate the trap + var itemToggleSystem = Server.System(); + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + + var damageSystem = Server.System(); + var startingDamage = damageSystem.GetTotalDamage(SPlayer); + + // Move player over the trap + await Move(DirectionFlag.East, 0.5f); + + Assert.That(Delta(), Is.LessThan(0.5), "Player did not move over mousetrap."); + + // Walking over the trap without shoes activates it + Assert.That(damageSystem.GetTotalDamage(SPlayer), Is.GreaterThan(startingDamage), "Player did not take damage."); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was not deactivated after triggering."); + + // Reactivate the trap + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + var afterStepDamage = damageSystem.GetTotalDamage(SPlayer); + + // Give the player some shoes + await PlaceInHands(ShoesProtoId); + // Thanks to quick-equip, using the shoes will wear them + await UseInHand(); + + // Move back over the trap + await Move(DirectionFlag.West, 1f); + Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not move back over mousetrap."); + + // Walking over the trap with shoes on does not activate it + Assert.That(damageSystem.GetTotalDamage(SPlayer), Is.LessThanOrEqualTo(afterStepDamage), "Player took damage from trap!"); + Assert.That(itemToggleComp.Activated, "Mousetrap was deactivated despite the player being protected by shoes."); + } +} diff --git a/Content.IntegrationTests/Tests/Movement/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs index 44ef02043e8..ee9d1f77ea6 100644 --- a/Content.IntegrationTests/Tests/Movement/MovementTest.cs +++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs @@ -2,6 +2,8 @@ using System.Numerics; using Content.IntegrationTests.Tests.Interaction; using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Movement; @@ -13,6 +15,7 @@ namespace Content.IntegrationTests.Tests.Movement; public abstract class MovementTest : InteractionTest { protected override string PlayerPrototype => "MobHuman"; + protected static readonly EntProtoId WallPrototype = "WallSolid"; /// /// Number of tiles to add either side of the player. @@ -47,8 +50,8 @@ public override async Task Setup() if (AddWalls) { - var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0))); - var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0))); + var sWallLeft = await SpawnEntity(WallPrototype, pCoords.Offset(new Vector2(-Tiles, 0))); + var sWallRight = await SpawnEntity(WallPrototype, pCoords.Offset(new Vector2(Tiles, 0))); WallLeft = SEntMan.GetNetEntity(sWallLeft); WallRight = SEntMan.GetNetEntity(sWallRight); @@ -59,7 +62,7 @@ public override async Task Setup() } /// - /// Get the relative horizontal between two entities. Defaults to using the target & player entity. + /// Get the relative horizontal between two entities. Defaults to using the target & player entity. /// protected float Delta(NetEntity? target = null, NetEntity? other = null) { @@ -73,5 +76,17 @@ protected float Delta(NetEntity? target = null, NetEntity? other = null) var delta = Transform.GetWorldPosition(SEntMan.GetEntity(target.Value)) - Transform.GetWorldPosition(SEntMan.GetEntity(other ?? Player)); return delta.X; } + + /// + /// Get the relative horizontal between a set of coordinates and an entity. Defaults to using the target coordinates and the player entity. + /// + protected float DeltaCoordinates(NetCoordinates? coords = null, NetEntity? other = null) + { + other ??= Player; + coords ??= TargetCoords; + + var delta = Transform.ToWorldPosition(ToServer(coords.Value)) - Transform.GetWorldPosition(ToServer(other.Value)); + return delta.X; + } } diff --git a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs index 7ee895d7c27..92e4d2471eb 100644 --- a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs @@ -1,10 +1,8 @@ #nullable enable -using System.Collections.Generic; -using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Helpers; using Content.Shared.Movement.Components; using Content.Shared.Slippery; using Content.Shared.Stunnable; -using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Maths; @@ -12,44 +10,32 @@ namespace Content.IntegrationTests.Tests.Movement; public sealed class SlippingTest : MovementTest { - public sealed class SlipTestSystem : EntitySystem - { - public HashSet Slipped = new(); - public override void Initialize() - { - SubscribeLocalEvent(OnSlip); - } - - private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args) - { - Slipped.Add(args.Slipped); - } - } + public sealed class SlipTestSystem : TestListenerSystem; [Test] public async Task BananaSlipTest() { - var sys = SEntMan.System(); await SpawnTarget("TrashBananaPeel"); var modifier = Comp(Player).SprintSpeedModifier; Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); - // Player is to the left of the banana peel and has not slipped. + // Player is to the left of the banana peel. Assert.That(Delta(), Is.GreaterThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); // Walking over the banana slowly does not trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down); - await Move(DirectionFlag.East, 1f); + await AssertFiresEvent(async () => await Move(DirectionFlag.East, 1f), count: 0); + Assert.That(Delta(), Is.LessThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); AssertComp(false, Player); // Moving at normal speeds does trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up); - await Move(DirectionFlag.West, 1f); - Assert.That(sys.Slipped, Does.Contain(SEntMan.GetEntity(Player))); + await AssertFiresEvent(async () => await Move(DirectionFlag.West, 1f)); + + // And the person that slipped was the player + AssertEvent(predicate: @event => @event.Slipped == SPlayer); AssertComp(true, Player); } } diff --git a/Content.IntegrationTests/Tests/NPC/NPCTest.cs b/Content.IntegrationTests/Tests/NPC/NPCTest.cs index 064fd6c5bf0..1568f360e7d 100644 --- a/Content.IntegrationTests/Tests/NPC/NPCTest.cs +++ b/Content.IntegrationTests/Tests/NPC/NPCTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.NPC.HTN; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.NPC; [TestFixture] -public sealed class NPCTest +public sealed class NPCTest : GameTest { [Test] public async Task CompoundRecursion() { - var pool = await PoolManager.GetServerClient(); + var pool = Pair; var server = pool.Server; await server.WaitIdleAsync(); @@ -30,8 +31,6 @@ await server.WaitAssertion(() => counts.Clear(); } }); - - await pool.CleanReturnAsync(); } private static void Count(HTNCompoundPrototype compound, Dictionary counts, HTNSystem htnSystem, IPrototypeManager protoManager) diff --git a/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs b/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs index c72be944fd3..7b4f048939b 100644 --- a/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs +++ b/Content.IntegrationTests/Tests/Networking/NetworkIdsMatchTest.cs @@ -1,14 +1,15 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Networking { [TestFixture] - public sealed class NetworkIdsMatchTest + public sealed class NetworkIdsMatchTest : GameTest { [Test] public async Task TestConnect() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -38,7 +39,6 @@ public async Task TestConnect() Assert.That(clientNetComps[netId].Name, Is.EqualTo(serverNetComps[netId].Name)); } }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs index b3955698489..786e14ae425 100644 --- a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs +++ b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; @@ -5,16 +6,17 @@ namespace Content.IntegrationTests.Tests.Networking; [TestFixture] -public sealed class PvsCommandTest +public sealed class PvsCommandTest : GameTest { private static readonly EntProtoId TestEnt = "MobHuman"; + public override PoolSettings PoolSettings => new() { Connected = true, DummyTicker = false }; + [Test] public async Task TestPvsCommands() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); + var pair = Pair; var (server, client) = pair; - await pair.RunTicksSync(5); // Spawn a complex entity. EntityUid entity = default; @@ -46,6 +48,5 @@ public async Task TestPvsCommands() Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied)); await server.WaitPost(() => server.EntMan.DeleteEntity(entity)); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs index 4539ca81a51..26e76376ce3 100644 --- a/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs +++ b/Content.IntegrationTests/Tests/Networking/ReconnectTest.cs @@ -1,15 +1,16 @@ +using Content.IntegrationTests.Fixtures; using Robust.Client.Console; using Robust.Shared.Network; namespace Content.IntegrationTests.Tests.Networking { [TestFixture] - public sealed class ReconnectTest + public sealed class ReconnectTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -32,7 +33,6 @@ public async Task Test() await pair.RunTicksSync(10); await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs index 3e494998451..15c7373b63a 100644 --- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs +++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Robust.Client.GameStates; using Robust.Client.Timing; using Robust.Shared; @@ -27,12 +28,12 @@ namespace Content.IntegrationTests.Tests.Networking // the tick where the server *should* have, but did not, acknowledge the state change. // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff. [TestFixture] - public sealed class SimplePredictReconcileTest + public sealed class SimplePredictReconcileTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + var pair = Pair; var server = pair.Server; var client = pair.Client; @@ -386,7 +387,6 @@ await client.WaitPost(() => } cfg.SetCVar(CVars.NetLogging, log); - await pair.CleanReturnAsync(); } public sealed class PredictionTestEntitySystem : EntitySystem diff --git a/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs new file mode 100644 index 00000000000..7a5195800ed --- /dev/null +++ b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs @@ -0,0 +1,97 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Chemistry.Components; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Nutrition; + +/// +/// Tests the mechanics of hunger and thirst. +/// +public sealed class HungerThirstTest : InteractionTest +{ + private readonly EntProtoId _drink = "DrinkLemonadeGlass"; + private readonly EntProtoId _food = "FoodCakeVanillaSlice"; + protected override string PlayerPrototype => "MobHuman"; + + /// + /// Tests that hunger and thirst values decrease over time (low means hungrier and thirstier). + /// Tests that hunger and thirst values increase when eating/drinking (high means less hungry and thirsty). + /// + [Test] + public async Task HungerThirstIncreaseDecreaseTest() + { + // Ensure that the player can breathe and not suffocate + await AddAtmosphere(); + + var hungerComponent = Comp(Player); + var thirstComponent = Comp(Player); + var hungerSystem = SEntMan.System(); + var thirstSystem = SEntMan.System(); + var ingestionSystem = SEntMan.System(); + + // Set initial value + hungerSystem.SetHunger(SPlayer, hungerComponent.Thresholds[HungerThreshold.Okay], hungerComponent); + thirstSystem.SetThirst(SPlayer, thirstComponent, thirstComponent.ThirstThresholds[ThirstThreshold.Okay]); + + // Ensure hunger and thirst value decrease over time (the Urist gets hungrier/thirstier) + var previousHungerValue = hungerSystem.GetHunger(hungerComponent); + var previousThirstValue = thirstComponent.CurrentThirst; // TODO: combined sation system with a sane API + + // Simulate long enough for both update loops to run + var runTime = Math.Max((float)hungerComponent.ThresholdUpdateRate.TotalSeconds, (float)thirstComponent.UpdateRate.TotalSeconds) + 1f; + await RunSeconds(runTime); + + var currentHungerValue = hungerSystem.GetHunger(hungerComponent); + Assert.That(currentHungerValue, Is.LessThan(previousHungerValue), "Hunger value did not decrease over time"); + previousHungerValue = currentHungerValue; + + var currentThirstValue = thirstComponent.CurrentThirst; + Assert.That(currentThirstValue, Is.LessThan(previousThirstValue), "Thirst value did not decrease over time"); + previousThirstValue = currentThirstValue; + + // Now we spawn food in the Urist's hand + await PlaceInHands(_food); + + // We eat the food in hand + await UseInHand(); + + // To see a change in hunger, we need to wait at least 30 seconds + await RunSeconds(30); + + // We ensure the food is fully eaten + var foodEaten = HandSys.GetActiveItem((SPlayer, Hands)); + Assert.That(foodEaten, Is.Null, "Food item did not disappear after eating it"); + + // Ensure that the hunger value has increased (The Urist is less hungry) + Assert.That(hungerSystem.GetHunger(hungerComponent), Is.GreaterThan(previousHungerValue), "Hunger value did not increase after eating food"); + + // Now we spawn a drink in the Urist's hand + var drink = await PlaceInHands(_drink); + + // Get the solution that can be consumed + Assert.That(ingestionSystem.CanConsume(SPlayer, SPlayer, ToServer(drink), out var solution, out _), + "Unable to get the solution or the entity can not be consumed"); + + // Find the initial amount of solution in the drink + var initialSolutionVolume = solution.Value.Comp.Solution.Volume; + + // We drink the drink in hand + await UseInHand(); + + // To see a change in thirst, we need to wait at least 30 seconds + await RunSeconds(30); + + // Ensure the solution volume has decreased + Assert.That(solution.Value.Comp.Solution.Volume, Is.LessThan(initialSolutionVolume), "Solution volume did not decrease after drinking"); + + // Ensure that the thirst value has increased (The Urist is less thirsty) + Assert.That(thirstComponent.CurrentThirst, Is.GreaterThan(previousThirstValue), "Thirst value did not increase after drinking"); + + // Make sure that the glass did not get deleted after drinking from it + var glass = HandSys.GetActiveItem((SPlayer, Hands)); + Assert.That(glass, Is.Not.Null, "Glass got deleted after drinking from it"); + } +} diff --git a/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs b/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs index a65e7d1fd67..f65bd324a00 100644 --- a/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs +++ b/Content.IntegrationTests/Tests/Physics/AnchorPrototypeTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.GameObjects; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests.Physics; [TestFixture] -public sealed class AnchorPrototypeTest +public sealed class AnchorPrototypeTest : GameTest { /// /// Asserts that entityprototypes marked as anchored are also static physics bodies. @@ -14,7 +15,7 @@ public sealed class AnchorPrototypeTest [Test] public async Task TestStaticAnchorPrototypes() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); @@ -37,7 +38,5 @@ await pair.Server.WaitAssertion(() => Assert.That(physics.BodyType, Is.EqualTo(BodyType.Static), $"Found entity prototype {ent} marked as anchored but not static for physics."); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index f6a90611866..a02179efc68 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -2,34 +2,42 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.IntegrationTests.Utility; +using YamlDotNet.RepresentationModel; using Content.Server.Administration.Systems; using Content.Server.GameTicking; -using Content.Server.Maps; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; using Content.Server.Spawners.Components; using Content.Server.Station.Components; using Content.Shared.CCVar; +using Content.Shared.Maps; 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] - public sealed class PostMapInitTest + public sealed class PostMapInitTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings() + { + Connected = true, + Dirty = true, + }; + private const bool SkipTestMaps = true; private const string TestMapsPath = "/Maps/Test/"; @@ -83,27 +91,9 @@ public sealed class PostMapInitTest .Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled)) .ToArray(); - private static readonly string[] GameMaps = - { - "Dev", - "TestTeg", - "Fland", - "Packed", - "Bagel", - "CentComm", - "Box", - "Marathon", - "MeteorArena", - "Saltern", - "Reach", - "Oasis", - "Amber", - "Plasma", - "Elkridge", - "Relic", - "dm01-entryway", - "Exo", - }; + private static readonly string[] GameMaps = GameDataScrounger.PrototypesOfKind().Where(x => x != PoolManager.TestMap).ToArray(); + private static readonly ResPath[] AllMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps", "*.yml"); + private static readonly ResPath[] ShuttleMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps/Shuttles", "*.yml"); private static readonly ProtoId DoNotMapCategory = "DoNotMap"; @@ -111,16 +101,16 @@ public sealed class PostMapInitTest /// Asserts that specific files have been saved as grids and not maps. /// [Test, TestCaseSource(nameof(Grids))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task GridsLoadableTest(string mapFile) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); var path = new ResPath(mapFile); await server.WaitPost(() => @@ -137,111 +127,89 @@ await server.WaitPost(() => mapSystem.DeleteMap(mapId); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// /// Asserts that shuttles are loadable and have been saved as grids and not maps. /// [Test] - public async Task ShuttlesLoadableTest() + [TestCaseSource(nameof(ShuttleMapFiles))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] + public async Task ShuttlesLoadableTest(ResPath path) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); - var resMan = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); - - var shuttleFolder = new ResPath("/Maps/Shuttles"); - var shuttles = resMan - .ContentFindFiles(shuttleFolder) - .Where(filePath => - filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); await server.WaitPost(() => { Assert.Multiple(() => { - foreach (var path in shuttles) + mapSystem.CreateMap(out var mapId); + try { - mapSystem.CreateMap(out var mapId); - try - { - Assert.That(mapLoader.TryLoadGrid(mapId, path, out _), - $"Failed to load shuttle {path}, was it saved as a map instead of a grid?"); - } - catch (Exception ex) - { - throw new Exception($"Failed to load shuttle {path}, was it saved as a map instead of a grid?", - ex); - } - mapSystem.DeleteMap(mapId); + Assert.That(mapLoader.TryLoadGrid(mapId, path, out _), + $"Failed to load shuttle {path}, was it saved as a map instead of a grid?"); } + catch (Exception ex) + { + throw new Exception($"Failed to load shuttle {path}, was it saved as a map instead of a grid?", + ex); + } + mapSystem.DeleteMap(mapId); }); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } [Test] - public async Task NoSavedPostMapInitTest() + [TestCaseSource(nameof(AllMapFiles))] + public async Task NoSavedPostMapInitTest(ResPath map) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var resourceManager = server.ResolveDependency(); var protoManager = server.ResolveDependency(); var loader = server.System(); - var mapFolder = new ResPath("/Maps"); - var maps = resourceManager - .ContentFindFiles(mapFolder) - .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); - - var v7Maps = new List(); - foreach (var map in maps) - { - var rootedPath = map.ToRootedPath(); + var rootedPath = map.ToRootedPath(); - // ReSharper disable once RedundantLogicalConditionalExpressionOperand - if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) - { - continue; - } + var isV7Map = false; - if (!resourceManager.TryContentFileRead(rootedPath, out var fileStream)) - { - Assert.Fail($"Map not found: {rootedPath}"); - } + // ReSharper disable once RedundantLogicalConditionalExpressionOperand + if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) + { + return; // We just pass immediately. + } - using var reader = new StreamReader(fileStream); - var yamlStream = new YamlStream(); + if (!resourceManager.TryContentFileRead(rootedPath, out var fileStream)) + { + Assert.Fail($"Map not found: {rootedPath}"); + } - yamlStream.Load(reader); + using var reader = new StreamReader(fileStream); + var yamlStream = new YamlStream(); - var root = yamlStream.Documents[0].RootNode; - var meta = root["meta"]; - var version = meta["format"].AsInt(); + yamlStream.Load(reader); - // TODO MAP TESTS - // Move this to some separate test? - CheckDoNotMap(map, root, protoManager); + var root = yamlStream.Documents[0].RootNode; + var meta = root["meta"]; + var version = meta["format"].AsInt(); - if (version >= 7) - { - v7Maps.Add(map); - continue; - } + // TODO MAP TESTS + // Move this to some separate test? + CheckDoNotMap(map, root, protoManager); + if (version >= 7) + { + isV7Map = true; + } + else + { var postMapInit = meta["postmapinit"].AsBool(); Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit"); } @@ -250,7 +218,7 @@ public async Task NoSavedPostMapInitTest() var ev = new BeforeEntityReadEvent(); server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev); - foreach (var map in v7Maps) + if (isV7Map) { Assert.That(IsPreInit(map, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes)); } @@ -271,8 +239,6 @@ public async Task NoSavedPostMapInitTest() await server.WaitPost(() => mapSys.InitializeMap(id)); Assert.That(loader.TrySaveMap(id, path)); Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes), Is.False); - - await pair.CleanReturnAsync(); } private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map) @@ -357,12 +323,10 @@ private bool IsPreInit(ResPath map, } [Test, TestCaseSource(nameof(GameMaps))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task GameMapsLoadableTest(string mapProto) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true // Stations spawn a bunch of nullspace entities and maps like centcomm. - }); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); @@ -373,7 +337,6 @@ public async Task GameMapsLoadableTest(string mapProto) var ticker = entManager.EntitySysManager.GetEntitySystem(); var shuttleSystem = entManager.EntitySysManager.GetEntitySystem(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { @@ -472,9 +435,6 @@ await server.WaitPost(() => throw new Exception($"Failed to delete map {mapProto}", ex); } }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } @@ -504,56 +464,33 @@ private static int GetCountLateSpawn(List gridUids, IEntityManager } [Test] - public async Task AllMapsTested() + [TestCaseSource(nameof(AllMapFiles))] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] + public async Task NonGameMapsLoadableTest(ResPath mapPath) { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - var protoMan = server.ResolveDependency(); - - var gameMaps = protoMan.EnumeratePrototypes() - .Where(x => !pair.IsTestPrototype(x)) - .Select(x => x.ID) - .ToHashSet(); - - Assert.That(gameMaps.Remove(PoolManager.TestMap)); - - Assert.That(gameMaps, Is.EquivalentTo(GameMaps.ToHashSet()), "Game map prototype missing from test cases."); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task NonGameMapsLoadableTest() - { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.ResolveDependency().GetEntitySystem(); var resourceManager = server.ResolveDependency(); var protoManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); var gameMaps = protoManager.EnumeratePrototypes().Select(o => o.MapPath).ToHashSet(); - var mapFolder = new ResPath("/Maps"); - var maps = resourceManager - .ContentFindFiles(mapFolder) - .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); - var mapPaths = new List(); - foreach (var map in maps) + if (gameMaps.Contains(mapPath)) { - if (gameMaps.Contains(map)) - continue; + // TODO: You might be able to save like, 1-2 seconds of test time if you eliminate these before + // actually needing a pair. + return; + } - var rootedPath = map.ToRootedPath(); - if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) - { - continue; - } - mapPaths.Add(rootedPath); + var rootedPath = mapPath.ToRootedPath(); + + if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) + { + return; } await server.WaitPost(() => @@ -572,34 +509,29 @@ await server.WaitPost(() => }; HashSet> maps; - foreach (var path in mapPaths) + + try { - try - { - Assert.That(mapLoader.TryLoadGeneric(path, out maps, out _, opts)); - } - catch (Exception ex) - { - throw new Exception($"Failed to load map {path}", ex); - } + Assert.That(mapLoader.TryLoadGeneric(mapPath, out maps, out _, opts)); + } + catch (Exception ex) + { + throw new Exception($"Failed to load map {mapPath}", ex); + } - try - { - foreach (var map in maps) - { - server.EntMan.DeleteEntity(map); - } - } - catch (Exception ex) + try + { + foreach (var map in maps) { - throw new Exception($"Failed to delete map {path}", ex); + server.EntMan.DeleteEntity(map); } } + catch (Exception ex) + { + throw new Exception($"Failed to delete map {mapPath}", ex); + } }); }); - - await server.WaitRunTicks(1); - await pair.CleanReturnAsync(); } /// diff --git a/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs b/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs new file mode 100644 index 00000000000..e991801330c --- /dev/null +++ b/Content.IntegrationTests/Tests/Power/PowerStatePrototypeTest.cs @@ -0,0 +1,58 @@ +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.Server.Power.Components; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Power; + +[TestFixture, TestOf(typeof(SharedPowerStateSystem))] +public sealed class PowerStatePrototypeTest : GameTest +{ + /// + /// Asserts that the 's load is the same + /// as the idle or working power draw from , + /// depending on the current power state. + /// + [Test] + public async Task AssertApcPowerMatchesPowerState() + { + var pair = Pair; + var server = pair.Server; + + var protoMan = server.ResolveDependency(); + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + Assert.Multiple(delegate + { + foreach (var prototype in protoMan.EnumeratePrototypes() + .Where(p => !p.Abstract) + .Where(p => !pair.IsTestPrototype(p))) + { + if (!prototype.TryGetComponent(out var powerStateComp, entMan.ComponentFactory)) + continue; + + // LESSON LEARNED: + // ENSURE THAT THE COMPONENT YOU ARE TRYING TO GET IS THE SERVER-SIDE VARIANT + if (!prototype.TryGetComponent(out var powerReceiverComp, entMan.ComponentFactory)) + { + Assert.Fail( + $"Entity prototype '{prototype.ID}' has a PowerStateComponent but is missing the required ApcPowerReceiverComponent."); + } + + var expectedLoad = powerStateComp.IsWorking + ? powerStateComp.WorkingPowerDraw + : powerStateComp.IdlePowerDraw; + + Assert.That(powerReceiverComp.Load, + Is.EqualTo(expectedLoad), + $"Entity prototype '{prototype.ID}' has mismatched power draw between PowerStateComponent and SharedApcPowerReceiverComponent."); + } + }); + }); + } +} diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs new file mode 100644 index 00000000000..a265de532fd --- /dev/null +++ b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs @@ -0,0 +1,181 @@ +using Content.IntegrationTests.Fixtures; +using Content.Shared.Coordinates; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests.Power; + +[TestFixture] +public sealed class PowerStateTest : GameTest +{ + [TestPrototypes] + private const string Prototypes = @" +- type: entity + id: PowerStateApcReceiverDummy + components: + - type: ApcPowerReceiver + - type: ExtensionCableReceiver + - type: Transform + anchored: true + - type: PowerState + isWorking: false + idlePowerDraw: 10 + workingPowerDraw: 50 +"; + + /// + /// Asserts that switching from idle to working updates the power receiver load to the working draw. + /// + [Test] + public async Task SetWorkingState_IdleToWorking_UpdatesLoad() + { + var pair = Pair; + var server = pair.Server; + + var mapManager = server.ResolveDependency(); + var entManager = server.ResolveDependency(); + var mapSys = entManager.System(); + + await server.WaitAssertion(() => + { + mapSys.CreateMap(out var mapId); + var grid = mapManager.CreateGridEntity(mapId); + + mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); + + var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates()); + + var receiver = entManager.GetComponent(ent); + var powerState = entManager.GetComponent(ent); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.False); + Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); + }); + + var system = entManager.System(); + system.SetWorkingState((ent, powerState), true); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.True); + Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); + }); + }); + } + + /// + /// Asserts that switching from working to idle updates the power receiver load to the idle draw. + /// + [Test] + public async Task SetWorkingState_WorkingToIdle_UpdatesLoad() + { + var pair = Pair; + var server = pair.Server; + + var mapManager = server.ResolveDependency(); + var entManager = server.ResolveDependency(); + var mapSys = entManager.System(); + + await server.WaitAssertion(() => + { + mapSys.CreateMap(out var mapId); + var grid = mapManager.CreateGridEntity(mapId); + + mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); + + var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates()); + + var receiver = entManager.GetComponent(ent); + var powerState = entManager.GetComponent(ent); + var system = entManager.System(); + Entity newEnt = (ent, powerState); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.False); + Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); + }); + + system.SetWorkingState(newEnt, true); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.True); + Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); + }); + + system.SetWorkingState(newEnt, false); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.False); + Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); + }); + }); + } + + /// + /// Asserts that setting the working state to the current state does not change the power receiver load. + /// + [Test] + public async Task SetWorkingState_AlreadyInState_NoChange() + { + var pair = Pair; + var server = pair.Server; + + var mapManager = server.ResolveDependency(); + var entManager = server.ResolveDependency(); + var mapSys = entManager.System(); + + await server.WaitAssertion(() => + { + mapSys.CreateMap(out var mapId); + var grid = mapManager.CreateGridEntity(mapId); + + mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); + + var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates()); + + var receiver = entManager.GetComponent(ent); + var powerState = entManager.GetComponent(ent); + var system = entManager.System(); + Entity valueTuple = (ent, powerState); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.False); + Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); + }); + + system.SetWorkingState(valueTuple, false); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.False); + Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f)); + }); + + system.SetWorkingState(valueTuple, true); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.True); + Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); + }); + + system.SetWorkingState(valueTuple, true); + + Assert.Multiple(() => + { + Assert.That(powerState.IsWorking, Is.True); + Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f)); + }); + }); + } +} + diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs index a448427d050..db6c0b7c894 100644 --- a/Content.IntegrationTests/Tests/Power/PowerTest.cs +++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs @@ -1,12 +1,12 @@ #nullable enable -using Content.Server.NodeContainer; +using Content.IntegrationTests.Fixtures; using Content.Server.NodeContainer.EntitySystems; -using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Power.Nodes; using Content.Shared.Coordinates; using Content.Shared.NodeContainer; +using Content.Shared.Power.Components; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Power { [TestFixture] - public sealed class PowerTest + public sealed class PowerTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -55,6 +55,7 @@ public sealed class PowerTest nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery + netsync: false - type: BatteryCharger - type: entity @@ -69,6 +70,7 @@ public sealed class PowerTest nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery + netsync: false - type: BatteryDischarger - type: entity @@ -86,6 +88,7 @@ public sealed class PowerTest nodeGroupID: HVPower - type: PowerNetworkBattery - type: Battery + netsync: false - type: BatteryDischarger node: output - type: BatteryCharger @@ -111,6 +114,7 @@ public sealed class PowerTest maxSupply: 1000 supplyRampTolerance: 1000 - type: Battery + netsync: false maxCharge: 1000 startingCharge: 1000 - type: Transform @@ -120,6 +124,7 @@ public sealed class PowerTest id: ApcDummy components: - type: Battery + netsync: false maxCharge: 10000 startingCharge: 10000 - type: PowerNetworkBattery @@ -163,7 +168,7 @@ public sealed class PowerTest [Test] public async Task TestSimpleSurplus() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -214,8 +219,6 @@ await server.WaitAssertion(() => Assert.That(supplier.CurrentSupply, Is.EqualTo(loadPower * 2).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } @@ -225,7 +228,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSimpleDeficit() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -276,14 +279,12 @@ await server.WaitAssertion(() => Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestSupplyRamp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -364,14 +365,12 @@ await server.WaitAssertion(() => Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestBatteryRamp() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -381,6 +380,8 @@ public async Task TestBatteryRamp() const float startingCharge = 100_000; PowerNetworkBatteryComponent netBattery = default!; + EntityUid generatorEnt = default!; + EntityUid consumerEnt = default!; BatteryComponent battery = default!; PowerConsumerComponent consumer = default!; @@ -396,15 +397,15 @@ await server.WaitAssertion(() => entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i)); } - var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates()); - var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2)); + generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates()); + consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2)); netBattery = entityManager.GetComponent(generatorEnt); battery = entityManager.GetComponent(generatorEnt); consumer = entityManager.GetComponent(consumerEnt); - batterySys.SetMaxCharge(generatorEnt, startingCharge, battery); - batterySys.SetCharge(generatorEnt, startingCharge, battery); + batterySys.SetMaxCharge((generatorEnt, battery), startingCharge); + batterySys.SetCharge((generatorEnt, battery), startingCharge); netBattery.MaxSupply = 400; netBattery.SupplyRampRate = 400; netBattery.SupplyRampTolerance = 100; @@ -442,7 +443,8 @@ await server.WaitAssertion(() => // Trivial integral to calculate expected power spent. const double spentExpected = (200 + 100) / 2.0 * 0.25; - Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); + var currentCharge = batterySys.GetCharge((generatorEnt, battery)); + Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); }); }); @@ -461,11 +463,10 @@ await server.WaitAssertion(() => // Trivial integral to calculate expected power spent. const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25; - Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); + var currentCharge = batterySys.GetCharge((generatorEnt, battery)); + Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] @@ -473,7 +474,7 @@ public async Task TestNoDemandRampdown() { // checks that batteries and supplies properly ramp down if the load is disconnected/disabled. - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -514,8 +515,8 @@ await server.WaitAssertion(() => supplier.SupplyRampRate = rampRate; supplier.SupplyRampTolerance = rampTol; - batterySys.SetMaxCharge(batteryEnt, 100_000, battery); - batterySys.SetCharge(batteryEnt, 100_000, battery); + batterySys.SetMaxCharge((batteryEnt, battery), 100_000); + batterySys.SetCharge((batteryEnt, battery), 100_000); netBattery.MaxSupply = draw / 2; netBattery.SupplyRampRate = rampRate; netBattery.SupplyRampTolerance = rampTol; @@ -563,20 +564,20 @@ await server.WaitAssertion(() => Assert.That(consumer.ReceivedPower, Is.EqualTo(0).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestSimpleBatteryChargeDeficit() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var batterySys = entityManager.System(); var mapSys = entityManager.System(); + EntityUid generatorEnt = default!; + EntityUid batteryEnt = default!; PowerSupplierComponent supplier = default!; BatteryComponent battery = default!; @@ -592,8 +593,8 @@ await server.WaitAssertion(() => entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i)); } - var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates()); - var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2)); + generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates()); + batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2)); supplier = entityManager.GetComponent(generatorEnt); var netBattery = entityManager.GetComponent(batteryEnt); @@ -601,7 +602,7 @@ await server.WaitAssertion(() => supplier.MaxSupply = 500; supplier.SupplyRampTolerance = 500; - batterySys.SetMaxCharge(batteryEnt, 100_000, battery); + batterySys.SetMaxCharge((batteryEnt, battery), 100_000); netBattery.MaxChargeRate = 1_000; netBattery.Efficiency = 0.5f; }); @@ -616,24 +617,26 @@ await server.WaitAssertion(() => { // half a second @ 500 W = 250 // 50% efficiency, so 125 J stored total. - Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1)); + var currentCharge = batterySys.GetCharge((batteryEnt, battery)); + Assert.That(currentCharge, Is.EqualTo(125).Within(0.1)); Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBattery() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); var batterySys = entityManager.System(); var mapSys = entityManager.System(); + EntityUid batteryEnt = default!; + EntityUid supplyEnt = default!; + EntityUid consumerEnt = default!; PowerConsumerComponent consumer = default!; PowerSupplierComponent supplier = default!; PowerNetworkBatteryComponent netBattery = default!; @@ -654,9 +657,9 @@ await server.WaitAssertion(() => var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); - var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2)); - var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); - var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3)); + batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2)); + supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); + consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3)); consumer = entityManager.GetComponent(consumerEnt); supplier = entityManager.GetComponent(supplyEnt); @@ -671,8 +674,8 @@ await server.WaitAssertion(() => netBattery.MaxSupply = 400; netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampRate = 100_000; - batterySys.SetMaxCharge(batteryEnt, 100_000, battery); - batterySys.SetCharge(batteryEnt, 100_000, battery); + batterySys.SetMaxCharge((batteryEnt, battery), 100_000); + batterySys.SetCharge((batteryEnt, battery), 100_000); }); // Run some ticks so everything is stable. @@ -695,23 +698,25 @@ await server.WaitAssertion(() => Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1)); const int expectedSpent = 200; - Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); + var currentCharge = batterySys.GetCharge((batteryEnt, battery)); + Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyPassThrough() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var gameTiming = server.ResolveDependency(); var batterySys = entityManager.System(); var mapSys = entityManager.System(); + EntityUid batteryEnt = default!; + EntityUid supplyEnt = default!; + EntityUid consumerEnt = default!; PowerConsumerComponent consumer = default!; PowerSupplierComponent supplier = default!; PowerNetworkBatteryComponent netBattery = default!; @@ -732,9 +737,9 @@ await server.WaitAssertion(() => var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1)); entityManager.GetComponent(terminal).LocalRotation = Angle.FromDegrees(180); - var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2)); - var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); - var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3)); + batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2)); + supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); + consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3)); consumer = entityManager.GetComponent(consumerEnt); supplier = entityManager.GetComponent(supplyEnt); @@ -751,8 +756,8 @@ await server.WaitAssertion(() => netBattery.SupplyRampTolerance = 400; netBattery.SupplyRampRate = 100_000; netBattery.Efficiency = 0.5f; - batterySys.SetMaxCharge(batteryEnt, 1_000_000, battery); - batterySys.SetCharge(batteryEnt, 1_000_000, battery); + batterySys.SetMaxCharge((batteryEnt, battery), 1_000_000); + batterySys.SetCharge((batteryEnt, battery), 1_000_000); }); // Run some ticks so everything is stable. @@ -773,17 +778,16 @@ await server.WaitAssertion(() => Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1)); const int expectedSpent = 400; - Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); + var currentCharge = batterySys.GetCharge((batteryEnt, battery)); + Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestFullBatteryEfficiencyDemandPassThrough() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -842,8 +846,8 @@ await server.WaitAssertion(() => supplier.MaxSupply = 1000; supplier.SupplyRampTolerance = 1000; - batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1); - batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2); + batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000); + batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000); netBattery1.MaxChargeRate = 1_000; netBattery2.MaxChargeRate = 1_000; @@ -869,8 +873,6 @@ await server.WaitAssertion(() => Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -880,7 +882,7 @@ await server.WaitAssertion(() => [Test] public async Task TestSupplyPrioritized() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -946,10 +948,10 @@ await server.WaitAssertion(() => netBattery2.SupplyRampTolerance = 1000; netBattery1.SupplyRampRate = 100_000; netBattery2.SupplyRampRate = 100_000; - batterySys.SetMaxCharge(batteryEnt1, 100_000, battery1); - batterySys.SetMaxCharge(batteryEnt2, 100_000, battery2); - batterySys.SetCharge(batteryEnt1, 100_000, battery1); - batterySys.SetCharge(batteryEnt2, 100_000, battery2); + batterySys.SetMaxCharge((batteryEnt1, battery1), 100_000); + batterySys.SetMaxCharge((batteryEnt2, battery2), 100_000); + batterySys.SetCharge((batteryEnt1, battery1), 100_000); + batterySys.SetCharge((batteryEnt2, battery2), 100_000); }); // Run some ticks so everything is stable. @@ -969,8 +971,6 @@ await server.WaitAssertion(() => Assert.That(netBattery2.SupplyRampPosition, Is.EqualTo(500).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -979,7 +979,7 @@ await server.WaitAssertion(() => [Test] public async Task TestBatteriesProportional() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1032,8 +1032,8 @@ await server.WaitAssertion(() => supplier.MaxSupply = 1000; supplier.SupplyRampTolerance = 1000; - batterySys.SetMaxCharge(batteryEnt1, 1_000_000, battery1); - batterySys.SetMaxCharge(batteryEnt2, 1_000_000, battery2); + batterySys.SetMaxCharge((batteryEnt1, battery1), 1_000_000); + batterySys.SetMaxCharge((batteryEnt2, battery2), 1_000_000); netBattery1.MaxChargeRate = 20; netBattery2.MaxChargeRate = 20; @@ -1060,14 +1060,12 @@ await server.WaitAssertion(() => Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestBatteryEngineCut() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1108,8 +1106,8 @@ await server.WaitPost(() => netBattery.MaxSupply = 1000; netBattery.SupplyRampTolerance = 200; netBattery.SupplyRampRate = 10; - batterySys.SetMaxCharge(batteryEnt, 100_000, battery); - batterySys.SetCharge(batteryEnt, 100_000, battery); + batterySys.SetMaxCharge((batteryEnt, battery), 100_000); + batterySys.SetCharge((batteryEnt, battery), 100_000); }); // Run some ticks so everything is stable. @@ -1142,8 +1140,6 @@ await server.WaitAssertion(() => Assert.That(netBattery.CurrentSupply, Is.GreaterThan(0)); }); }); - - await pair.CleanReturnAsync(); } /// @@ -1152,7 +1148,7 @@ await server.WaitAssertion(() => [Test] public async Task TestTerminalNodeGroups() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1211,19 +1207,20 @@ await server.WaitAssertion(() => Assert.That(leftNode.NodeGroup, Is.Not.EqualTo(rightNode.NodeGroup)); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ApcChargingTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); var batterySys = entityManager.System(); var mapSys = entityManager.System(); + EntityUid generatorEnt = default!; + EntityUid substationEnt = default!; + EntityUid apcEnt = default!; PowerNetworkBatteryComponent substationNetBattery = default!; BatteryComponent apcBattery = default!; @@ -1243,9 +1240,9 @@ await server.WaitAssertion(() => entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1)); entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2)); - var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); - var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1)); - var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2)); + generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0)); + substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1)); + apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2)); var generatorSupplier = entityManager.GetComponent(generatorEnt); substationNetBattery = entityManager.GetComponent(substationEnt); @@ -1254,7 +1251,7 @@ await server.WaitAssertion(() => generatorSupplier.MaxSupply = 1000; generatorSupplier.SupplyRampTolerance = 1000; - batterySys.SetCharge(apcEnt, 0, apcBattery); + batterySys.SetCharge((apcEnt, apcBattery), 0); }); server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc @@ -1263,18 +1260,17 @@ await server.WaitAssertion(() => { Assert.Multiple(() => { + var currentCharge = batterySys.GetCharge((apcEnt, apcBattery)); Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power - Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge + Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task ApcNetTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var entityManager = server.ResolveDependency(); @@ -1315,8 +1311,8 @@ await server.WaitAssertion(() => extensionCableSystem.SetProviderTransferRange(apcExtensionEnt, range); extensionCableSystem.SetReceiverReceptionRange(powerReceiverEnt, range); - batterySys.SetMaxCharge(apcEnt, 10000, battery); //arbitrary nonzero amount of charge - batterySys.SetCharge(apcEnt, battery.MaxCharge, battery); //fill battery + batterySys.SetMaxCharge((apcEnt, battery), 10000); //arbitrary nonzero amount of charge + batterySys.SetCharge((apcEnt, battery), battery.MaxCharge); //fill battery receiver.Load = 1; //arbitrary small amount of power }); @@ -1332,8 +1328,6 @@ await server.WaitAssertion(() => Assert.That(apcNetBattery.CurrentSupply, Is.EqualTo(1).Within(0.1)); }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs index c9f94987500..763cec328e0 100644 --- a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs +++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs @@ -1,17 +1,20 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; -using Content.Server.Maps; using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; using Content.Server.Power.NodeGroups; using Content.Server.Power.Pow3r; +using Content.Shared.Maps; +using Content.Shared.Power.Components; using Content.Shared.NodeContainer; +using Robust.Server.GameObjects; using Robust.Shared.EntitySerialization; namespace Content.IntegrationTests.Tests.Power; -[Explicit] -public sealed class StationPowerTests +public sealed class StationPowerTests : GameTest { /// /// How long the station should be able to survive on stored power if nothing is changed from round start. @@ -20,39 +23,36 @@ public sealed class StationPowerTests private static readonly string[] GameMaps = [ - "Fland", - "Meta", - "Packed", - "Omega", "Bagel", "Box", - "Core", + "Elkridge", + "Fland", "Marathon", - "Saltern", - "Reach", - "Train", "Oasis", - "Gate", - "Amber", - "Loop", + "Packed", "Plasma", - "Elkridge", - "Convex", "Relic", + "Snowball", + "Reach", + "Exo", ]; + public override PoolSettings PoolSettings => new () + { + Dirty = true, + }; + + [Explicit] [Test, TestCaseSource(nameof(GameMaps))] public async Task TestStationStartingPowerWindow(string mapProtoId) { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Dirty = true, - }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; var protoMan = server.ProtoMan; var ticker = entMan.System(); + var batterySys = entMan.System(); // Load the map await server.WaitAssertion(() => @@ -76,7 +76,8 @@ await server.WaitAssertion(() => if (node.NodeGroup is not IBasePowerNet group) continue; networks.TryGetValue(group.NetworkNode, out var charge); - networks[group.NetworkNode] = charge + battery.CurrentCharge; + var currentCharge = batterySys.GetCharge((uid, battery)); + networks[group.NetworkNode] = charge + currentCharge; } var totalStartingCharge = networks.MaxBy(n => n.Value).Value; @@ -98,8 +99,49 @@ await server.WaitAssertion(() => Assert.That(totalStartingCharge, Is.GreaterThanOrEqualTo(requiredStoredPower), $"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!"); }); + } + + [Test, TestCaseSource(nameof(GameMaps))] + public async Task TestApcLoad(string mapProtoId) + { + var pair = Pair; + var server = pair.Server; + + var entMan = server.EntMan; + var protoMan = server.ProtoMan; + var ticker = entMan.System(); + var xform = entMan.System(); + + // Load the map + await server.WaitAssertion(() => + { + Assert.That(protoMan.TryIndex(mapProtoId, out var mapProto)); + var opts = DeserializationOptions.Default with { InitializeMaps = true }; + ticker.LoadGameMap(mapProto, out var mapId, opts); + }); + // Wait long enough for power to ramp up, but before anything can trip + await pair.RunSeconds(2); - await pair.CleanReturnAsync(); + // Check that no APCs start overloaded + var apcQuery = entMan.EntityQueryEnumerator(); + Assert.Multiple(() => + { + while (apcQuery.MoveNext(out var uid, out var apc, out var battery)) + { + // Uncomment the following line to log starting APC load to the console + //Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}"); + if (xform.TryGetMapOrGridCoordinates(uid, out var coord)) + { + Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply), + $"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}"); + } + else + { + Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply), + $"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}"); + } + } + }); } } diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs index 267b3637e0a..62c9f1a460c 100644 --- a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Station.Systems; using Content.Shared.Inventory; using Content.Shared.Preferences; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Preferences; [TestFixture] -public sealed class LoadoutTests +public sealed class LoadoutTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -42,16 +43,18 @@ public sealed class LoadoutTests ["jumpsuit"] = "ClothingUniformJumpsuitColorGrey" }; + public override PoolSettings PoolSettings => new() + { + Dirty = true, + }; + /// /// Checks that an empty loadout still spawns with default gear and not naked. /// [Test] public async Task TestEmptyLoadout() { - var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Dirty = true, - }); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -87,7 +90,5 @@ await server.WaitAssertion(() => entManager.DeleteEntity(tester); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs index afaeafc6655..ad93e083fbd 100644 --- a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs @@ -1,24 +1,27 @@ using System.Collections.Generic; -using System.Linq; +using System.Threading; +using Content.IntegrationTests.Fixtures; using Content.Server.Database; -using Content.Shared.GameTicking; +using Content.Server.Preferences.Managers; +using Content.Shared.Body; using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; -using Content.Shared.Preferences.Loadouts; -using Content.Shared.Preferences.Loadouts.Effects; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; -using Robust.Shared.Enums; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.UnitTesting; namespace Content.IntegrationTests.Tests.Preferences { [TestFixture] - public sealed class ServerDbSqliteTests + public sealed class ServerDbSqliteTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -46,10 +49,6 @@ private static HumanoidCharacterProfile CharlieCharlieson() Species = "Human", Age = 21, Appearance = new( - "Afro", - Color.Aqua, - "Shaved", - Color.Aquamarine, Color.Azure, Color.Beige, new ()) @@ -59,21 +58,53 @@ private static HumanoidCharacterProfile CharlieCharlieson() private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server) { var cfg = server.ResolveDependency(); + var serialization = server.ResolveDependency(); var opsLog = server.ResolveDependency().GetSawmill("db.ops"); var builder = new DbContextOptionsBuilder(); var conn = new SqliteConnection("Data Source=:memory:"); conn.Open(); builder.UseSqlite(conn); - return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog); + return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog, serialization); } [Test] public async Task TestUserDoesNotExist() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var db = GetDb(pair.Server); // Database should be empty so a new GUID should do it. Assert.That(await db.GetPlayerPreferencesAsync(NewUserId()), Is.Null); + } + + [Test] + public async Task TestAppearanceValidationAndSave() + { + var pair = await PoolManager.GetServerClient(); + var db = GetDb(pair.Server); + var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); + + var profile = CharlieCharlieson(); + profile.Appearance.Markings["Head"] = new Dictionary> + { + [HumanoidVisualLayers.Hair] = [], + [HumanoidVisualLayers.FacialHair] = [], + }; + profile.Appearance.Markings["OrganFake"] = new Dictionary>(); + + await pair.Server.WaitAssertion(() => + { + var updated = HumanoidCharacterAppearance.EnsureValid(profile.Appearance, profile.Species, profile.Sex); + Assert.That(updated.Markings["Head"], Is.Empty); + Assert.That(updated.Markings.ContainsKey("OrganFake"), Is.False); + profile.Appearance = updated; + }); + + Assert.DoesNotThrowAsync(async () => await db.InitPrefsAsync(username, profile)); + + var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); + var prefs = await db.GetPlayerPreferencesAsync(username); + var fetchedProfile = preferences.ConvertProfiles(prefs!.Profiles.Find(p => p.Slot == 0)); + Assert.That(fetchedProfile.MemberwiseEquals(profile)); await pair.CleanReturnAsync(); } @@ -81,21 +112,22 @@ public async Task TestUserDoesNotExist() [Test] public async Task TestInitPrefs() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var db = GetDb(pair.Server); + var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); const int slot = 0; var originalProfile = CharlieCharlieson(); await db.InitPrefsAsync(username, originalProfile); var prefs = await db.GetPlayerPreferencesAsync(username); - Assert.That(prefs.Characters.Single(p => p.Key == slot).Value.MemberwiseEquals(originalProfile)); - await pair.CleanReturnAsync(); + var profile = preferences.ConvertProfiles(prefs!.Profiles.Find(p => p.Slot == slot)); + Assert.That(profile.MemberwiseEquals(originalProfile)); } [Test] public async Task TestDeleteCharacter() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var db = GetDb(server); var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); @@ -104,24 +136,64 @@ public async Task TestDeleteCharacter() await db.SaveSelectedCharacterIndexAsync(username, 1); await db.SaveCharacterSlotAsync(username, null, 1); var prefs = await db.GetPlayerPreferencesAsync(username); - Assert.That(!prefs.Characters.Any(p => p.Key != 0)); - await pair.CleanReturnAsync(); + Assert.That(prefs!.Profiles, Has.Count.EqualTo(1)); } [Test] public async Task TestNoPendingDatabaseChanges() { - var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var db = GetDb(server); Assert.That(async () => await db.HasPendingModelChanges(), Is.False, "The database has pending model changes. Add a new migration to apply them. See https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations"); - await pair.CleanReturnAsync(); } private static NetUserId NewUserId() { return new(Guid.NewGuid()); } + + private const string InvalidSpecies = "WingusDingus"; + + private static bool[] _trueFalse = [true, false]; + + [Test] + [TestCaseSource(nameof(_trueFalse))] + public async Task InvalidSpeciesConversion(bool legacy) + { + var pair = Pair; + var server = pair.Server; + var db = GetDb(pair.Server); + var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); + + var proto = server.ResolveDependency(); + Assert.That(!proto.HasIndex(InvalidSpecies), "You should not have added a species called WingusDingus, but change it in this test to something else I guess"); + + var bogus = new HumanoidCharacterProfile() + { + Species = InvalidSpecies, + }; + + var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); + await db.InitPrefsAsync(username, new HumanoidCharacterProfile()); + await db.SaveCharacterSlotAsync(username, bogus, 0); + await db.SaveSelectedCharacterIndexAsync(username, 0); + + if (legacy) + await db.MakeCharacterSlotLegacyAsync(username, 0); + + var prefs = await db.GetPlayerPreferencesAsync(username, CancellationToken.None); + + Assert.That(prefs, Is.Not.Null); + await server.WaitAssertion(() => + { + var converted = preferences.ConvertPreferences(prefs); + + Assert.That(converted.Characters, Has.Count.EqualTo(1)); + Assert.That(converted.Characters[0].Species, Is.Not.EqualTo(InvalidSpecies)); + Assert.That(converted.Characters[0].Species, Is.EqualTo(HumanoidCharacterProfile.DefaultSpecies)); + }); + } } } diff --git a/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs b/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs index 6bede9660a9..d3afcc72534 100644 --- a/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs +++ b/Content.IntegrationTests/Tests/Procedural/DungeonTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Server.Procedural; using Content.Shared.Procedural; using Robust.Shared.Maths; @@ -7,12 +8,12 @@ namespace Content.IntegrationTests.Tests.Procedural; [TestOf(typeof(DungeonSystem))] -public sealed class DungeonTests +public sealed class DungeonTests : GameTest { [Test] public async Task TestDungeonRoomPackBounds() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); await pair.Server.WaitAssertion(() => @@ -55,14 +56,12 @@ await pair.Server.WaitAssertion(() => } } }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestDungeonPresets() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var protoManager = pair.Server.ResolveDependency(); await pair.Server.WaitAssertion(() => @@ -92,7 +91,5 @@ await pair.Server.WaitAssertion(() => } } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs index 9ff8ca29009..5977e7deee2 100644 --- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs +++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Coordinates; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -27,12 +28,12 @@ namespace Content.IntegrationTests.Tests; /// spawn it into a new empty map and seeing what the map yml looks like. /// [TestFixture] -public sealed class PrototypeSaveTest +public sealed class PrototypeSaveTest : GameTest { [Test] public async Task UninitializedSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entityMan = server.ResolveDependency(); @@ -156,7 +157,6 @@ await server.WaitAssertion(() => } }); }); - await pair.CleanReturnAsync(); } public sealed class TestEntityUidContext : ISerializationContext, diff --git a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs index 440d9e636ec..3b4bed882d4 100644 --- a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs +++ b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.PrototypeTests; -public sealed class PrototypeTests +public sealed class PrototypeTests : GameTest { /// /// This test writes all known server prototypes as yaml files, then validates that the result is valid yaml. @@ -17,10 +18,9 @@ public sealed class PrototypeTests [Test] public async Task TestAllServerPrototypesAreSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveThenValidatePrototype(pair.Server, "server", context); - await pair.CleanReturnAsync(); } /// @@ -30,10 +30,9 @@ public async Task TestAllServerPrototypesAreSerializable() [Test] public async Task TestAllClientPrototypesAreSerializable() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveThenValidatePrototype(pair.Client, "client", context); - await pair.CleanReturnAsync(); } public async Task SaveThenValidatePrototype(RobustIntegrationTest.IntegrationInstance instance, string instanceId, @@ -69,10 +68,9 @@ public async Task SaveThenValidatePrototype(RobustIntegrationTest.IntegrationIns [Test] public async Task ServerPrototypeSaveLoadSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveLoadSavePrototype(pair.Server, context); - await pair.CleanReturnAsync(); } /// @@ -81,10 +79,9 @@ public async Task ServerPrototypeSaveLoadSaveTest() [Test] public async Task ClientPrototypeSaveLoadSaveTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var context = new PrototypeSaveTest.TestEntityUidContext(); await SaveLoadSavePrototype(pair.Client, context); - await pair.CleanReturnAsync(); } private async Task SaveLoadSavePrototype( diff --git a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs index c641cd9bd1c..ac7bdcd24d5 100644 --- a/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs +++ b/Content.IntegrationTests/Tests/PrototypeTests/PrototypeUploadTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Client.Upload.Commands; using Robust.Shared.GameObjects; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests.PrototypeTests; -public sealed class PrototypeUploadTest +public sealed class PrototypeUploadTest : GameTest { public const string IdA = "UploadTestPrototype"; public const string IdB = $"{IdA}NoParent"; @@ -36,7 +37,7 @@ public sealed class PrototypeUploadTest [TestOf(typeof(LoadPrototypeCommand))] public async Task TestFileUpload() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings {Connected = true}); + var pair = Pair; var sCompFact = pair.Server.ResolveDependency(); var cCompFact = pair.Client.ResolveDependency(); @@ -79,7 +80,5 @@ await pair.Client.WaitPost(() => Assert.That(cProtoB!.TryGetComponent(out _, cCompFact), Is.False); Assert.That(cProtoD!.TryGetComponent(out _, cCompFact), Is.True); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs b/Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs new file mode 100644 index 00000000000..b53a9a1aad6 --- /dev/null +++ b/Content.IntegrationTests/Tests/Puller/InteractingEntitiesTest.cs @@ -0,0 +1,38 @@ +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"; + + /// + /// 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. + /// + [Test] + public async Task PullerIsConsideredInteractingTest() + { + await SpawnTarget(MobHuman); + var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords)); + + var pullSys = SEntMan.System(); + await Server.WaitAssertion(() => + { + Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)), + $"{puller} failed to start pulling {Target}"); + }); + + var list = new HashSet(); + Server.System() + .GetEntitiesInteractingWithTarget(ToServer(Target.Value), list); + Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}"); + } +} diff --git a/Content.IntegrationTests/Tests/Puller/PullerTest.cs b/Content.IntegrationTests/Tests/Puller/PullerTest.cs index a4fde86dbfb..541417354a6 100644 --- a/Content.IntegrationTests/Tests/Puller/PullerTest.cs +++ b/Content.IntegrationTests/Tests/Puller/PullerTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Hands.Components; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Prototypes; @@ -9,7 +10,7 @@ namespace Content.IntegrationTests.Tests.Puller; #nullable enable [TestFixture] -public sealed class PullerTest +public sealed class PullerTest : GameTest { /// /// Checks that needsHands on PullerComponent is not set on mobs that don't even have hands. @@ -17,7 +18,7 @@ public sealed class PullerTest [Test] public async Task PullerSanityTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var compFactory = server.ResolveDependency(); @@ -39,7 +40,5 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs index dcff6c3b458..4bd5f62e78c 100644 --- a/Content.IntegrationTests/Tests/Replays/ReplayTests.cs +++ b/Content.IntegrationTests/Tests/Replays/ReplayTests.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Shared.CCVar; using Robust.Shared; @@ -6,20 +7,21 @@ namespace Content.IntegrationTests.Tests.Replays; [TestFixture] -public sealed class ReplayTests +public sealed class ReplayTests : GameTest { + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Dirty = true + }; + /// /// Simple test that just makes sure that automatic replay recording on round restarts works without any issues. /// [Test] public async Task AutoRecordReplayTest() { - var settings = new PoolSettings - { - DummyTicker = false, - Dirty = true - }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; Assert.That(server.CfgMan.GetCVar(CVars.ReplayServerRecordingEnabled), Is.False); @@ -54,7 +56,5 @@ public async Task AutoRecordReplayTest() await server.WaitPost(() => ticker.RestartRound()); await pair.RunTicksSync(25); Assert.That(recordMan.IsRecording, Is.False); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ResearchTest.cs b/Content.IntegrationTests/Tests/ResearchTest.cs index 4661a1ea9e1..c95d68d9e25 100644 --- a/Content.IntegrationTests/Tests/ResearchTest.cs +++ b/Content.IntegrationTests/Tests/ResearchTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Lathe; using Content.Shared.Research.Prototypes; using Robust.Shared.GameObjects; @@ -8,12 +9,12 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class ResearchTest +public sealed class ResearchTest : GameTest { [Test] public async Task DisciplineValidTierPrerequesitesTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoManager = server.ResolveDependency(); @@ -42,14 +43,12 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task AllTechPrintableTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entMan = server.ResolveDependency(); @@ -99,7 +98,5 @@ await server.WaitAssertion(() => } }); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs index 40457f54883..13bb3cfd74d 100644 --- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs +++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking; +using Content.IntegrationTests.Fixtures; +using Content.Server.GameTicking; using Content.Shared.GameTicking; using Robust.Shared.GameObjects; using Robust.Shared.Reflection; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { [TestFixture] [TestOf(typeof(RoundRestartCleanupEvent))] - public sealed class ResettingEntitySystemTests + public sealed class ResettingEntitySystemTests : GameTest { public sealed class TestRoundRestartCleanupEvent : EntitySystem { @@ -26,15 +27,17 @@ public void Reset(RoundRestartCleanupEvent ev) } } + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task ResettingEntitySystemResetTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var entitySystemManager = server.ResolveDependency(); @@ -52,7 +55,6 @@ await server.WaitAssertion(() => Assert.That(system.HasBeenReset); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Respirator/LungTest.cs b/Content.IntegrationTests/Tests/Respirator/LungTest.cs new file mode 100644 index 00000000000..097fe5e3a01 --- /dev/null +++ b/Content.IntegrationTests/Tests/Respirator/LungTest.cs @@ -0,0 +1,186 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Body.Components; +using Content.Shared.Body.Systems; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using System.Numerics; +using Content.IntegrationTests.Fixtures; +using Content.Shared.Atmos.Components; +using Robust.Shared.EntitySerialization.Systems; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Respirator; + +[TestFixture] +[TestOf(typeof(LungSystem))] +public sealed class LungTest : GameTest +{ + [TestPrototypes] + private const string Prototypes = @" +- type: entity + name: HumanLungDummy + id: HumanLungDummy + components: + - type: SolutionContainerManager + - type: Body + - type: EntityTableContainerFill + containers: + body_organs: !type:AllSelector + children: + - id: OrganHumanLungs + - type: MobState + allowedStates: + - Alive + - type: Damageable + - type: ThermalRegulator + metabolismHeat: 5000 + radiatedHeat: 400 + implicitHeatRegulation: 5000 + sweatHeatRegulation: 5000 + shiveringHeatRegulation: 5000 + normalBodyTemperature: 310.15 + thermalRegulationTemperatureThreshold: 25 + - type: Respirator + damage: + types: + Asphyxiation: 1.5 + damageRecovery: + types: + Asphyxiation: -1.5 +"; + + [Test] + public async Task AirConsistencyTest() + { + // --- Setup + var pair = Pair; + var server = pair.Server; + + await server.WaitIdleAsync(); + + var entityManager = server.ResolveDependency(); + var mapLoader = entityManager.System(); + var mapSys = entityManager.System(); + + EntityUid? grid = null; + RespiratorComponent resp = default; + EntityUid human = default; + GridAtmosphereComponent relevantAtmos = default; + var startingMoles = 0.0f; + + var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml"); + + await server.WaitPost(() => + { + mapSys.CreateMap(out var mapId); + Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt)); + grid = gridEnt!.Value.Owner; + }); + + Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); + + float GetMapMoles() + { + var totalMapMoles = 0.0f; + foreach (var tile in relevantAtmos.Tiles.Values) + { + totalMapMoles += tile.Air?.TotalMoles ?? 0.0f; + } + + return totalMapMoles; + } + + await server.WaitAssertion(() => + { + var center = new Vector2(0.5f, 0.5f); + var coordinates = new EntityCoordinates(grid.Value, center); + human = entityManager.SpawnEntity("HumanLungDummy", coordinates); + relevantAtmos = entityManager.GetComponent(grid.Value); + startingMoles = 100f; // Hardcoded because GetMapMoles returns 900 here for some reason. + +#pragma warning disable NUnit2045 + Assert.That(entityManager.TryGetComponent(human, out resp), Is.True); +#pragma warning restore NUnit2045 + }); + + // --- End setup + + var inhaleCycles = 100; + for (var i = 0; i < inhaleCycles; i++) + { + // Breathe in + await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Exhaling); + Assert.That( + GetMapMoles(), Is.LessThan(startingMoles), + "Did not inhale in any gas" + ); + + // Breathe out + await PoolManager.WaitUntil(server, () => resp.Status == RespiratorStatus.Inhaling); + Assert.That( + GetMapMoles(), Is.EqualTo(startingMoles).Within(0.0002), + "Did not exhale as much gas as was inhaled" + ); + } + } + + [Test] + public async Task NoSuffocationTest() + { + var pair = Pair; + var server = pair.Server; + + var mapManager = server.ResolveDependency(); + var entityManager = server.ResolveDependency(); + var cfg = server.ResolveDependency(); + var mapLoader = entityManager.System(); + var mapSys = entityManager.System(); + + EntityUid? grid = null; + RespiratorComponent respirator = null; + EntityUid human = default; + + var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml"); + + await server.WaitPost(() => + { + mapSys.CreateMap(out var mapId); + Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt)); + grid = gridEnt!.Value.Owner; + }); + + Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found."); + + await server.WaitAssertion(() => + { + var center = new Vector2(0.5f, 0.5f); + + var coordinates = new EntityCoordinates(grid.Value, center); + human = entityManager.SpawnEntity("HumanLungDummy", coordinates); + + var mixture = entityManager.System().GetContainingMixture(human); +#pragma warning disable NUnit2045 + Assert.That(mixture.TotalMoles, Is.GreaterThan(0)); + Assert.That(entityManager.TryGetComponent(human, out respirator), Is.True); + Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold)); +#pragma warning restore NUnit2045 + }); + + var increment = 10; + + // 20 seconds + var total = 20 * cfg.GetCVar(CVars.NetTickrate); + + for (var tick = 0; tick < total; tick += increment) + { + await server.WaitRunTicks(increment); + await server.WaitAssertion(() => + { + Assert.That(respirator.SuffocationCycles, Is.LessThanOrEqualTo(respirator.SuffocationCycleThreshold), + $"Entity {entityManager.GetComponent(human).EntityName} is suffocating on tick {tick}"); + }); + } + } +} diff --git a/Content.IntegrationTests/Tests/RestartRoundTest.cs b/Content.IntegrationTests/Tests/RestartRoundTest.cs index 69c9a7dedf1..1ed43b0951b 100644 --- a/Content.IntegrationTests/Tests/RestartRoundTest.cs +++ b/Content.IntegrationTests/Tests/RestartRoundTest.cs @@ -1,20 +1,23 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class RestartRoundTest + public sealed class RestartRoundTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; var sysManager = server.ResolveDependency(); @@ -23,8 +26,7 @@ await server.WaitPost(() => sysManager.GetEntitySystem().RestartRound(); }); - await pair.RunTicksSync(10); - await pair.CleanReturnAsync(); + await pair.RunUntilSynced(); } } } diff --git a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs index de89f16be76..e4f80eebb73 100644 --- a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs +++ b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Roles; using Content.Server.Storage.EntitySystems; using Robust.Shared.GameObjects; @@ -7,16 +8,17 @@ namespace Content.IntegrationTests.Tests.Roles; [TestFixture] -public sealed class StartingGearPrototypeStorageTest +public sealed class StartingGearPrototypeStorageTest : GameTest { + public override PoolSettings PoolSettings => new() { Connected = true, Dirty = true }; + /// /// Checks that a storage fill on a StartingGearPrototype will properly fill /// [Test] public async Task TestStartingGearStorage() { - var settings = new PoolSettings { Connected = true, Dirty = true }; - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; var server = pair.Server; var mapSystem = server.System(); var storageSystem = server.System(); @@ -65,7 +67,5 @@ await server.WaitAssertion(() => mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Round/JobTest.cs b/Content.IntegrationTests/Tests/Round/JobTest.cs index 215890791da..927f34a9eff 100644 --- a/Content.IntegrationTests/Tests/Round/JobTest.cs +++ b/Content.IntegrationTests/Tests/Round/JobTest.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.IntegrationTests.Pair; using Content.Server.GameTicking; using Content.Server.Mind; @@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Round; [TestFixture] -public sealed class JobTest +public sealed class JobTest : GameTest { private static readonly ProtoId Passenger = "Passenger"; private static readonly ProtoId Engineer = "StationEngineer"; @@ -44,6 +45,13 @@ public sealed class JobTest {Captain}: [ 1, 1 ] "; + public override PoolSettings PoolSettings => new() + { + DummyTicker = false, + Connected = true, + InLobby = true + }; + private void AssertJob(TestPair pair, ProtoId job, NetUserId? user = null, bool isAntag = false) { var jobSys = pair.Server.System(); @@ -71,12 +79,7 @@ private void AssertJob(TestPair pair, ProtoId job, NetUserId? user [Test] public async Task StartRoundTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -95,7 +98,6 @@ public async Task StartRoundTest() AssertJob(pair, Passenger); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -104,12 +106,7 @@ public async Task StartRoundTest() [Test] public async Task JobPreferenceTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -133,7 +130,6 @@ public async Task JobPreferenceTest() AssertJob(pair, Passenger); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -143,12 +139,7 @@ public async Task JobPreferenceTest() [Test] public async Task JobWeightTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -169,7 +160,6 @@ public async Task JobWeightTest() AssertJob(pair, Captain); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } /// @@ -178,12 +168,7 @@ public async Task JobWeightTest() [Test] public async Task JobPriorityTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - InLobby = true - }); + var pair = Pair; pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map); var ticker = pair.Server.System(); @@ -217,6 +202,5 @@ public async Task JobPriorityTest() }); await pair.Server.WaitPost(() => ticker.RestartRound()); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/RoundEndTest.cs b/Content.IntegrationTests/Tests/RoundEndTest.cs index 5de6de381de..16c460c3da1 100644 --- a/Content.IntegrationTests/Tests/RoundEndTest.cs +++ b/Content.IntegrationTests/Tests/RoundEndTest.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.RoundEnd; using Content.Shared.CCVar; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class RoundEndTest + public sealed class RoundEndTest : GameTest { private sealed class RoundEndTestSystem : EntitySystem { @@ -25,15 +26,18 @@ private void OnRoundEnd(RoundEndSystemChangedEvent ev) } } + + public override PoolSettings PoolSettings => new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }; + [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - DummyTicker = false, - Connected = true, - Dirty = true - }); + var pair = Pair; var server = pair.Server; @@ -151,7 +155,6 @@ await server.WaitAssertion(() => roundEndSystem.DefaultCountdownDuration = TimeSpan.FromMinutes(4); ticker.RestartRound(); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/SalvageTest.cs b/Content.IntegrationTests/Tests/SalvageTest.cs index 0059db6292b..68bdf4726c1 100644 --- a/Content.IntegrationTests/Tests/SalvageTest.cs +++ b/Content.IntegrationTests/Tests/SalvageTest.cs @@ -1,4 +1,6 @@ -using Content.Shared.CCVar; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.Shared.CCVar; using Content.Shared.Salvage; using Robust.Shared.Configuration; using Robust.Shared.EntitySerialization.Systems; @@ -8,15 +10,16 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class SalvageTest +public sealed class SalvageTest : GameTest { /// /// Asserts that all salvage maps have been saved as grids and are loadable. /// [Test] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task AllSalvageMapsLoadableTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -24,7 +27,6 @@ public async Task AllSalvageMapsLoadableTest() var prototypeManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); var mapSystem = entManager.System(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { @@ -50,8 +52,6 @@ await server.WaitPost(() => } } }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); + await RunUntilSynced(); } } diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs index eb3dc147208..914a1e21c29 100644 --- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs @@ -1,4 +1,6 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; using Content.Shared.CCVar; using Robust.Server.GameObjects; using Robust.Shared.Configuration; @@ -12,14 +14,15 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class SaveLoadMapTest + public sealed class SaveLoadMapTest : GameTest { [Test] + [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)] public async Task SaveLoadMultiGridMap() { var mapPath = new ResPath("/Maps/Test/TestMap.yml"); - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapManager = server.ResolveDependency(); var sEntities = server.ResolveDependency(); @@ -27,8 +30,6 @@ public async Task SaveLoadMultiGridMap() var mapSystem = sEntities.System(); var xformSystem = sEntities.EntitySysManager.GetEntitySystem(); var resManager = server.ResolveDependency(); - var cfg = server.ResolveDependency(); - Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitAssertion(() => { @@ -94,8 +95,6 @@ await server.WaitAssertion(() => }); } }); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs index b41aa0bf2f3..9339d612707 100644 --- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.CCVar; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; @@ -16,12 +17,12 @@ namespace Content.IntegrationTests.Tests /// Tests that a grid's yaml does not change when saved consecutively. /// [TestFixture] - public sealed class SaveLoadSaveTest + public sealed class SaveLoadSaveTest : GameTest { [Test] public async Task CreateSaveLoadSaveGrid() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); @@ -85,10 +86,9 @@ await server.WaitPost(() => } }); testSystem.Enabled = false; - await pair.CleanReturnAsync(); } - private const string TestMap = "Maps/bagel.yml"; + private new const string TestMap = "Maps/bagel.yml"; /// /// Loads the default map, runs it for 5 ticks, then assert that it did not change. @@ -96,7 +96,7 @@ await server.WaitPost(() => [Test] public async Task LoadSaveTicksSaveBagel() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.ResolveDependency().GetEntitySystem(); var mapSys = server.System(); @@ -167,7 +167,6 @@ await server.WaitPost(() => testSystem.Enabled = false; await server.WaitPost(() => mapSys.DeleteMap(mapId)); - await pair.CleanReturnAsync(); } /// @@ -183,7 +182,7 @@ await server.WaitPost(() => [Test] public async Task LoadTickLoadBagel() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var mapLoader = server.System(); @@ -241,7 +240,6 @@ public async Task LoadTickLoadBagel() testSystem.Enabled = false; await server.WaitPost(() => mapSys.DeleteMap(mapId1)); await server.WaitPost(() => mapSys.DeleteMap(mapId2)); - await pair.CleanReturnAsync(); } /// diff --git a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs index 339420362c1..daf14e42a0c 100644 --- a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs +++ b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Content.IntegrationTests.Fixtures; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Serialization; [TestFixture] -public sealed partial class SerializationTest +public sealed partial class SerializationTest : GameTest { /// /// Check that serializing generic enums works as intended. This should really be in engine, but engine @@ -17,7 +18,7 @@ public sealed partial class SerializationTest [Test] public async Task SerializeGenericEnums() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var seriMan = server.ResolveDependency(); var refMan = server.ResolveDependency(); @@ -67,8 +68,6 @@ public async Task SerializeGenericEnums() Assert.That(seriMan.ValidateNode(genericNode).GetErrors().Any(), Is.True); Assert.That(seriMan.ValidateNode(typedNode).GetErrors().Any(), Is.True); Assert.That(seriMan.ValidateNode(typedNode).GetErrors().Any(), Is.False); - - await pair.CleanReturnAsync(); } private enum TestEnum : byte { Aa, Bb, Cc, Dd } diff --git a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs index ab82a3d2f91..927f9734722 100644 --- a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs +++ b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Shuttles.Systems; using Content.Tests; using Robust.Server.GameObjects; @@ -13,7 +14,7 @@ namespace Content.IntegrationTests.Tests.Shuttle; -public sealed class DockTest : ContentUnitTest +public sealed class DockTest : GameTest { private static IEnumerable TestSource() { @@ -26,7 +27,7 @@ private static IEnumerable TestSource() [TestCaseSource(nameof(TestSource))] public async Task TestDockingConfig(Vector2 dock1Pos, Vector2 dock2Pos, Angle dock1Angle, Angle dock2Angle, bool result) { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -83,14 +84,12 @@ await server.WaitAssertion(() => Assert.That(result, Is.EqualTo(config != null)); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestPlanetDock() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -126,7 +125,5 @@ await server.WaitAssertion(() => var dockingConfig = dockingSystem.GetDockingConfig(shuttle, map.MapUid); Assert.That(dockingConfig, Is.Not.EqualTo(null)); }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/ShuttleTest.cs b/Content.IntegrationTests/Tests/ShuttleTest.cs index da5b82d91e7..1c447db8715 100644 --- a/Content.IntegrationTests/Tests/ShuttleTest.cs +++ b/Content.IntegrationTests/Tests/ShuttleTest.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.IntegrationTests.Fixtures; using Content.Server.Shuttles.Components; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -9,12 +10,12 @@ namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class ShuttleTest + public sealed class ShuttleTest : GameTest { [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -50,7 +51,6 @@ await server.WaitAssertion(() => { Assert.That(entManager.GetComponent(map.Grid).LocalPosition, Is.Not.EqualTo(Vector2.Zero)); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs b/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs index aa2ebef606b..18f47e2701f 100644 --- a/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs +++ b/Content.IntegrationTests/Tests/SmartFridge/SmartFridgeInteractionTest.cs @@ -17,19 +17,21 @@ public sealed class SmartFridgeInteractionTest : InteractionTest parent: PillCanister id: {SampleDumpableAndInsertableId} components: - - type: StorageFill - contents: - - id: PillCopper - amount: 5 + - type: EntityTableContainerFill + containers: + storagebase: + id: PillCopper + amount: 5 - type: entity parent: ChemBag id: {SampleDumpableId} components: - - type: StorageFill - contents: - - id: PillCopper - amount: 5 + - type: EntityTableContainerFill + containers: + storagebase: + id: PillCopper + amount: 5 "; [Test] diff --git a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs index da7e1e8e9b0..ca667f2e7af 100644 --- a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs +++ b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.Item; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; @@ -22,7 +23,7 @@ namespace Content.IntegrationTests.Tests.Sprite; /// /// [TestFixture] -public sealed class PrototypeSaveTest +public sealed class PrototypeSaveTest : GameTest { private static readonly HashSet Ignored = new() { @@ -34,8 +35,7 @@ public sealed class PrototypeSaveTest [Test] public async Task AllItemsHaveSpritesTest() { - var settings = new PoolSettings() { Connected = true }; // client needs to be in-game - await using var pair = await PoolManager.GetServerClient(settings); + var pair = Pair; List badPrototypes = []; await pair.Client.WaitPost(() => @@ -58,7 +58,5 @@ await pair.Client.WaitPost(() => Assert.Fail($"Item prototype has no sprite: {proto.ID}. It should probably either be marked as abstract, not be an item, or have a valid sprite"); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/StartTest.cs b/Content.IntegrationTests/Tests/StartTest.cs index e2bf5e8ff1c..223f4676b04 100644 --- a/Content.IntegrationTests/Tests/StartTest.cs +++ b/Content.IntegrationTests/Tests/StartTest.cs @@ -1,9 +1,10 @@ +using Content.IntegrationTests.Fixtures; using Robust.Shared.Exceptions; namespace Content.IntegrationTests.Tests { [TestFixture] - public sealed class StartTest + public sealed class StartTest : GameTest { /// /// Test that the server, and client start, and stop. @@ -11,7 +12,7 @@ public sealed class StartTest [Test] public async Task TestClientStart() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var client = pair.Client; Assert.That(client.IsAlive); await client.WaitRunTicks(5); @@ -27,8 +28,6 @@ public async Task TestClientStart() Assert.That(sRuntimeLog.ExceptionCount, Is.EqualTo(0), "No exceptions must be logged on server."); await server.WaitIdleAsync(); Assert.That(server.IsAlive); - - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index 02552669f7a..8da97cbab74 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Server.GameTicking; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; @@ -12,15 +13,21 @@ namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(EmergencyShuttleSystem))] -public sealed class EvacShuttleTest +public sealed class EvacShuttleTest : GameTest { + public override PoolSettings PoolSettings => new PoolSettings() + { + DummyTicker = true, + Dirty = true, + }; + /// /// Ensure that the emergency shuttle can be called, and that it will travel to centcomm /// [Test] public async Task EmergencyEvacTest() { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Dirty = true }); + var pair = Pair; var server = pair.Server; var entMan = server.EntMan; var ticker = server.System(); @@ -122,6 +129,5 @@ public async Task EmergencyEvacTest() server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime); pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, false); pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Station/JobTests.cs b/Content.IntegrationTests/Tests/Station/JobTests.cs index 5172049a991..0961db1c67b 100644 --- a/Content.IntegrationTests/Tests/Station/JobTests.cs +++ b/Content.IntegrationTests/Tests/Station/JobTests.cs @@ -2,12 +2,13 @@ using Content.Shared.Roles.Jobs; using Robust.Shared.Prototypes; using System.Linq; +using Content.IntegrationTests.Fixtures; namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(SharedJobSystem))] -public sealed class JobTest +public sealed class JobTest : GameTest { /// /// Ensures that every job belongs to at most 1 primary department. @@ -16,7 +17,7 @@ public sealed class JobTest [Test] public async Task PrimaryDepartmentsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -43,6 +44,5 @@ await server.WaitAssertion(() => } } }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs index 3fee4a146ce..c0f0b19b195 100644 --- a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs +++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Linq; -using Content.Server.Maps; +using Content.IntegrationTests.Fixtures; using Content.Server.Station.Components; using Content.Server.Station.Systems; +using Content.Shared.Maps; using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Shared.GameObjects; @@ -15,7 +16,7 @@ namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(StationJobsSystem))] -public sealed class StationJobsTest +public sealed class StationJobsTest : GameTest { private const string StationMapId = "FooStation"; @@ -85,7 +86,7 @@ public sealed class StationJobsTest [Test] public async Task AssignJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -153,13 +154,12 @@ await server.WaitAssertion(() => Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TCaptain")); }); }); - await pair.CleanReturnAsync(); } [Test] public async Task AdjustJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -203,13 +203,12 @@ await server.WaitAssertion(() => Assert.That(stationJobs.IsJobUnlimited(station, "TChaplain"), "Could not make TChaplain unlimited."); }); }); - await pair.CleanReturnAsync(); } [Test] public async Task InvalidRoundstartJobsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var prototypeManager = server.ResolveDependency(); @@ -247,7 +246,6 @@ await server.WaitAssertion(() => } }); }); - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs index 45ee69a9efa..6b100f89fdf 100644 --- a/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs +++ b/Content.IntegrationTests/Tests/Storage/EntityStorageTests.cs @@ -1,12 +1,14 @@ +using Content.IntegrationTests.Fixtures; using Content.Server.Storage.EntitySystems; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Robust.Shared.Containers; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Storage; [TestFixture] -public sealed class EntityStorageTests +public sealed class EntityStorageTests : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -30,7 +32,7 @@ public sealed class EntityStorageTests [Test] public async Task TestContainerDestruction() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var map = await pair.CreateTestMap(); @@ -75,7 +77,5 @@ await server.WaitPost(() => await server.WaitRunTicks(5); Assert.That(server.EntMan.Deleted(box)); Assert.That(server.EntMan.Deleted(crowbar), Is.False); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Storage/StorageTest.cs b/Content.IntegrationTests/Tests/Storage/StorageTest.cs new file mode 100644 index 00000000000..c0ef6a3691a --- /dev/null +++ b/Content.IntegrationTests/Tests/Storage/StorageTest.cs @@ -0,0 +1,258 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Content.IntegrationTests.Fixtures; +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 : GameTest +{ + /// + /// Can an item store more than itself weighs. + /// In an ideal world this test wouldn't need to exist because sizes would be recursive. + /// + [Test] + public async Task StorageSizeArbitrageTest() + { + var pair = Pair; + var server = pair.Server; + + var protoManager = server.ResolveDependency(); + var entMan = server.ResolveDependency(); + + var itemSys = entMan.System(); + + await server.WaitAssertion(() => + { + foreach (var proto in protoManager.EnumeratePrototypes()) + { + if (!proto.TryGetComponent("Storage", out var storage) || + storage.Whitelist != null || + storage.MaxItemSize == null || + !proto.TryGetComponent("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}"); + } + }); + } + + [Test] + public async Task TestStorageFillPrototypes() + { + var pair = Pair; + var server = pair.Server; + + var protoManager = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + foreach (var proto in protoManager.EnumeratePrototypes()) + { + if (!proto.TryGetComponent("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}"); + } + } + }); + }); + } + + [Test] + public async Task TestSufficientSpaceForFill() + { + var pair = Pair; + var server = pair.Server; + + var entMan = server.ResolveDependency(); + var protoMan = server.ResolveDependency(); + var compFact = server.ResolveDependency(); + var id = compFact.GetComponentName(); + + var itemSys = entMan.System(); + + var allSizes = protoMan.EnumeratePrototypes().ToList(); + allSizes.Sort(); + + await Assert.MultipleAsync(async () => + { + foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) + { + if (proto.HasComponent(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(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"); + } + } + }); + } + + [Test] + public async Task TestSufficientSpaceForEntityStorageFill() + { + var pair = Pair; + var server = pair.Server; + + var entMan = server.ResolveDependency(); + var protoMan = server.ResolveDependency(); + var compFact = server.ResolveDependency(); + var id = compFact.GetComponentName(); + + var itemSys = entMan.System(); + + foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) + { + if (proto.HasComponent(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."); + }); + } + } + + private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem) + { + if (entry.PrototypeId == null) + return 0; + + if (!protoMan.TryIndex(entry.PrototypeId, out var proto)) + { + Assert.Fail($"Unknown prototype: {entry.PrototypeId}"); + return 0; + } + + if (getCount) + return entry.Amount; + + + if (proto.TryGetComponent("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(); + 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(); + } + + /// + /// Tests that prototypes are not using multiple container fill components at the same time. + /// + [Test] + public async Task NoMultipleContainerFillsTest() + { + var pair = Pair; + var compFact = pair.Server.ResolveDependency(); + + Assert.Multiple(() => + { + foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) + { + Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}."); + Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}."); + } + + foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) + { + Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}."); + } + }); + } +} diff --git a/Content.IntegrationTests/Tests/StorageTest.cs b/Content.IntegrationTests/Tests/StorageTest.cs deleted file mode 100644 index 13ddc56567b..00000000000 --- a/Content.IntegrationTests/Tests/StorageTest.cs +++ /dev/null @@ -1,240 +0,0 @@ -#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 - { - /// - /// Can an item store more than itself weighs. - /// In an ideal world this test wouldn't need to exist because sizes would be recursive. - /// - [Test] - public async Task StorageSizeArbitrageTest() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - - var protoManager = server.ResolveDependency(); - var entMan = server.ResolveDependency(); - - var itemSys = entMan.System(); - - await server.WaitAssertion(() => - { - foreach (var proto in protoManager.EnumeratePrototypes()) - { - if (!proto.TryGetComponent("Storage", out var storage) || - storage.Whitelist != null || - storage.MaxItemSize == null || - !proto.TryGetComponent("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(); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - foreach (var proto in protoManager.EnumeratePrototypes()) - { - if (!proto.TryGetComponent("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(); - var protoMan = server.ResolveDependency(); - var compFact = server.ResolveDependency(); - var id = compFact.GetComponentName(); - - var itemSys = entMan.System(); - - var allSizes = protoMan.EnumeratePrototypes().ToList(); - allSizes.Sort(); - - await Assert.MultipleAsync(async () => - { - foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) - { - if (proto.HasComponent(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(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(); - var protoMan = server.ResolveDependency(); - var compFact = server.ResolveDependency(); - var id = compFact.GetComponentName(); - - var itemSys = entMan.System(); - - foreach (var (proto, fill) in pair.GetPrototypesWithComponent()) - { - if (proto.HasComponent(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(entry.PrototypeId, out var proto)) - { - Assert.Fail($"Unknown prototype: {entry.PrototypeId}"); - return 0; - } - - if (getCount) - return entry.Amount; - - - if (proto.TryGetComponent("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(); - 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(); - } - } -} diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs index 39df0fc8cc3..5a63dfa5633 100644 --- a/Content.IntegrationTests/Tests/StoreTests.cs +++ b/Content.IntegrationTests/Tests/StoreTests.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using System.Linq; -using System.Threading; -using Content.Server.Store.Systems; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.Server.PDA.Ringer; using Content.Server.Traitor.Uplink; using Content.Shared.FixedPoint; using Content.Shared.Inventory; @@ -10,13 +10,12 @@ using Content.Shared.Store.Components; using Content.Shared.StoreDiscount.Components; using Robust.Shared.GameObjects; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class StoreTests +public sealed class StoreTests : GameTest { [TestPrototypes] @@ -32,10 +31,23 @@ public sealed class StoreTests - idcard - type: Pda "; + + [Test] + [Ignore(""" + This currently causes the client to crash, failing the test. + When this is fixed, this test should be removed and StoreDiscountAndRefund + should just use the default pair config. + """)] + public async Task StoreDiscountAndRefundWithClient() + { + await StoreDiscountAndRefund(); + } + [Test] + [PairConfig(nameof(PsDisconnected))] public async Task StoreDiscountAndRefund() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -57,6 +69,7 @@ public async Task StoreDiscountAndRefund() EntityUid pda = default; var uplinkSystem = entManager.System(); + var ringerSystem = entManager.System(); var listingPrototypes = prototypeManager.EnumeratePrototypes() .ToArray(); @@ -78,10 +91,13 @@ await server.WaitAssertion(() => mindSystem.TransferTo(mind, human, mind: mind); FixedPoint2 originalBalance = 20; - uplinkSystem.AddUplink(human, originalBalance, null, true); + uplinkSystem.AddUplink(human, originalBalance, out var notes, pda, true); - var storeComponent = entManager.GetComponent(pda); - var discountComponent = entManager.GetComponent(pda); + Assert.That(notes != null); + ringerSystem.TryMatchRingtoneToStore(notes, out var storeEnt); + Assert.That(storeEnt.HasValue); + var storeComponent = entManager.GetComponent(storeEnt.Value); + var discountComponent = entManager.GetComponent(storeEnt.Value); Assert.That( discountComponent.Discounts, Has.Exactly(6).Items, @@ -127,8 +143,8 @@ await server.WaitAssertion(() => Assert.That(plainDiscountedCost.Value, Is.LessThan(prototypeCost.Value), "Expected discounted cost to be lower then prototype cost."); - var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID){Actor = human}; - server.EntMan.EventBus.RaiseLocalEvent(pda, buyMsg); + var buyMsg = new StoreBuyListingMessage(discountedListingItem.ID, null){Actor = human}; + server.EntMan.EventBus.RaiseLocalEvent(storeEnt.Value, buyMsg); var newBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype]; Assert.That(newBalance.Value, Is.EqualTo((originalBalance - plainDiscountedCost).Value), "Expected to have balance reduced by discounted cost"); @@ -141,7 +157,7 @@ await server.WaitAssertion(() => Assert.That(costAfterBuy.Value, Is.EqualTo(prototypeCost.Value), "Expected cost after discount refund to be equal to prototype cost."); var refundMsg = new StoreRequestRefundMessage { Actor = human }; - server.EntMan.EventBus.RaiseLocalEvent(pda, refundMsg); + server.EntMan.EventBus.RaiseLocalEvent(storeEnt.Value, refundMsg); // get refreshed item after refund re-generated items discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId); @@ -168,7 +184,5 @@ await server.WaitAssertion(() => } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Strip/StrippableTest.cs b/Content.IntegrationTests/Tests/Strip/StrippableTest.cs index f65bab1f810..d2ae3bd7ec4 100644 --- a/Content.IntegrationTests/Tests/Strip/StrippableTest.cs +++ b/Content.IntegrationTests/Tests/Strip/StrippableTest.cs @@ -1,8 +1,6 @@ -using Content.Client.Interaction; -using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Strip.Components; using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.Map; namespace Content.IntegrationTests.Tests.Strip; @@ -10,37 +8,22 @@ public sealed class StrippableTest : InteractionTest { protected override string PlayerPrototype => "MobHuman"; + /// + /// Tests that the stripping UI is opened when drag dropping from another mob onto the player. + /// [Test] public async Task DragDropOpensStrip() { - // Spawn one tile away - TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0)); await SpawnTarget("MobHuman"); var userInterface = Comp(Target); - Assert.That(userInterface.Actors.Count == 0); + Assert.That(userInterface.Actors, Is.Empty); - // screenCoordinates diff needs to be larger than DragDropSystem._deadzone - var screenX = CEntMan.System().Deadzone + 1f; + await DragDrop(Target.Value, Player); - // Start drag - await SetKey(EngineKeyFunctions.Use, - BoundKeyState.Down, - TargetCoords, - Target, - screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main)); + Assert.That(userInterface.Actors, Is.Not.Empty); - await RunTicks(5); - - // End drag - await SetKey(EngineKeyFunctions.Use, - BoundKeyState.Up, - PlayerCoords, - Player, - screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main)); - - await RunTicks(5); - - Assert.That(userInterface.Actors.Count > 0); + Assert.That(CUiSys.IsUiOpen(CTarget.Value, StrippingUiKey.Key)); + Assert.That(SUiSys.IsUiOpen(STarget.Value, StrippingUiKey.Key)); } } diff --git a/Content.IntegrationTests/Tests/Tag/TagTest.cs b/Content.IntegrationTests/Tests/Tag/TagTest.cs index e6cd2accaf5..4632e3ea2eb 100644 --- a/Content.IntegrationTests/Tests/Tag/TagTest.cs +++ b/Content.IntegrationTests/Tests/Tag/TagTest.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; using Content.Shared.Tag; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Tag { [TestFixture] [TestOf(typeof(TagComponent))] - public sealed class TagTest + public sealed class TagTest : GameTest { private const string TagEntityId = "TagTestDummy"; @@ -44,7 +45,7 @@ public sealed class TagTest [Test] public async Task TagComponentTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var sEntityManager = server.ResolveDependency(); @@ -295,7 +296,6 @@ await server.WaitAssertion(() => Assert.Throws(() => { tagSystem.AddTags(sTagEntity, new HashSet> { UnregisteredTag }); }); #endif }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs index 0827e11b705..64c4c291feb 100644 --- a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs +++ b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs @@ -100,4 +100,25 @@ public async Task FloorConstructDeconstruct() await AssertEntityLookup((FloorItem, 1)); } + + /// + /// Test brassPlating -> floor -> brassPlating using tilestacking + /// + [Test] + public async Task BrassPlatingPlace() + { + await SetTile(PlatingBrass); + + // Brass Plating -> Tile + await InteractUsing(FloorItem); + Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null); + await AssertTile(Floor); + AssertGridCount(1); + + // Tile -> Brass Plating + await InteractUsing(Pry); + await AssertTile(PlatingBrass); + AssertGridCount(1); + await AssertEntityLookup((FloorItem, 1)); + } } diff --git a/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs new file mode 100644 index 00000000000..d1c26e9bec9 --- /dev/null +++ b/Content.IntegrationTests/Tests/Tiles/TileStackRecursionTest.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using Content.IntegrationTests.Fixtures; +using Content.Shared.CCVar; +using Content.Shared.Maps; +using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Tiles; + +public sealed class TileStackRecursionTest : GameTest +{ + [Test] + public async Task TestBaseTurfRecursion() + { + var pair = Pair; + var protoMan = pair.Server.ResolveDependency(); + var cfg = pair.Server.ResolveDependency(); + var maxTileHistoryLength = cfg.GetCVar(CCVars.TileStackLimit); + Assert.That(protoMan.TryGetInstances(out var tiles)); + Assert.That(tiles, Is.Not.EqualTo(null)); + //store the distance from the root node to the given tile node + var nodes = new List<(ProtoId, int)>(); + //each element of list is a connection from BaseTurf tile to tile that goes on it + var edges = new List<(ProtoId, ProtoId)>(); + foreach (var ctdef in tiles!.Values) + { + //at first, each node is unexplored and has infinite distance to root. + //we use space node as root - everything is supposed to start at space, and it's hardcoded into the game anyway. + if (ctdef.ID == ContentTileDefinition.SpaceID) + { + nodes.Insert(0, (ctdef.ID, 0)); //space is the first element + continue; + } + Assert.That(ctdef.BaseTurf != ctdef.ID); + nodes.Add((ctdef.ID, int.MaxValue)); + if (ctdef.BaseTurf != null) + edges.Add((ctdef.BaseTurf.Value, ctdef.ID)); + Assert.That(ctdef.BaseWhitelist, Does.Not.Contain(ctdef.ID)); + edges.AddRange(ctdef.BaseWhitelist.Select(possibleTurf => + (possibleTurf, new ProtoId(ctdef.ID)))); + } + Bfs(nodes, edges, maxTileHistoryLength); + } + + private void Bfs(List<(ProtoId, int)> nodes, List<(ProtoId, ProtoId)> edges, int depthLimit) + { + var root = nodes[0]; + var queue = new Queue<(ProtoId, int)>(); + queue.Enqueue(root); + while (queue.Count != 0) + { + var u = queue.Dequeue(); + //get a list of tiles that can be put on this tile + var adj = edges.Where(n => n.Item1 == u.Item1).Select(n => n.Item2); + var adjNodes = nodes.Where(n => adj.Contains(n.Item1)).ToList(); + foreach (var node in adjNodes) + { + var adjNode = node; + adjNode.Item2 = u.Item2 + 1; + Assert.That(adjNode.Item2, Is.LessThanOrEqualTo(depthLimit)); //we can doomstack tiles on top of each other. Bad! + queue.Enqueue(adjNode); + } + } + } +} diff --git a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs index ca70120ee9a..cc440d0ccb7 100644 --- a/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs +++ b/Content.IntegrationTests/Tests/Toolshed/AdminTest.cs @@ -14,7 +14,7 @@ public async Task AllCommandsHavePermissions() var toolMan = Server.ResolveDependency(); var admin = Server.ResolveDependency(); var ignored = new HashSet() - {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly}; + {typeof(LocTest).Assembly}; await Server.WaitAssertion(() => { diff --git a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs index fb210eba505..849f9e55d3b 100644 --- a/Content.IntegrationTests/Tests/Toolshed/LocTest.cs +++ b/Content.IntegrationTests/Tests/Toolshed/LocTest.cs @@ -19,7 +19,7 @@ public async Task AllCommandsHaveDescriptions() var locStrings = new HashSet(); var ignored = new HashSet() - {typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly}; + {typeof(LocTest).Assembly}; await Server.WaitAssertion(() => { diff --git a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs index 5efa009ca7e..2aea2986ec9 100644 --- a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs +++ b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Client.LateJoin; +using Content.IntegrationTests.Fixtures; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.ContentPack; using Robust.Shared.IoC; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.UserInterface; [TestFixture] -public sealed class UiControlTest +public sealed class UiControlTest : GameTest { // You should not be adding to this. private Type[] _ignored = new Type[] @@ -22,10 +23,7 @@ public sealed class UiControlTest [Test] public async Task TestWindows() { - var pair = await PoolManager.GetServerClient(new PoolSettings() - { - Connected = true, - }); + var pair = Pair; var activator = pair.Client.ResolveDependency(); var refManager = pair.Client.ResolveDependency(); var loader = pair.Client.ResolveDependency(); @@ -50,7 +48,5 @@ await pair.Client.WaitAssertion(() => activator.CreateInstance(type, oneOff: true, inject: false); } }); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs b/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs index d460fd354f0..6ab5ca16918 100644 --- a/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs +++ b/Content.IntegrationTests/Tests/Utility/EntitySystemExtensionsTest.cs @@ -1,4 +1,5 @@ #nullable enable +using Content.IntegrationTests.Fixtures; using Content.Shared.Physics; using Content.Shared.Spawning; using Robust.Shared.GameObjects; @@ -8,7 +9,7 @@ namespace Content.IntegrationTests.Tests.Utility { [TestFixture] [TestOf(typeof(EntitySystemExtensions))] - public sealed class EntitySystemExtensionsTest + public sealed class EntitySystemExtensionsTest : GameTest { private const string BlockerDummyId = "BlockerDummy"; @@ -32,7 +33,7 @@ public sealed class EntitySystemExtensionsTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -95,7 +96,6 @@ await server.WaitAssertion(() => Assert.That(entity, Is.Not.Null); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs index 19b25816fa3..d7e1239603d 100644 --- a/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs +++ b/Content.IntegrationTests/Tests/Utility/EntityWhitelistTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Containers.ItemSlots; using Content.Shared.Whitelist; using Robust.Shared.GameObjects; @@ -7,7 +8,7 @@ namespace Content.IntegrationTests.Tests.Utility { [TestFixture] [TestOf(typeof(EntityWhitelist))] - public sealed class EntityWhitelistTest + public sealed class EntityWhitelistTest : GameTest { private const string InvalidComponent = "Sprite"; private const string ValidComponent = "Physics"; @@ -58,7 +59,7 @@ public sealed class EntityWhitelistTest [Test] public async Task Test() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -111,7 +112,6 @@ await server.WaitAssertion(() => Assert.That(sys.IsValid(whitelistSer, WhitelistTestInvalidTag), Is.False); }); }); - await pair.CleanReturnAsync(); } } } diff --git a/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs b/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs new file mode 100644 index 00000000000..9bfadc284d5 --- /dev/null +++ b/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs @@ -0,0 +1,49 @@ +using Content.IntegrationTests.Utility; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Utility; + +[TestOf(typeof(GameDataScrounger))] +public sealed class ScroungerTests +{ + [Test] + [Description("Assert that the data scrounger finds prototypes by type successfully.")] + public void ScroungeByType() + { + var scrounged = GameDataScrounger.PrototypesOfKind(); + Assert.That(scrounged, Is.Not.Empty); + } + + [Test] + [Description("Assert that the data scrounger finds all files by pattern in a directory successfully.")] + [TestCase("*.yml")] + [TestCase("*.txt")] + public void ScroungeByPattern(string pattern) + { + var files = GameDataScrounger.FilesInDirectory("/", pattern); + + Assert.That(files, Is.Not.Empty); + } + + [Test] + [Description("Assert that the data scrounger finds all files by pattern in a directory successfully, and returns valid VFS paths.")] + public void ScroungeByPatternInVfs() + { + var files = GameDataScrounger.FilesInDirectoryInVfs("/Maps", "*.yml"); + + Assert.That(files, Is.Not.Empty); + + Assert.That(files[0].IsRooted, Is.True); + Assert.That(files[0].ToString(), Does.StartWith("/Maps/")); + } + + [Test] + [Description("Assert that the data scrounger finds entities by component successfully.")] + public void ScroungeByComponent() + { + var items = GameDataScrounger.EntitiesWithComponent("Item"); + + Assert.That(items, Is.Not.Empty); + } +} diff --git a/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs index 3645667737a..f7e5bc2a33a 100644 --- a/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs +++ b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs @@ -2,7 +2,9 @@ using Content.IntegrationTests.Tests.Interaction; using Content.Server.VendingMachines; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.VendingMachines; using Robust.Shared.Prototypes; @@ -194,14 +196,14 @@ public async Task RepairTest() private async Task BreakVendor() { var damageableSys = SEntMan.System(); - Assert.That(TryComp(out var damageableComp), $"{VendingMachineProtoId} does not have DamageableComponent."); - Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage."); + Assert.That(HasComp(), $"{VendingMachineProtoId} does not have DamageableComponent."); + Assert.That(damageableSys.GetAllDamage(STarget!.Value).GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage."); // Damage the vending machine to the point that it breaks var damageType = ProtoMan.Index(TestDamageType); var damage = new DamageSpecifier(damageType, FixedPoint2.New(100)); - await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true)); + await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target).Value, damage, ignoreResistances: true)); await RunTicks(5); - Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage."); + Assert.That(damageableSys.GetAllDamage(STarget!.Value).GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage."); } } diff --git a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs index f30eed0651e..dfaaae05ad7 100644 --- a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs +++ b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs @@ -1,12 +1,16 @@ #nullable enable using System.Collections.Generic; +using Content.IntegrationTests.Fixtures; 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.Components; +using Content.Shared.Storage.EntitySystems; using Content.Shared.VendingMachines; using Content.Shared.Wires; using Robust.Shared.GameObjects; @@ -18,7 +22,7 @@ namespace Content.IntegrationTests.Tests [TestFixture] [TestOf(typeof(VendingMachineRestockComponent))] [TestOf(typeof(VendingMachineSystem))] - public sealed class VendingMachineRestockTest : EntitySystem + public sealed class VendingMachineRestockTest : GameTest { private static readonly ProtoId TestDamageType = "Blunt"; @@ -108,79 +112,95 @@ public sealed class VendingMachineRestockTest : EntitySystem [Test] public async Task TestAllRestocksAreAvailableToBuy() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); var prototypeManager = server.ResolveDependency(); var compFact = server.ResolveDependency(); + var entityTable = server.EntMan.System(); await server.WaitAssertion(() => { - HashSet restocks = new(); - Dictionary> restockStores = new(); - - // Collect all the prototypes with restock components. + // Collect all entity prototypes which are vending machine restocks. + var restockEntities = new HashSet>(); foreach (var proto in prototypeManager.EnumeratePrototypes()) { if (proto.Abstract || pair.IsTestPrototype(proto) || !proto.HasComponent()) - { continue; - } - restocks.Add(proto.ID); + restockEntities.Add(proto.ID); } - // Collect all the prototypes with StorageFills referencing those entities. + // Collect all entity prototypes with `EntityTableContainerFill`s which contain those restock entities. + // Specifically, this is a mapping of entities-with-container-fill to their-contained-entities-which-are-restocks. + Dictionary, + List>> entitiesWhichSpawnRestocks = new(); foreach (var proto in prototypeManager.EnumeratePrototypes()) { - if (!proto.TryGetComponent(out var storage, compFact)) + if (!proto.TryGetComponent(out var fill, compFact)) + continue; + + var containers = fill.Containers; + + // We only care about the special known container. + if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) continue; - List restockStore = new(); - foreach (var spawnEntry in storage.Contents) + var entitiesInProtoContainingRestock = new List>(); + foreach (var (fillSpawnEntry, _) in entityTable.ListSpawns(container)) { - if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId)) - restockStore.Add(spawnEntry.PrototypeId); + if (restockEntities.Contains(fillSpawnEntry.Id)) + entitiesInProtoContainingRestock.Add(fillSpawnEntry.Id); } - if (restockStore.Count > 0) - restockStores.Add(proto.ID, restockStore); + if (entitiesInProtoContainingRestock.Count > 0) + entitiesWhichSpawnRestocks.Add(proto.ID, entitiesInProtoContainingRestock); } - // Iterate through every CargoProduct and make sure each - // prototype with a restock component is referenced in a - // purchaseable entity with a StorageFill. + // Remove all restock entities from our set which are either directly purchasable as a CargoProduct, or + // which are spawned by EntityTableContainerFill on a CargoProduct. foreach (var proto in prototypeManager.EnumeratePrototypes()) { - if (restockStores.ContainsKey(proto.Product)) + // If the cargo product's product is the restock itself, just remove it. + restockEntities.Remove(proto.Product.Id); + + // Check if the product is an entity which spawns a restock. + if (entitiesWhichSpawnRestocks.TryGetValue(proto.Product.Id, out var restocksSpawnedByProduct)) { - foreach (var entry in restockStores[proto.Product]) - restocks.Remove(entry); + foreach (var entry in restocksSpawnedByProduct) + { + restockEntities.Remove(entry); + } - restockStores.Remove(proto.Product); + entitiesWhichSpawnRestocks.Remove(proto.Product.Id); } } + // Any entities left in restockEntities are restocks which can't be bought from Cargo. Assert.Multiple(() => { - Assert.That(restockStores, Has.Count.EqualTo(0), - $"Some entities containing entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restockStores.Keys)}"); + const string restockCompName = nameof(VendingMachineRestockComponent); + + Assert.That(entitiesWhichSpawnRestocks, + Has.Count.EqualTo(0), + $"Some entities containing entities with {restockCompName} are unavailable for purchase: \n - {string.Join("\n - ", entitiesWhichSpawnRestocks.Keys)}"); - Assert.That(restocks, Has.Count.EqualTo(0), - $"Some entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restocks)}"); + + + Assert.That(restockEntities, + Has.Count.EqualTo(0), + $"Some entities with {restockCompName} are unavailable for purchase: \n - {string.Join("\n - ", restockEntities)}"); }); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestCompleteRestockProcess() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -259,14 +279,12 @@ await server.WaitAssertion(() => mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestRestockBreaksOpen() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -296,14 +314,12 @@ await server.WaitAssertion(() => restock = entityManager.SpawnEntity("TestRestockExplode", coordinates); var damageSpec = new DamageSpecifier(prototypeManager.Index(TestDamageType), 100); - var damageResult = damageableSystem.TryChangeDamage(restock, damageSpec); + var damageResult = damageableSystem.ChangeDamage(restock, damageSpec); #pragma warning disable NUnit2045 - Assert.That(damageResult, Is.Not.Null, - "Received null damageResult when attempting to damage restock box."); + Assert.That(!damageResult.Empty, "Received empty damageResult when attempting to damage restock box."); - Assert.That((int) damageResult!.GetTotal(), Is.GreaterThan(0), - "Box damage result was not greater than 0."); + Assert.That((int) damageResult.GetTotal(), Is.GreaterThan(0), "Box damage result was not greater than 0."); #pragma warning restore NUnit2045 }); await server.WaitRunTicks(15); @@ -323,14 +339,12 @@ await server.WaitAssertion(() => mapSystem.DeleteMap(testMap.MapId); }); - - await pair.CleanReturnAsync(); } [Test] public async Task TestRestockInventoryBounds() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; await server.WaitIdleAsync(); @@ -369,10 +383,6 @@ await server.WaitAssertion(() => Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3), "Machine's available inventory did not stay the same after a third restock."); }); - - await pair.CleanReturnAsync(); } } } - -#nullable disable diff --git a/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs b/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs new file mode 100644 index 00000000000..8ddbe0ed089 --- /dev/null +++ b/Content.IntegrationTests/Tests/Weapons/WeaponTests.cs @@ -0,0 +1,66 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.Wieldable.Components; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Weapons; + +public sealed class WeaponTests : InteractionTest +{ + protected override string PlayerPrototype => "MobHuman"; // The default test mob only has one hand + private static readonly EntProtoId MobHuman = "MobHuman"; + private static readonly EntProtoId SniperMosin = "WeaponSniperMosin"; + + [Test] + public async Task GunRequiresWieldTest() + { + var gunSystem = SEntMan.System(); + var damageSystem = SEntMan.System(); + + await AddAtmosphere(); // prevent the Urist from suffocating + + var urist = await SpawnTarget(MobHuman); + var damageComp = Comp(urist); + + var mosinNet = await PlaceInHands(SniperMosin); + var mosinEnt = ToServer(mosinNet); + + await Pair.RunSeconds(2f); // Guns have a cooldown when picking them up. + + Assert.That(HasComp(mosinNet), + "Looks like you've removed the 'GunRequiresWield' component from the mosin sniper." + + "If this was intentional, please update WeaponTests.cs to reflect this change!"); + + var startAmmo = gunSystem.GetAmmoCount(mosinEnt); + var wieldComp = Comp(mosinNet); + + Assert.That(startAmmo, Is.GreaterThan(0), "Mosin was spawned with no ammo!"); + Assert.That(wieldComp.Wielded, Is.False, "Mosin was spawned wielded!"); + + await AttemptShoot(urist, false); // should fail due to not being wielded + var updatedAmmo = gunSystem.GetAmmoCount(mosinEnt); + + Assert.That(updatedAmmo, + Is.EqualTo(startAmmo), + "Mosin discharged ammo when the weapon should not have fired!"); + Assert.That(damageSystem.GetTotalDamage(ToServer(urist)), + Is.EqualTo(FixedPoint2.Zero), + "Urist took damage when the weapon should not have fired!"); + + await UseInHand(); + + Assert.That(wieldComp.Wielded, Is.True, "Mosin failed to wield when interacted with!"); + + await AttemptShoot(urist); + updatedAmmo = gunSystem.GetAmmoCount(mosinEnt); + + Assert.That(updatedAmmo, Is.EqualTo(startAmmo - 1), "Mosin failed to discharge appropriate amount of ammo!"); + Assert.That(damageSystem.GetTotalDamage(ToServer(urist)), + Is.GreaterThan(FixedPoint2.Zero), + "Mosin was fired but urist sustained no damage!"); + } +} diff --git a/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs b/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs index 920dc088186..d9801275b90 100644 --- a/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs +++ b/Content.IntegrationTests/Tests/Wires/WireLayoutTest.cs @@ -1,4 +1,5 @@ -using Content.Server.Doors; +using Content.IntegrationTests.Fixtures; +using Content.Server.Doors; using Content.Server.Power; using Content.Server.Wires; using Robust.Shared.GameObjects; @@ -10,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Wires; [TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(WiresSystem))] -public sealed class WireLayoutTest +public sealed class WireLayoutTest : GameTest { [TestPrototypes] public const string Prototypes = """ @@ -53,7 +54,7 @@ public sealed class WireLayoutTest [Test] public async Task TestLayoutInheritance() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var testMap = await pair.CreateTestMap(); @@ -89,8 +90,6 @@ await server.WaitAssertion(() => Assert.That(ent3.Comp.WiresList, Has.One.With.Property("Action").InstanceOf(), "1 door bolt wire"); }); }); - - await pair.CleanReturnAsync(); } private static Entity SpawnWithComp(IEntityManager entityManager, string prototype, MapCoordinates coords) diff --git a/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs index 891525a25b5..801c6c49b61 100644 --- a/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs +++ b/Content.IntegrationTests/Tests/WizdenContentFreeze/WizdenContentFreeze.cs @@ -1,3 +1,4 @@ +using Content.IntegrationTests.Fixtures; using Content.Shared.Kitchen; namespace Content.IntegrationTests.Tests.WizdenContentFreeze; @@ -5,7 +6,7 @@ namespace Content.IntegrationTests.Tests.WizdenContentFreeze; /// /// These tests are limited to adding a specific type of content, essentially freezing it. If you are a fork developer, you may want to disable these tests. /// -public sealed class WizdenContentFreeze +public sealed class WizdenContentFreeze : GameTest { /// /// This freeze prohibits the addition of new microwave recipes. @@ -18,7 +19,7 @@ public sealed class WizdenContentFreeze [Test] public async Task MicrowaveRecipesFreezeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var protoMan = server.ProtoMan; @@ -35,7 +36,5 @@ public async Task MicrowaveRecipesFreezeTest() { Assert.Fail($"Oh, you deleted the microwave recipes? YOU ARE SO COOL! Please lower the number of recipes in MicrowaveRecipesFreezeTest from {recipesLimit} to {recipesCount} so that future contributors cannot add new recipes back."); } - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/XenoArtifactTest.cs b/Content.IntegrationTests/Tests/XenoArtifactTest.cs index ac4c58c52cc..35b03470e9a 100644 --- a/Content.IntegrationTests/Tests/XenoArtifactTest.cs +++ b/Content.IntegrationTests/Tests/XenoArtifactTest.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.IntegrationTests.Fixtures; using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.Components; using Robust.Shared.GameObjects; @@ -6,7 +7,7 @@ namespace Content.IntegrationTests.Tests; [TestFixture] -public sealed class XenoArtifactTest +public sealed class XenoArtifactTest : GameTest { [TestPrototypes] private const string Prototypes = @" @@ -90,7 +91,7 @@ public sealed class XenoArtifactTest [Test] public async Task XenoArtifactAddNodeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -133,9 +134,6 @@ await server.WaitPost(() => Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(1)); Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(2)); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -144,7 +142,7 @@ await server.WaitPost(() => [Test] public async Task XenoArtifactRemoveNodeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -182,9 +180,6 @@ await server.WaitPost(() => Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Is.Empty); Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -193,7 +188,7 @@ await server.WaitPost(() => [Test] public async Task XenoArtifactResizeTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -245,9 +240,6 @@ await server.WaitPost(() => Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -256,7 +248,7 @@ await server.WaitPost(() => [Test] public async Task XenoArtifactReplaceTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -304,9 +296,6 @@ await server.WaitPost(() => Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } /// @@ -315,7 +304,7 @@ await server.WaitPost(() => [Test] public async Task XenoArtifactBuildActiveNodesTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -367,15 +356,12 @@ await server.WaitPost(() => Assert.That(artifactEnt.Comp.CachedActiveNodes, Has.Count.EqualTo(expectedActiveNodes.Length)); }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } [Test] public async Task XenoArtifactGenerateSegmentsTest() { - await using var pair = await PoolManager.GetServerClient(); + var pair = Pair; var server = pair.Server; var entManager = server.ResolveDependency(); @@ -412,8 +398,5 @@ await server.WaitPost(() => Assert.That(grouped[2].Count(), Is.LessThanOrEqualTo(2)); // maintain same width or, if we used 3 nodes on previous layer - we only have 1 left! }); - await server.WaitRunTicks(1); - - await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Zombie/ZombieMarkingTests.cs b/Content.IntegrationTests/Tests/Zombie/ZombieMarkingTests.cs new file mode 100644 index 00000000000..a3a81acd579 --- /dev/null +++ b/Content.IntegrationTests/Tests/Zombie/ZombieMarkingTests.cs @@ -0,0 +1,95 @@ +using System.Linq; +using Content.IntegrationTests.Tests.Interaction; +using Content.Server.Zombies; +using Content.Shared.Body; +using Content.Shared.Zombies; + +namespace Content.IntegrationTests.Tests.Zombie; + +[TestOf(typeof(ZombieSystem))] +public sealed class ZombieMarkingTests : InteractionTest +{ + protected override string PlayerPrototype => "MobVulpkanin"; + + [Test] + public async Task ProfileApplication() + { + await Server.WaitAssertion(() => + { + var zombie = SEntMan.System(); + var visualBody = SEntMan.System(); + zombie.ZombifyEntity(SPlayer); + var comp = SEntMan.GetComponent(SPlayer); + + if (!visualBody.TryGatherMarkingsData(SPlayer, + null, + out var profiles, + out _, + out _)) + { + Assert.Fail($"Failed to gather markings data for {SEntMan.ToPrettyString(SPlayer):SPlayer}"); + } + + foreach (var (organ, profile) in profiles) + { + Assert.That(profile.SkinColor, Is.EqualTo(comp.SkinColor), $"Organ {organ} has non-zombified skin color"); + Assert.That(profile.EyeColor, Is.EqualTo(comp.EyeColor), $"Organ {organ} has non-zombified skin color"); + } + }); + } + + [Test] + public async Task MarkingApplication() + { + await Server.WaitAssertion(() => + { + var visualBody = SEntMan.System(); + if (!visualBody.TryGatherMarkingsData(SPlayer, + null, + out _, + out _, + out var preZombieMarkings)) + { + Assert.Fail($"Failed to gather pre-zombie markings data for {SEntMan.ToPrettyString(SPlayer):SPlayer}"); + } + + var zombie = SEntMan.System(); + zombie.ZombifyEntity(SPlayer); + var comp = SEntMan.GetComponent(SPlayer); + + if (!visualBody.TryGatherMarkingsData(SPlayer, + null, + out _, + out _, + out var postZombieMarkings)) + { + Assert.Fail($"Failed to gather post-zombie markings data for {SEntMan.ToPrettyString(SPlayer):SPlayer}"); + } + + foreach (var (organ, layers) in postZombieMarkings) + { + Assert.That(preZombieMarkings, Does.ContainKey(organ), "Zombification added organs (it shouldn't)"); + Assert.That(preZombieMarkings[organ], Is.Not.SameAs(layers), "Zombification shouldn't mutate the existing data structures"); + + foreach (var (layer, markingSet) in layers) + { + Assert.That(preZombieMarkings[organ], Does.ContainKey(layer), "Zombification added layers (it shouldn't)"); + Assert.That(preZombieMarkings[organ][layer], Is.Not.SameAs(markingSet), "Zombification shouldn't mutate the existing data structures"); + Assert.That(preZombieMarkings[organ][layer], Has.Count.EqualTo(markingSet.Count), "Zombification shouldn't change the amount of markings"); + + if (!ZombieSystem.AdditionalZombieLayers.Contains(layer)) + continue; + + foreach (var (preMarking, postMarking) in preZombieMarkings[organ][layer].Zip(markingSet)) + { + Assert.That(preMarking, Is.Not.EqualTo(postMarking), $"Zombification should change marking {postMarking.MarkingId} on layer {layer}"); + foreach (var color in postMarking.MarkingColors) + { + Assert.That(color, Is.EqualTo(comp.SkinColor), $"Zombification should change {postMarking.MarkingId} on layer {layer} to the skin color"); + } + } + } + } + }); + } +} diff --git a/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs b/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs new file mode 100644 index 00000000000..14fae1e978e --- /dev/null +++ b/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs @@ -0,0 +1,100 @@ +using System.IO; +using System.Linq; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Utility; + +public static partial class GameDataScrounger +{ + /// + /// Returns all files in a given content location that match a pattern. + /// + /// The directory within the VFS to search + /// A glob pattern to use as a filter + /// Whether to search directories within the given directory. + /// A list of all files within the VFS directory matching the pattern. + public static string[] FilesInDirectory(string location, string pattern, bool recursive = true) + { + var path = GetContentPathOnDisk(location); + + return Directory.EnumerateFiles(path, + pattern ?? "*", + recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .ToArray(); + } + + /// + /// Returns all files in a given content location that match a pattern, as their VFS paths. + /// + /// The directory within the VFS to search + /// A glob pattern to use as a filter + /// Whether to search directories within the given directory. + /// A list of all file paths within the VFS directory matching the pattern. + public static ResPath[] FilesInDirectoryInVfs(string location, string pattern, bool recursive = true) + { + var path = GetContentPathOnDisk(location.TrimEnd('/')); + var resBasePath = ContentResources(); + + return Directory.EnumerateFiles(path, + pattern ?? "*", + recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .Select(x => ResPath.FromRelativeSystemPath(x.Remove(0, resBasePath.Length))) + .ToArray(); + } + + /// + /// Get the full directory path that the executable is located in. + /// + private static string GetExecutableDirectory() + { + // TODO: remove this shitty hack, either through making it less hardcoded into shared, + // or by making our file structure less spaghetti somehow. + var assembly = typeof(IResourceManager).Assembly; + var location = assembly.Location; + if (location == string.Empty) + { + // See https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?view=net-5.0#remarks + // This doesn't apply to us really because we don't do that kind of publishing, but whatever. + throw new InvalidOperationException("Cannot find path of executable."); + } + + return Path.GetDirectoryName(location)!; + } + + /// + /// Turns a relative path from the executable directory into a full path. + /// + private static string ExecutableRelativeFile(string file) + { + return Path.GetFullPath(Path.Combine(GetExecutableDirectory(), file)); + } + + /// + /// Mirrors a function in the engine for ease of maintenance. + /// + private static string FindContentRootDir() + { + return "../../"; + } + + /// + /// Mirrors a function in the engine for ease of maintenance. + /// + private static string ContentResources() + { + return ExecutableRelativeFile($"{FindContentRootDir()}Resources"); + } + + /// + /// Gets the real path of a path within the VFS. + /// + /// The path to get the real filesystem path of. + /// The real filesystem path. + public static string GetContentPathOnDisk(string path) + { + Assert.That(path, Does.StartWith("/"), "Path must be rooted."); + + return $"{ContentResources()}{path.ToString()}"; + } +} diff --git a/Content.IntegrationTests/Utility/GameDataScrounger.cs b/Content.IntegrationTests/Utility/GameDataScrounger.cs new file mode 100644 index 00000000000..764fd9dd9ab --- /dev/null +++ b/Content.IntegrationTests/Utility/GameDataScrounger.cs @@ -0,0 +1,369 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.IntegrationTests.Utility; + +/// +/// A helper class for when you need prototype or VFS data particularly early, like for test source lists. +/// +/// +/// +/// This does not include engine prototypes, nor anything generated at runtime, as it's made to be simple and fast +/// for usage during test framework startup where we cannot afford to initialize all of . +/// +/// +/// No initialization is required, it initializes itself on usage and all methods can be called immediately. +/// +/// +/// +/// +/// public static readonly string[] Maps = GameDataScrounger.PrototypesOfKind<GameMapPrototype>(); +/// +/// +public static partial class GameDataScrounger +{ + + /// + /// + /// YAML Linter, for Reasons, depends on the entirety of the test suite. + /// As such, scrounging erroring out due to bad YAML can make the linter fail spectacularly. + /// We do not want that, so the linter sets this, and we refuse to do any yaml-ing ourselves so the nicer set of + /// errors get to it. + /// + /// + /// Also, this means obviously bad YAML causes the main test suite to exit early. This is probably a pro, honestly. + /// + /// + /// + /// This should not be set mid-run, as some methods will return data and others will not depending on their implementation. + /// Set it before any operations that would call into GameDataScrounger. + /// + public static bool NoScrounging = false; + + /// + /// Prototype type to ID index. + /// + private static Dictionary>? _prototypeIndex = null; + + /// + /// Component type to prototype ID index. + /// + private static Dictionary>? _entitiesWithComponentIndex = null; + + /// + /// Entity proto to metadata index. + /// + private static Dictionary? _entitiesMetaIndex = null; + + private sealed class EntityMetadata + { + public required string Id; + public required HashSet Components; + public required List Parents; + public required bool Abstract; + } + + /// + /// Lock used to synchronize access to the prototype index. + /// + private static readonly Lock DataLock = new(); + + /// + /// Gets all prototypes of the given type kind. + /// + public static string[] PrototypesOfKind() + where T : IPrototype + { + if (typeof(T).GetCustomAttribute() is { Type: { } ty }) + return PrototypesOfKind(ty); + + return PrototypesOfKind(PrototypeUtility.CalculatePrototypeName(typeof(T).Name)); + } + + /// + /// Gets all prototypes of the given string kind. + /// + public static string[] PrototypesOfKind(string kind) + { + if (NoScrounging) + return Array.Empty(); + + lock (DataLock) + { + Scrounge(); + + return _prototypeIndex[kind].ToArray(); + } + } + + /// + /// Returns all entities with the given component. + /// + /// + /// This does not accept components by type due to a non-public engine API that needs unsealed. + /// When that is done, this will be removed and usages of it upstream will be refactored to go by type. + /// + /// The name (like one would put in YAML) of the component to look for. + /// A list of entity prototype IDs that have the given component. + public static string[] EntitiesWithComponent(string componentId) + { + if (NoScrounging) + return Array.Empty(); + + lock (DataLock) + { + if (_entitiesWithComponentIndex is { } index) + { + return index[componentId].ToArray(); + } + else + { + Scrounge(); + + return _entitiesWithComponentIndex[componentId].ToArray(); + } + } + } + + /// + /// Ensures that the internal indices are initialized and not null. + /// This will not do anything if they're initialized, and will initialize them to be empty if + /// is set. + /// + [MemberNotNull(nameof(_prototypeIndex))] + [MemberNotNull(nameof(_entitiesWithComponentIndex))] + [MemberNotNull(nameof(_entitiesMetaIndex))] + private static void Scrounge() + { + if (_prototypeIndex is not null && _entitiesWithComponentIndex is not null && _entitiesMetaIndex is not null) + return; + + // Initialize regardless of if we actually fill them out. + // As we promise they will not be null, and various methods rely on this even if they're mpty. + _prototypeIndex = new(); + _entitiesWithComponentIndex = new(); + _entitiesMetaIndex = new(); + + if (NoScrounging) + return; + + var resDir = ContentResources(); + Assert.That(Directory.Exists($"{resDir}/Prototypes")); + + var ignoreList = GetIgnoredPrototypes(resDir); + + // Start with our root directory. We use this as a stack of directories to traverse. + var explorationStack = new List() { $"{resDir}/Prototypes" }; + + while (explorationStack.Count > 0) + { + // Take a directory off the stack. + var dir = explorationStack.Pop(); + + if (ignoreList.Contains(dir)) + continue; // It's all abstract anyway. + + explorationStack.AddRange(Directory.EnumerateDirectories(dir)); + + foreach (var file in Directory.EnumerateFiles(dir, "*.yml")) + { + if (ignoreList.Contains(file)) + continue; // It's all abstract anyway. + + foreach (var (kind, id) in IndexPrototypesIn(file)) + { + // alternate universe where .net has rust's Entry api. + if (!_prototypeIndex.TryGetValue(kind, out var list)) + { + _prototypeIndex[kind] = new(); + list = _prototypeIndex[kind]; + } + + list.Add(id); + } + } + } + + PushInheritanceAndIndex(); + } + + private static readonly YamlScalarNode IdNode = new("id"); + private static readonly YamlScalarNode TypeNode = new("type"); + + /// + /// Indexes all prototypes in a folder, adding them to as necessary and + /// yielding all (type, id) pairs. + /// + /// The file to index. + /// An enumerator of all prototypes in the file, regardless of kind. + private static IEnumerable<(string type, string id)> IndexPrototypesIn(string file) + { + var stream = new YamlStream(); + + stream.Load(File.OpenText(file)); + + foreach (var document in stream) + { + Assert.That(document.RootNode, Is.AssignableTo()); + var node = (YamlSequenceNode)document.RootNode; + + foreach (var entry in node.Children) + { + Assert.That(entry, Is.AssignableTo()); + var entryMapping = (YamlMappingNode)entry; + + var id = entryMapping[IdNode]; + var type = entryMapping[TypeNode]; + var @abstract = false; + if (entryMapping.TryGetNode("abstract", out YamlScalarNode? abstractNode)) + { + // TODO: This technically will exclude prototypes that use the abstract field for their own stuff, + // and not for parenting. However no such prototype exists in the game as of writing and solving + // this is mildly nontrivial. + + // We use exact equality to match what serialization does. + if (abstractNode.Value == "true") + @abstract = true; + } + + if (!@abstract) + yield return (((YamlScalarNode)type).Value!, ((YamlScalarNode)id).Value!); + + // If we're an entity prototype.. + if (type is not YamlScalarNode { Value: "entity" }) + continue; + + // then do some metadata indexing that's feasible w/o serializationmanager. + + entryMapping.TryGetNode("components", out YamlSequenceNode? components); + + var parents = new List(); + + if (entryMapping.TryGetNode("parent", out var parentNode)) + { + switch (parentNode) + { + case YamlScalarNode scalar: + { + parents.Add(scalar.Value!); + break; + } + case YamlSequenceNode seq: + { + parents.AddRange(seq.Children.Select(x => x.AsString())); + break; + } + } + } + + + // Assemble metadata for this entity prototype w/o needing serializationmanager. + var entity = new EntityMetadata() + { + Abstract = @abstract, + Components = components?.Children.Select(x => x["type"].ToString()).ToHashSet() ?? new(), + Parents = parents, + Id = id.AsString(), + }; + + _entitiesMetaIndex![id.AsString()] = entity; + } + } + } + + /// + /// Iterates through , filling out + /// as it works and ensuring all inheritance-provided components are found. + /// + private static void PushInheritanceAndIndex() + { + var visitedEntities = new HashSet(); + + foreach (var entity in _entitiesMetaIndex!.Values) + { + VisitEntity(entity, visitedEntities); + + if (entity.Abstract) + continue; // We don't index abstract entities here. + + foreach (var component in entity.Components) + { + if (!_entitiesWithComponentIndex!.TryGetValue(component, out var list)) + { + list = new(); + _entitiesWithComponentIndex[component] = list; + } + + list.Add(entity.Id); + } + } + } + + /// + /// Visits the given entity, potentially recursively in order to discover all of its components. + /// + /// The entity to 'visit' and gather metadata on. + /// The set of all visited entities. + private static void VisitEntity(EntityMetadata entity, in HashSet visitedEntities) + { + // Return if we've visited already. + if (!visitedEntities.Add(entity.Id)) + return; + + foreach (var parent in entity.Parents) + { + var parentMeta = _entitiesMetaIndex![parent]; + VisitEntity(parentMeta, visitedEntities); + + entity.Components.UnionWith(parentMeta.Components); + } + } + + // Did you know there's no way to find the resources folder in the real filesystem + // from content? Makes sense, but ough. So this is unfortunately copy-pasted from engine. + // I don't think it's worth it to add an API for this to engine due to the security implications for sandboxed + // content. + + // This is indeed, unfortunately, a replica of Content.Shared/Entry/EntryPoint.cs:129 + // That code relies on engine tools we can't use here, because we can't even spin up engine. + private static HashSet GetIgnoredPrototypes(string resDir) + { + var ignores = new HashSet(); + var ignoredProtosPath = $"{resDir}/IgnoredPrototypes"; + + if (!Directory.Exists(ignoredProtosPath)) + return ignores; // Nothing to do. + + foreach (var path in Directory.EnumerateFiles($"{resDir}/IgnoredPrototypes")) + { + var stream = new YamlStream(); + + stream.Load(File.OpenText(path)); + + foreach (var document in stream) + { + if (document.RootNode is not YamlSequenceNode seq) + throw new Exception($"The ignored prototypes file at {path} isn't a valid yaml sequence/list."); + + foreach (var entry in seq) + { + if (entry is not YamlScalarNode { Value: {} value }) + throw new Exception($"An entry in {path} is not a valid YAML scalar/string literal. Entry: {entry}"); + + ignores.Add(value); + } + } + } + + return ignores; + } +} diff --git a/Content.IntegrationTests/Utility/TestProperties.cs b/Content.IntegrationTests/Utility/TestProperties.cs new file mode 100644 index 00000000000..c8ed338a829 --- /dev/null +++ b/Content.IntegrationTests/Utility/TestProperties.cs @@ -0,0 +1,9 @@ +namespace Content.IntegrationTests.Utility; + +public static class TestProperties +{ + /// + /// Name of the property describing what kind of test frame a test is running under. + /// + public const string TestFrameKind = "TestFrameKind"; +} diff --git a/Content.MapRenderer/Content.MapRenderer.csproj b/Content.MapRenderer/Content.MapRenderer.csproj index 43207177322..98fb446bd56 100644 --- a/Content.MapRenderer/Content.MapRenderer.csproj +++ b/Content.MapRenderer/Content.MapRenderer.csproj @@ -3,19 +3,27 @@ Exe ..\bin\Content.MapRenderer\ false - enable true + false + + - + + + + + + + diff --git a/Content.MapRenderer/Content.MapRenderer.csproj.lscache b/Content.MapRenderer/Content.MapRenderer.csproj.lscache new file mode 100644 index 00000000000..b6764667db8 --- /dev/null +++ b/Content.MapRenderer/Content.MapRenderer.csproj.lscache @@ -0,0 +1,361 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.MapRenderer +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.MapRenderer +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=../bin/Content.MapRenderer/Content.MapRenderer.dll +TargetRefPath=obj/Debug/ref/Content.MapRenderer.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.MapRenderer.dll +/refout:obj\Debug\refint\Content.MapRenderer.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +CommandLineArguments.cs +Extensions/ + DirectoryExtensions.cs + EnvironmentExtensions.cs +MapViewerData.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.MapRenderer.AssemblyInfo.cs +Painters/ + DecalData.cs + DecalPainter.cs + EntityData.cs + EntityPainter.cs + GridPainter.cs + MapPainter.cs + TilePainter.cs +ParallaxOutput.cs +Program.cs +RenderedGridImage.cs +RenderMap.cs + +[metadataReferences] +../ + Content.Client/obj/Debug/ref/Content.Client.dll + Content.IntegrationTests/obj/Debug/ref/Content.IntegrationTests.dll + Content.Packaging/obj/Debug/net10.0/ref/Content.Packaging.dll + Content.Server.Database/obj/Debug/ref/Content.Server.Database.dll + Content.Server/obj/Debug/ref/Content.Server.dll + Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll + Content.Shared/obj/Debug/ref/Content.Shared.dll + Content.Tests/obj/Debug/ref/Content.Tests.dll + RobustToolbox/ + Avalonia.Base/obj/Debug/net10.0/ref/Avalonia.Base.dll + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Client/obj/Debug/ref/Robust.Client.dll + Robust.LoaderApi/Robust.LoaderApi/obj/Debug/net10.0/ref/Robust.LoaderApi.dll + Robust.Packaging/obj/Debug/net10.0/ref/Robust.Packaging.dll + Robust.Server.Testing/obj/Debug/net10.0/ref/Robust.Server.Testing.dll + Robust.Server/obj/Debug/ref/Robust.Server.dll + Robust.Shared.Maths.Testing/obj/Debug/net10.0/ref/Robust.Shared.Maths.Testing.dll + Robust.Shared.Maths.Tests/obj/Debug/net10.0/ref/Robust.Shared.Maths.Tests.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared.Scripting/obj/Debug/ref/Robust.Shared.Scripting.dll + Robust.Shared.Testing/obj/Debug/net10.0/ref/Robust.Shared.Testing.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll + Robust.UnitTesting/obj/Debug/net10.0/ref/Robust.UnitTesting.dll + Robust.Xaml/bin/Debug/netstandard2.0/Robust.Xaml.dll + XamlX/src/ + XamlX.IL.Cecil/bin/Debug/netstandard2.0/XamlX.IL.Cecil.dll + XamlX/bin/Debug/netstandard2.0/XamlX.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + castle.core/5.1.1/lib/net6.0/Castle.Core.dll + microsoft.applicationinsights/2.23.0/lib/netstandard2.0/Microsoft.ApplicationInsights.dll + microsoft.codecoverage/18.0.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll + microsoft.data.sqlite.core/10.0.6/lib/net8.0/Microsoft.Data.Sqlite.dll + microsoft.entityframeworkcore.abstractions/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll + microsoft.entityframeworkcore.relational/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll + microsoft.entityframeworkcore.sqlite.core/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Sqlite.dll + microsoft.entityframeworkcore/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.dll + microsoft.extensions.caching.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + microsoft.extensions.caching.memory/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Memory.dll + microsoft.extensions.configuration.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll + microsoft.extensions.dependencyinjection.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll + microsoft.extensions.dependencyinjection/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll + microsoft.extensions.dependencymodel/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.extensions.http/3.1.0/lib/netcoreapp3.1/Microsoft.Extensions.Http.dll + microsoft.extensions.logging.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll + microsoft.extensions.logging/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.dll + microsoft.extensions.objectpool/10.0.0/lib/net10.0/Microsoft.Extensions.ObjectPool.dll + microsoft.extensions.options/10.0.6/lib/net10.0/Microsoft.Extensions.Options.dll + microsoft.extensions.primitives/10.0.6/lib/net10.0/Microsoft.Extensions.Primitives.dll + microsoft.testing.extensions.telemetry/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.Telemetry.dll + microsoft.testing.extensions.trxreport.abstractions/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.TrxReport.Abstractions.dll + microsoft.testing.extensions.vstestbridge/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.VSTestBridge.dll + microsoft.testing.platform.msbuild/1.9.0/lib/net9.0/Microsoft.Testing.Extensions.MSBuild.dll + microsoft.testing.platform/1.9.0/lib/net9.0/Microsoft.Testing.Platform.dll + microsoft.testplatform.adapterutilities/17.13.0/lib/net9.0/Microsoft.TestPlatform.AdapterUtilities.dll + microsoft.testplatform.testhost/18.0.1/lib/net8.0/ + Microsoft.TestPlatform.CommunicationUtilities.dll + Microsoft.TestPlatform.CoreUtilities.dll + Microsoft.TestPlatform.CrossPlatEngine.dll + Microsoft.TestPlatform.PlatformAbstractions.dll + Microsoft.TestPlatform.Utilities.dll + Microsoft.VisualStudio.TestPlatform.Common.dll + Microsoft.VisualStudio.TestPlatform.ObjectModel.dll + testhost.dll + moq/4.20.72/lib/net6.0/Moq.dll + netcord/1.0.0-alpha.388/lib/net9.0/NetCord.dll + nett/0.15.0/lib/netstandard2.0/Nett.dll + newtonsoft.json/13.0.4/lib/net6.0/Newtonsoft.Json.dll + npgsql.entityframeworkcore.postgresql/10.0.0/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll + npgsql/10.0.0/lib/net10.0/Npgsql.dll + nunit/4.4.0/lib/net8.0/ + nunit.framework.dll + nunit.framework.legacy.dll + pidgin/3.5.1/lib/net7.0/Pidgin.dll + prometheus-net/8.2.1/lib/net7.0/Prometheus.NetStandard.dll + robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + sixlabors.imagesharp/3.1.12/lib/net6.0/SixLabors.ImageSharp.dll + sqlitepclraw.config.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.batteries_v2.dll + sqlitepclraw.core/3.0.2/lib/netstandard2.0/SQLitePCLRaw.core.dll + sqlitepclraw.provider.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll + system.diagnostics.eventlog/9.0.0/lib/net9.0/System.Diagnostics.EventLog.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll +/ + benchmarkdotnet.annotations/0.15.8/analyzers/dotnet/roslyn4.8/cs/BenchmarkDotNet.Analyzers.dll + microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll + microsoft.entityframeworkcore.analyzers/10.0.6/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll + microsoft.extensions.logging.abstractions/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll + microsoft.extensions.options/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll + microsoft.net.illink.tasks/10.0.0/analyzers/dotnet/cs/ + ILLink.CodeFixProvider.dll + ILLink.RoslynAnalyzer.dll + nunit.analyzers/4.11.2/analyzers/dotnet/cs/ + nunit.analyzers.codefixes.dll + nunit.analyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.MapRenderer.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.MapRenderer/Program.cs b/Content.MapRenderer/Program.cs index 534b12565c1..668e04f63d1 100644 --- a/Content.MapRenderer/Program.cs +++ b/Content.MapRenderer/Program.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Content.IntegrationTests; using Content.MapRenderer.Painters; -using Content.Server.Maps; +using Content.Shared.Maps; using Robust.Shared.Prototypes; using Robust.UnitTesting.Pool; using SixLabors.ImageSharp; @@ -145,7 +145,7 @@ internal static async Task Main(string[] args) { Console.Write($"Following map files did not exist on disk directly, searching through prototypes: {string.Join(", ", lookupPrototypeFiles)}"); - await using var pair = await PoolManager.GetServerClient(); + await using var pair = await PoolManager.GetServerClient(testContext: testContext); var mapPrototypes = pair.Server .ResolveDependency() .EnumeratePrototypes() diff --git a/Content.MapRenderer/RenderMap.cs b/Content.MapRenderer/RenderMap.cs index 4ebf4ee5d4b..8d6087ec7ab 100644 --- a/Content.MapRenderer/RenderMap.cs +++ b/Content.MapRenderer/RenderMap.cs @@ -1,5 +1,5 @@ using System.IO; -using Content.Server.Maps; +using Content.Shared.Maps; using Robust.Shared.Prototypes; using Robust.Shared.Utility; diff --git a/Content.Packaging/ClientPackaging.cs b/Content.Packaging/ClientPackaging.cs index 21215d3bcbf..f17ebf5d20f 100644 --- a/Content.Packaging/ClientPackaging.cs +++ b/Content.Packaging/ClientPackaging.cs @@ -13,13 +13,13 @@ public static class ClientPackaging /// /// Be advised this can be called from server packaging during a HybridACZ build. /// - public static async Task PackageClient(bool skipBuild, string configuration, IPackageLogger logger) + public static async Task PackageClient(bool skipBuild, bool logBuild, string configuration, IPackageLogger logger) { logger.Info("Building client..."); if (!skipBuild) { - await ProcessHelpers.RunCheck(new ProcessStartInfo + var startInfo = new ProcessStartInfo { FileName = "dotnet", ArgumentList = @@ -33,7 +33,15 @@ await ProcessHelpers.RunCheck(new ProcessStartInfo "/p:FullRelease=true", "/m" } - }); + }; + + if (logBuild) + { + startInfo.ArgumentList.Add($"/bl:{Path.Combine("release", "client.binlog")}"); + startInfo.ArgumentList.Add("/p:ReportAnalyzer=true"); + } + + await ProcessHelpers.RunCheck(startInfo); } logger.Info("Packaging client..."); diff --git a/Content.Packaging/CommandLineArgs.cs b/Content.Packaging/CommandLineArgs.cs index 23f661921e0..ed5e5f2b29b 100644 --- a/Content.Packaging/CommandLineArgs.cs +++ b/Content.Packaging/CommandLineArgs.cs @@ -36,6 +36,11 @@ public sealed class CommandLineArgs /// public string Configuration { get; set; } + /// + /// Log builds with MSBuild binlog. Logs get saved to release/ + /// + public bool LogBuild { get; set; } + // CommandLineArgs, 3rd of her name. public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out CommandLineArgs? parsed) { @@ -44,6 +49,7 @@ public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out var skipBuild = false; var wipeRelease = true; var hybridAcz = false; + var logBuild = false; var configuration = "Release"; List? platforms = null; @@ -84,6 +90,10 @@ public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out { hybridAcz = true; } + else if (arg == "--log-build") + { + logBuild = true; + } else if (arg == "--platform") { if (!enumerator.MoveNext()) @@ -122,7 +132,7 @@ public static bool TryParse(IReadOnlyList args, [NotNullWhen(true)] out return false; } - parsed = new CommandLineArgs(client.Value, skipBuild, wipeRelease, hybridAcz, platforms, configuration); + parsed = new CommandLineArgs(client.Value, skipBuild, wipeRelease, hybridAcz, logBuild, platforms, configuration); return true; } @@ -137,6 +147,7 @@ private static void PrintHelp() --hybrid-acz Use HybridACZ for server builds. --platform Platform for server builds. Default will output several x64 targets. --configuration Configuration to use for building the server (Release, Debug, Tools). Default is Release. + --log-build Log builds with MSBuild binlog. Logs get saved to release/ "); } @@ -145,6 +156,7 @@ private CommandLineArgs( bool skipBuild, bool wipeRelease, bool hybridAcz, + bool logBuild, List? platforms, string configuration) { @@ -154,5 +166,6 @@ private CommandLineArgs( HybridAcz = hybridAcz; Platforms = platforms; Configuration = configuration; + LogBuild = logBuild; } } diff --git a/Content.Packaging/Content.Packaging.csproj b/Content.Packaging/Content.Packaging.csproj index 9823e40b8e3..d278b487345 100644 --- a/Content.Packaging/Content.Packaging.csproj +++ b/Content.Packaging/Content.Packaging.csproj @@ -2,13 +2,13 @@ Exe enable - enable True - - - + + + + diff --git a/Content.Packaging/Content.Packaging.csproj.lscache b/Content.Packaging/Content.Packaging.csproj.lscache new file mode 100644 index 00000000000..d6c04f3c91f --- /dev/null +++ b/Content.Packaging/Content.Packaging.csproj.lscache @@ -0,0 +1,261 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Packaging +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Packaging +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=bin/Debug/net10.0/Content.Packaging.dll +TargetRefPath=obj/Debug/net10.0/ref/Content.Packaging.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\net10.0\Content.Packaging.dll +/refout:obj\Debug\net10.0\refint\Content.Packaging.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +ClientPackaging.cs +CommandLineArgs.cs +DepsHandler.cs +obj/Debug/net10.0/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Packaging.AssemblyInfo.cs + Content.Packaging.GlobalUsings.g.cs +Program.cs +ServerPackaging.cs +SharedPackaging.cs + +[metadataReferences] +../RobustToolbox/ + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Packaging/obj/Debug/net10.0/ref/Robust.Packaging.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/net10.0/Content.Packaging.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Packaging/DepsHandler.cs b/Content.Packaging/DepsHandler.cs new file mode 100644 index 00000000000..9907b97ba9d --- /dev/null +++ b/Content.Packaging/DepsHandler.cs @@ -0,0 +1,80 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Content.Packaging; + +/// +/// Helper class for working with .deps.json files. +/// +public sealed class DepsHandler +{ + public readonly Dictionary Libraries = new(); + + public DepsHandler(DepsData data) + { + if (data.Targets.Count != 1) + throw new Exception("Expected exactly one target"); + + var target = data.Targets.Single().Value; + + foreach (var (libNameAndVersion, libInfo) in target) + { + var split = libNameAndVersion.Split('/', 2); + + Libraries.Add(split[0], libInfo); + } + } + + public static DepsHandler Load(string depsFile) + { + using var f = File.OpenRead(depsFile); + var depsData = JsonSerializer.Deserialize(f) ?? throw new InvalidOperationException("Deps are null!"); + + return new DepsHandler(depsData); + } + + public HashSet RecursiveGetLibrariesFrom(string start) + { + var found = new HashSet(); + + RecursiveAddLibraries(start, found); + + return found; + } + + private void RecursiveAddLibraries(string start, HashSet set) + { + if (!set.Add(start)) + return; + + var lib = Libraries[start]; + if (lib.Dependencies == null) + return; + + foreach (var dep in lib.Dependencies.Keys) + { + RecursiveAddLibraries(dep, set); + } + } + + public sealed class DepsData + { + [JsonInclude, JsonPropertyName("targets")] + public required Dictionary> Targets; + } + + public sealed class LibraryInfo + { + [JsonInclude, JsonPropertyName("dependencies")] + public Dictionary? Dependencies; + + [JsonInclude, JsonPropertyName("runtime")] + public Dictionary? Runtime; + + // Paths are like lib/netstandard2.0/JetBrains.Annotations.dll + public IEnumerable GetDllNames() + { + return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]); + } + } +} diff --git a/Content.Packaging/Program.cs b/Content.Packaging/Program.cs index 9457e9dacc9..25fc550a2fd 100644 --- a/Content.Packaging/Program.cs +++ b/Content.Packaging/Program.cs @@ -22,11 +22,11 @@ if (parsed.Client) { - await ClientPackaging.PackageClient(parsed.SkipBuild, parsed.Configuration, logger); + await ClientPackaging.PackageClient(parsed.SkipBuild, parsed.LogBuild, parsed.Configuration, logger); } else { - await ServerPackaging.PackageServer(parsed.SkipBuild, parsed.HybridAcz, logger, parsed.Configuration, parsed.Platforms); + await ServerPackaging.PackageServer(parsed.SkipBuild, parsed.HybridAcz, parsed.LogBuild, logger, parsed.Configuration, parsed.Platforms); } void WipeBin() diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs index a15dc7244f6..b64e02ced75 100644 --- a/Content.Packaging/ServerPackaging.cs +++ b/Content.Packaging/ServerPackaging.cs @@ -19,9 +19,6 @@ public static class ServerPackaging new PlatformReg("osx-x64", "MacOS", true), new PlatformReg("osx-arm64", "MacOS", true), // Non-default platforms (i.e. for Watchdog Git) - new PlatformReg("win-x86", "Windows", false), - new PlatformReg("linux-x86", "Linux", false), - new PlatformReg("linux-arm", "Linux", false), new PlatformReg("freebsd-x64", "FreeBSD", false), }; @@ -40,25 +37,9 @@ public static class ServerPackaging .Select(o => o.Rid) .ToList(); - private static readonly List ServerContentAssemblies = new() - { - "Content.Server.Database", - "Content.Server", - "Content.Shared", - "Content.Shared.Database", - }; - - private static readonly List ServerExtraAssemblies = new() - { - // Python script had Npgsql. though we want Npgsql.dll as well soooo - "Npgsql", - "Microsoft", - "NetCord", - }; - private static readonly List ServerNotExtraAssemblies = new() { - "Microsoft.CodeAnalysis", + "JetBrains.Annotations", }; private static readonly HashSet BinSkipFolders = new() @@ -79,7 +60,7 @@ public static class ServerPackaging "zh-Hant" }; - public static async Task PackageServer(bool skipBuild, bool hybridAcz, IPackageLogger logger, string configuration, List? platforms = null) + public static async Task PackageServer(bool skipBuild, bool hybridAcz, bool logBuild, IPackageLogger logger, string configuration, List? platforms = null) { if (platforms == null) { @@ -92,7 +73,7 @@ public static async Task PackageServer(bool skipBuild, bool hybridAcz, IPackageL // Rather than hosting the client ZIP on the watchdog or on a separate server, // Hybrid ACZ uses the ACZ hosting functionality to host it as part of the status host, // which means that features such as automatic UPnP forwarding still work properly. - await ClientPackaging.PackageClient(skipBuild, configuration, logger); + await ClientPackaging.PackageClient(skipBuild, logBuild, configuration, logger); } // Good variable naming right here. @@ -101,17 +82,22 @@ public static async Task PackageServer(bool skipBuild, bool hybridAcz, IPackageL if (!platforms.Contains(platform.Rid)) continue; - await BuildPlatform(platform, skipBuild, hybridAcz, configuration, logger); + await BuildPlatform(platform, skipBuild, hybridAcz, logBuild, configuration, logger); } } - private static async Task BuildPlatform(PlatformReg platform, bool skipBuild, bool hybridAcz, string configuration, IPackageLogger logger) + private static async Task BuildPlatform(PlatformReg platform, + bool skipBuild, + bool hybridAcz, + bool logBuild, + string configuration, + IPackageLogger logger) { logger.Info($"Building project for {platform.TargetOs}..."); if (!skipBuild) { - await ProcessHelpers.RunCheck(new ProcessStartInfo + var startInfo = new ProcessStartInfo { FileName = "dotnet", ArgumentList = @@ -126,7 +112,15 @@ await ProcessHelpers.RunCheck(new ProcessStartInfo "/p:FullRelease=true", "/m" } - }); + }; + + if (logBuild) + { + startInfo.ArgumentList.Add($"/bl:{Path.Combine("release", $"server-{platform.Rid}.binlog")}"); + startInfo.ArgumentList.Add("/p:ReportAnalyzer=true"); + } + + await ProcessHelpers.RunCheck(startInfo); await PublishClientServer(platform.Rid, platform.TargetOs, configuration); } @@ -184,23 +178,13 @@ private static async Task WriteServerResources( var inputPassCore = graph.InputCore; var inputPassResources = graph.InputResources; - var contentAssemblies = new List(ServerContentAssemblies); // Additional assemblies that need to be copied such as EFCore. var sourcePath = Path.Combine(contentDir, "bin", "Content.Server"); - // Should this be an asset pass? - // For future archaeologists I just want audio rework to work and need the audio pass so - // just porting this as is from python. - foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories)) - { - var fileName = Path.GetFileNameWithoutExtension(fullPath); + var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json")); - if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o))) - { - contentAssemblies.Add(fileName); - } - } + var contentAssemblies = GetContentAssemblyNamesToCopy(deps); await RobustSharedPackaging.DoResourceCopy( Path.Combine("RobustToolbox", "bin", "Server", @@ -232,5 +216,21 @@ await RobustServerPackaging.WriteServerResources( inputPassResources.InjectFinished(); } + // This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql) + private static IEnumerable GetContentAssemblyNamesToCopy(DepsHandler deps) + { + var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames); + var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames); + + var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet(); + + // Remove .dll suffix and apply filtering. + var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith)); + + return names; + + IEnumerable GetLibraryNames(string library) => deps.Libraries[library].GetDllNames(); + } + private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault); } diff --git a/Content.PatreonParser/Content.PatreonParser.csproj b/Content.PatreonParser/Content.PatreonParser.csproj index 1724ec0cea2..bba47a062d0 100644 --- a/Content.PatreonParser/Content.PatreonParser.csproj +++ b/Content.PatreonParser/Content.PatreonParser.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable true diff --git a/Content.PatreonParser/Content.PatreonParser.csproj.lscache b/Content.PatreonParser/Content.PatreonParser.csproj.lscache new file mode 100644 index 00000000000..d1c3b17bac9 --- /dev/null +++ b/Content.PatreonParser/Content.PatreonParser.csproj.lscache @@ -0,0 +1,258 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.PatreonParser +CommandLineArgsForDesignTimeEvaluation=-langversion:14.0 -define:TRACE +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.PatreonParser +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=bin/Debug/net10.0/Content.PatreonParser.dll +TargetRefPath=obj/Debug/net10.0/ref/Content.PatreonParser.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:TRACE;DEBUG;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\net10.0\Content.PatreonParser.dll +/refout:obj\Debug\net10.0\refint\Content.PatreonParser.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14.0 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +Attributes.cs +CurrentlyEntitledTiers.cs +Data.cs +Included.cs +obj/Debug/net10.0/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.PatreonParser.AssemblyInfo.cs + Content.PatreonParser.GlobalUsings.g.cs +Patron.cs +Program.cs +Relationships.cs +Root.cs +Row.cs +TierData.cs + +[metadataReferences] +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/csvhelper/33.1.0/lib/net9.0/CsvHelper.dll + +[analyzerReferences] +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.ComInterfaceGenerator.dll + Microsoft.Interop.JavaScript.JSImportGenerator.dll + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/net10.0/Content.PatreonParser.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Replay/Content.Replay.csproj b/Content.Replay/Content.Replay.csproj index 28cfc7f6666..5008214522a 100644 --- a/Content.Replay/Content.Replay.csproj +++ b/Content.Replay/Content.Replay.csproj @@ -1,26 +1,23 @@ - $(TargetFramework) - 12 - false false ..\bin\Content.Replay\ Exe RA0032;nullable - enable + + - - - - + + + diff --git a/Content.Replay/Content.Replay.csproj.lscache b/Content.Replay/Content.Replay.csproj.lscache new file mode 100644 index 00000000000..3f53a4086f7 --- /dev/null +++ b/Content.Replay/Content.Replay.csproj.lscache @@ -0,0 +1,290 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Replay +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Replay +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=../bin/Content.Replay/Content.Replay.dll +TargetRefPath=obj/Debug/ref/Content.Replay.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.Replay.dll +/refout:obj\Debug\refint\Content.Replay.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:RA0032,nullable,SYSLIB0011 + +[sourceFiles] +EntryPoint.cs +GlobalUsings.cs +Menu/ + ReplayMainMenu.cs + ReplayMainMenuControl.xaml.cs + SelectReplayWindow.xaml.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Replay.AssemblyInfo.cs +Program.cs + +[metadataReferences] +../ + Content.Client/obj/Debug/ref/Content.Client.dll + Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll + Content.Shared/obj/Debug/ref/Content.Shared.dll + RobustToolbox/ + Avalonia.Base/obj/Debug/net10.0/ref/Avalonia.Base.dll + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Client/obj/Debug/ref/Robust.Client.dll + Robust.LoaderApi/Robust.LoaderApi/obj/Debug/net10.0/ref/Robust.LoaderApi.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared.Scripting/obj/Debug/ref/Robust.Shared.Scripting.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll + Robust.Xaml/bin/Debug/netstandard2.0/Robust.Xaml.dll + XamlX/src/ + XamlX.IL.Cecil/bin/Debug/netstandard2.0/XamlX.IL.Cecil.dll + XamlX/bin/Debug/netstandard2.0/XamlX.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + jetbrains.annotations/2025.2.4/lib/netstandard2.0/JetBrains.Annotations.dll + nett/0.15.0/lib/netstandard2.0/Nett.dll + pidgin/3.5.1/lib/net7.0/Pidgin.dll + robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + sixlabors.imagesharp/3.1.12/lib/net6.0/SixLabors.ImageSharp.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Client.NameGenerator/bin/Debug/netstandard2.0/Robust.Client.NameGenerator.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll +/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll +/microsoft.net.illink.tasks/10.0.0/analyzers/dotnet/cs/ + ILLink.CodeFixProvider.dll + ILLink.RoslynAnalyzer.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.Replay.GeneratedMSBuildEditorConfig.editorconfig + +[additionalFiles] +Menu/ + ReplayMainMenuControl.xaml + SelectReplayWindow.xaml diff --git a/Content.Replay/EntryPoint.cs b/Content.Replay/EntryPoint.cs index ed6460a7e75..6a6658f5eff 100644 --- a/Content.Replay/EntryPoint.cs +++ b/Content.Replay/EntryPoint.cs @@ -19,8 +19,8 @@ public sealed class EntryPoint : GameClient public override void Init() { base.Init(); - IoCManager.BuildGraph(); - IoCManager.InjectDependencies(this); + Dependencies.BuildGraph(); + Dependencies.InjectDependencies(this); } public override void PostInit() diff --git a/Content.Replay/Menu/ReplayMainMenuControl.xaml b/Content.Replay/Menu/ReplayMainMenuControl.xaml index 9eeafd95dea..ef2f97ce3dd 100644 --- a/Content.Replay/Menu/ReplayMainMenuControl.xaml +++ b/Content.Replay/Menu/ReplayMainMenuControl.xaml @@ -44,7 +44,7 @@ MinSize="300 150"> diff --git a/Content.Server.Database/Content.Server.Database.csproj b/Content.Server.Database/Content.Server.Database.csproj index d98d0642db0..22b718b3636 100644 --- a/Content.Server.Database/Content.Server.Database.csproj +++ b/Content.Server.Database/Content.Server.Database.csproj @@ -1,16 +1,13 @@ - - $(TargetFramework) - 12 - false false ..\bin\Content.Server.Database\ true - enable RA0003 + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Content.Server.Database/Content.Server.Database.csproj.lscache b/Content.Server.Database/Content.Server.Database.csproj.lscache new file mode 100644 index 00000000000..613f0ee6b00 --- /dev/null +++ b/Content.Server.Database/Content.Server.Database.csproj.lscache @@ -0,0 +1,515 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Server.Database +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Server.Database +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=../bin/Content.Server.Database/Content.Server.Database.dll +TargetRefPath=obj/Debug/ref/Content.Server.Database.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:RA0003,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.Server.Database.dll +/refout:obj\Debug\refint\Content.Server.Database.dll +/target:library +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +DesignTimeContextFactories.cs +Migrations/Postgres/ + 20200929113117_Init.cs + 20200929113117_Init.Designer.cs + 20201006223000_SelectedCharacterSlotFk.cs + 20201028210620_Admins.cs + 20201028210620_Admins.Designer.cs + 20201109092921_ExtraIndices.cs + 20201109092921_ExtraIndices.Designer.cs + 20201203093409_ClothingAndPronouns.cs + 20201203093409_ClothingAndPronouns.Designer.cs + 20210103151756_BackpackPreference.cs + 20210103151756_BackpackPreference.Designer.cs + 20210211211033_AdminOOCColor.cs + 20210211211033_AdminOOCColor.Designer.cs + 20210321230012_HWID.cs + 20210321230012_HWID.Designer.cs + 20210915093340_UniqueHighPriorityJob.cs + 20210915093340_UniqueHighPriorityJob.Designer.cs + 20211120202701_AdminLogs.cs + 20211120202701_AdminLogs.Designer.cs + 20211121123543_AdminLogsImpact.cs + 20211121123543_AdminLogsImpact.Designer.cs + 20220103235647_whitelist.cs + 20220103235647_whitelist.Designer.cs + 20220108185749_add-species.cs + 20220108185749_add-species.Designer.cs + 20220127170845_UnifyMore.cs + 20220127170845_UnifyMore.Designer.cs + 20220129005644_ServerBanHit.cs + 20220129005644_ServerBanHit.Designer.cs + 20220214061058_RoleBans.cs + 20220214061058_RoleBans.Designer.cs + 20220308222742_Cleanup.cs + 20220308222742_Cleanup.Designer.cs + 20220310173734_SpeciesMarkings.cs + 20220310173734_SpeciesMarkings.Designer.cs + 20220313151800_ServerNameFts.cs + 20220313151800_ServerNameFts.Designer.cs + 20220324144654_AdminNotes.cs + 20220324144654_AdminNotes.Designer.cs + 20220325170225_PlayerReadRules.cs + 20220325170225_PlayerReadRules.Designer.cs + 20220326104916_UploadedResourcesLog.cs + 20220326104916_UploadedResourcesLog.Designer.cs + 20220503222955_FixIndices.cs + 20220503222955_FixIndices.Designer.cs + 20220505084828_MarkingsJsonb.cs + 20220505084828_MarkingsJsonb.Designer.cs + 20220509060724_FlavorText.cs + 20220509060724_FlavorText.Designer.cs + 20220724000132_PlayTime.cs + 20220724000132_PlayTime.Designer.cs + 20220816163319_Traits.cs + 20220816163319_Traits.Designer.cs + 20230319110655_ProfileTraitIndexUnique.cs + 20230319110655_ProfileTraitIndexUnique.Designer.cs + 20230319112124_ServerBanExemption.cs + 20230319112124_ServerBanExemption.Designer.cs + 20230402214647_BanAutoDelete.cs + 20230402214647_BanAutoDelete.Designer.cs + 20230503001749_AdminNotesImprovement.cs + 20230503001749_AdminNotesImprovement.Designer.cs + 20230725193102_AdminNotesImprovementsForeignKeys.cs + 20230725193102_AdminNotesImprovementsForeignKeys.Designer.cs + 20230727190902_AdminLogCompoundKey.cs + 20230727190902_AdminLogCompoundKey.Designer.cs + 20231021071411_RoundStartDate.cs + 20231021071411_RoundStartDate.Designer.cs + 20231024041204_DropAdminLogEntity.cs + 20231024041204_DropAdminLogEntity.Designer.cs + 20231126234054_ConnectionLogServer.cs + 20231126234054_ConnectionLogServer.Designer.cs + 20231226154937_AdminLogPk.cs + 20231226154937_AdminLogPk.Designer.cs + 20240112194620_Blacklist.cs + 20240112194620_Blacklist.Designer.cs + 20240201091301_SpawnPriorityPreference.cs + 20240201091301_SpawnPriorityPreference.Designer.cs + 20240301130641_ClothingRemoval.cs + 20240301130641_ClothingRemoval.Designer.cs + 20240318022005_AdminMessageDismiss.cs + 20240318022005_AdminMessageDismiss.Designer.cs + 20240403072242_Loadouts.cs + 20240403072242_Loadouts.Designer.cs + 20240409013837_FixRoundStartDateNullability.cs + 20240409013837_FixRoundStartDateNullability.Designer.cs + 20240531011555_RoleWhitelist.cs + 20240531011555_RoleWhitelist.Designer.cs + 20240606065731_RemoveLastReadRules.cs + 20240606065731_RemoveLastReadRules.Designer.cs + 20240606121555_ban_notify_trigger.cs + 20240606121555_ban_notify_trigger.Designer.cs + 20240606175154_ReturnLastReadRules.cs + 20240606175154_ReturnLastReadRules.Designer.cs + 20240621120713_ConnectionLogTimeIndex.cs + 20240621120713_ConnectionLogTimeIndex.Designer.cs + 20240623005121_BanTemplate.cs + 20240623005121_BanTemplate.Designer.cs + 20241111170112_ModernHwid.cs + 20241111170112_ModernHwid.Designer.cs + 20241111193608_ConnectionTrust.cs + 20241111193608_ConnectionTrust.Designer.cs + 20241122174243_IPIntel.cs + 20241122174243_IPIntel.Designer.cs + 20241223235939_AdminStatus.cs + 20241223235939_AdminStatus.Designer.cs + 20250211131539_LoadoutNames.cs + 20250211131539_LoadoutNames.Designer.cs + 20250314222016_ConstructionFavorites.cs + 20250314222016_ConstructionFavorites.Designer.cs + 20251120192733_NrpViolation.cs + 20251120192733_NrpViolation.Designer.cs + 20260118084629_OrganMarkings.cs + 20260118084629_OrganMarkings.Designer.cs + 20260120200503_BanRefactor.cs + 20260120200503_BanRefactor.Designer.cs + PostgresServerDbContextModelSnapshot.cs +Migrations/Sqlite/ + 20200929113112_Init.cs + 20200929113112_Init.Designer.cs + 20201028210616_Admins.cs + 20201028210616_Admins.Designer.cs + 20201109092917_ExtraIndices.cs + 20201109092917_ExtraIndices.Designer.cs + 20201203093351_ClothingAndPronouns.cs + 20201203093351_ClothingAndPronouns.Designer.cs + 20210103151752_BackpackPreference.cs + 20210103151752_BackpackPreference.Designer.cs + 20210211211028_AdminOOCColor.cs + 20210211211028_AdminOOCColor.Designer.cs + 20210321225959_HWID.cs + 20210321225959_HWID.Designer.cs + 20210915093336_UniqueHighPriorityJob.cs + 20210915093336_UniqueHighPriorityJob.Designer.cs + 20211120202654_AdminLogs.cs + 20211120202654_AdminLogs.Designer.cs + 20211121123536_AdminLogsImpact.cs + 20211121123536_AdminLogsImpact.Designer.cs + 20220103235637_whitelist.cs + 20220103235637_whitelist.Designer.cs + 20220108185734_add-species.cs + 20220108185734_add-species.Designer.cs + 20220127165122_UnifyMore.cs + 20220127165122_UnifyMore.Designer.cs + 20220129005638_ServerBanHit.cs + 20220129005638_ServerBanHit.Designer.cs + 20220214060745_RoleBans.cs + 20220214060745_RoleBans.Designer.cs + 20220308222736_Cleanup.cs + 20220308222736_Cleanup.Designer.cs + 20220310173728_SpeciesMarkings.cs + 20220310173728_SpeciesMarkings.Designer.cs + 20220313151753_ServerNameFts.cs + 20220313151753_ServerNameFts.Designer.cs + 20220324144649_AdminNotes.cs + 20220324144649_AdminNotes.Designer.cs + 20220325170220_PlayerReadRules.cs + 20220325170220_PlayerReadRules.Designer.cs + 20220326104908_UploadedResourcesLog.cs + 20220326104908_UploadedResourcesLog.Designer.cs + 20220503222949_FixIndices.cs + 20220503222949_FixIndices.Designer.cs + 20220505084819_MarkingsJsonb.cs + 20220505084819_MarkingsJsonb.Designer.cs + 20220509060717_FlavorText.cs + 20220509060717_FlavorText.Designer.cs + 20220724000127_PlayTime.cs + 20220724000127_PlayTime.Designer.cs + 20220816163313_Traits.cs + 20220816163313_Traits.Designer.cs + 20230319110651_ProfileTraitIndexUnique.cs + 20230319110651_ProfileTraitIndexUnique.Designer.cs + 20230319112120_ServerBanExemption.cs + 20230319112120_ServerBanExemption.Designer.cs + 20230402214642_BanAutoDelete.cs + 20230402214642_BanAutoDelete.Designer.cs + 20230503001739_AdminNotesImprovement.cs + 20230503001739_AdminNotesImprovement.Designer.cs + 20230725193058_AdminNotesImprovementsForeignKeys.cs + 20230725193058_AdminNotesImprovementsForeignKeys.Designer.cs + 20230727190858_AdminLogCompoundKey.cs + 20230727190858_AdminLogCompoundKey.Designer.cs + 20231021071407_RoundStartDate.cs + 20231021071407_RoundStartDate.Designer.cs + 20231024041159_DropAdminLogEntity.cs + 20231024041159_DropAdminLogEntity.Designer.cs + 20231126234049_ConnectionLogServer.cs + 20231126234049_ConnectionLogServer.Designer.cs + 20231226154929_AdminLogPk.cs + 20231226154929_AdminLogPk.Designer.cs + 20240112194612_Blacklist.cs + 20240112194612_Blacklist.Designer.cs + 20240201091254_SpawnPriorityPreference.cs + 20240201091254_SpawnPriorityPreference.Designer.cs + 20240301130602_ClothingRemoval.cs + 20240301130602_ClothingRemoval.Designer.cs + 20240318021959_AdminMessageDismiss.cs + 20240318021959_AdminMessageDismiss.Designer.cs + 20240403072258_Loadouts.cs + 20240403072258_Loadouts.Designer.cs + 20240409013832_FixRoundStartDateNullability.cs + 20240409013832_FixRoundStartDateNullability.Designer.cs + 20240409014937_FixRoundStartDateNullability2.cs + 20240409014937_FixRoundStartDateNullability2.Designer.cs + 20240531011549_RoleWhitelist.cs + 20240531011549_RoleWhitelist.Designer.cs + 20240606065717_RemoveLastReadRules.cs + 20240606065717_RemoveLastReadRules.Designer.cs + 20240606175141_ReturnLastReadRules.cs + 20240606175141_ReturnLastReadRules.Designer.cs + 20240621120705_ConnectionLogTimeIndex.cs + 20240621120705_ConnectionLogTimeIndex.Designer.cs + 20240623005113_BanTemplate.cs + 20240623005113_BanTemplate.Designer.cs + 20241111170107_ModernHwid.cs + 20241111170107_ModernHwid.Designer.cs + 20241111193602_ConnectionTrust.cs + 20241111193602_ConnectionTrust.Designer.cs + 20241122174236_IPIntel.cs + 20241122174236_IPIntel.Designer.cs + 20241223235932_AdminStatus.cs + 20241223235932_AdminStatus.Designer.cs + 20250211131517_LoadoutNames.cs + 20250211131517_LoadoutNames.Designer.cs + 20250314222717_ConstructionFavorites.cs + 20250314222717_ConstructionFavorites.Designer.cs + 20251120192726_NrpViolation.cs + 20251120192726_NrpViolation.Designer.cs + 20260118084622_OrganMarkings.cs + 20260118084622_OrganMarkings.Designer.cs + 20260120200455_BanRefactor.cs + 20260120200455_BanRefactor.Designer.cs + SqliteServerDbContextModelSnapshot.cs +Model.Ban.cs +Model.cs +ModelPostgres.cs +ModelSqlite.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Server.Database.AssemblyInfo.cs +SnakeCaseNaming.cs + +[metadataReferences] +../Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + microsoft.data.sqlite.core/10.0.6/lib/net8.0/Microsoft.Data.Sqlite.dll + microsoft.entityframeworkcore.abstractions/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll + microsoft.entityframeworkcore.relational/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll + microsoft.entityframeworkcore.sqlite.core/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Sqlite.dll + microsoft.entityframeworkcore/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.dll + microsoft.extensions.caching.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + microsoft.extensions.caching.memory/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Memory.dll + microsoft.extensions.configuration.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll + microsoft.extensions.dependencyinjection.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll + microsoft.extensions.dependencyinjection/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll + microsoft.extensions.dependencymodel/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.extensions.logging.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll + microsoft.extensions.logging/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.dll + microsoft.extensions.options/10.0.6/lib/net10.0/Microsoft.Extensions.Options.dll + microsoft.extensions.primitives/10.0.6/lib/net10.0/Microsoft.Extensions.Primitives.dll + npgsql.entityframeworkcore.postgresql/10.0.0/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll + npgsql/10.0.0/lib/net10.0/Npgsql.dll + sqlitepclraw.config.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.batteries_v2.dll + sqlitepclraw.core/3.0.2/lib/netstandard2.0/SQLitePCLRaw.core.dll + sqlitepclraw.provider.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll +/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll +/ + microsoft.entityframeworkcore.analyzers/10.0.6/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll + microsoft.extensions.logging.abstractions/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll + microsoft.extensions.options/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.Server.Database.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs new file mode 100644 index 00000000000..a8d031faa0f --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs @@ -0,0 +1,2125 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20260118084629_OrganMarkings")] + partial class OrganMarkings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("boolean") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("boolean") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ipintel_cache_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("real") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ServerBanId") + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ServerRoleBanId") + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs new file mode 100644 index 00000000000..ec2b926f165 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class OrganMarkings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "organ_markings", + table: "profile", + type: "jsonb", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "organ_markings", + table: "profile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs new file mode 100644 index 00000000000..62dde10b99c --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.Designer.cs @@ -0,0 +1,2125 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20260120200503_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("boolean") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("boolean") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_template_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("interval") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("real") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ipintel_cache_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("real") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("integer") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs new file mode 100644 index 00000000000..6e6a9dfbe51 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260120200503_BanRefactor.cs @@ -0,0 +1,535 @@ +using System; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + type = table.Column(type: "smallint", nullable: false), + playtime_at_note = table.Column(type: "interval", nullable: false), + ban_time = table.Column(type: "timestamp with time zone", nullable: false), + expiration_time = table.Column(type: "timestamp with time zone", nullable: true), + reason = table.Column(type: "text", nullable: false), + severity = table.Column(type: "integer", nullable: false), + banning_admin = table.Column(type: "uuid", nullable: true), + last_edited_by_id = table.Column(type: "uuid", nullable: true), + last_edited_at = table.Column(type: "timestamp with time zone", nullable: true), + exempt_flags = table.Column(type: "integer", nullable: false), + auto_delete = table.Column(type: "boolean", nullable: false), + hidden = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + address = table.Column(type: "inet", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.CheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + hwid = table.Column(type: "bytea", nullable: false), + hwid_type = table.Column(type: "integer", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "uuid", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_type = table.Column(type: "text", nullable: false), + role_id = table.Column(type: "text", nullable: false), + ban_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + round_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ban_id = table.Column(type: "integer", nullable: false), + unbanning_admin = table.Column(type: "uuid", nullable: true), + unban_time = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.Sql(""" + CREATE INDEX "IX_ban_address_address" + ON ban_address + USING gist + (address inet_ops) + INCLUDE (ban_id); + + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql($""" + -- REMOVE: + -- TRUNCATE ban RESTART IDENTITY CASCADE; + + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, {(int)BanType.Server}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Update ID sequence to be after newly inserted IDs. + SELECT setval('ban_ban_id_seq', (SELECT MAX(ban_id) FROM ban)); + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_merge_map; + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE) ON COMMIT DROP; + -- TRUNCATE _role_ban_import_id_map; + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND date_trunc('second', main.ban_time, 'utc') = date_trunc('second', sub.ban_time, 'utc') + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR date_trunc('minute', main.expiration_time, 'utc') = date_trunc('minute', sub.expiration_time, 'utc') + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT INTO + _role_ban_import_id_map + SELECT + DISTINCT ON (merge_id) + nextval('ban_ban_id_seq'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, {(int)BanType.Role}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + split_part(role_id, ':', 1), split_part(role_id, ':', 2), im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + + migrationBuilder.Sql($""" + CREATE OR REPLACE FUNCTION send_server_ban_notification() + RETURNS trigger AS $$ + BEGIN + PERFORM pg_notify( + 'ban_notification', + json_build_object('ban_id', NEW.ban_id)::text + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER notify_on_server_ban_insert + AFTER INSERT ON ban + FOR EACH ROW + WHEN (NEW.type = {(int)BanType.Server}) + EXECUTE FUNCTION send_server_ban_notification(); + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20260422144550_PlayerAchievementsProgress.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260422144550_PlayerAchievementsProgress.Designer.cs index be9c958aa56..686d0792de8 100644 --- a/Content.Server.Database/Migrations/Postgres/20260422144550_PlayerAchievementsProgress.Designer.cs +++ b/Content.Server.Database/Migrations/Postgres/20260422144550_PlayerAchievementsProgress.Designer.cs @@ -1130,6 +1130,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("jsonb") .HasColumnName("markings"); + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + b.Property("PreferenceId") .HasColumnType("integer") .HasColumnName("preference_id"); diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index a6261f406f1..caee996c2ae 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("ProductVersion", "10.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -519,6 +519,221 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_address_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_role_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("ban_round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -1127,6 +1342,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("jsonb") .HasColumnName("markings"); + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + b.Property("PreferenceId") .HasColumnType("integer") .HasColumnName("preference_id"); @@ -1317,137 +1536,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { - b.Property("Id") + b.Property("UserId") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("boolean") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") .HasColumnType("uuid") - .HasColumnName("banning_admin"); + .HasColumnName("user_id"); - b.Property("ExemptFlags") + b.Property("Flags") .HasColumnType("integer") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); + .HasColumnName("flags"); - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("RoundId") + b.Property("BanId") .HasColumnType("integer") - .HasColumnName("round_id"); + .HasColumnName("ban_id"); - b.Property("Severity") + b.Property("ConnectionId") .HasColumnType("integer") - .HasColumnName("severity"); + .HasColumnName("connection_id"); b.HasKey("Id") - .HasName("PK_server_ban"); + .HasName("PK_server_ban_hit"); - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); - - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => - { - b.Property("UserId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.Property("Flags") - .HasColumnType("integer") - .HasColumnName("flags"); - - b.HasKey("UserId") - .HasName("PK_server_ban_exemption"); - - b.ToTable("server_ban_exemption", null, t => - { - t.HasCheckConstraint("FlagsNotZero", "flags != 0"); - }); - }); - - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("server_ban_hit_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("BanId") - .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("ConnectionId") - .HasColumnType("integer") - .HasColumnName("connection_id"); - - b.HasKey("Id") - .HasName("PK_server_ban_hit"); - - b.HasIndex("BanId") - .HasDatabaseName("IX_server_ban_hit_ban_id"); + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); b.HasIndex("ConnectionId") .HasDatabaseName("IX_server_ban_hit_connection_id"); @@ -1455,123 +1585,65 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server_ban_hit", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + modelBuilder.Entity("Content.Server.Database.Skill", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); + .HasColumnName("skill_id"); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("Address") - .HasColumnType("inet") - .HasColumnName("address"); - - b.Property("BanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("uuid") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("boolean") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("uuid") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("uuid") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("interval") - .HasColumnName("playtime_at_note"); + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); - b.Property("Reason") - .IsRequired() - .HasColumnType("text") - .HasColumnName("reason"); + b.Property("SkillLevel") + .HasColumnType("integer") + .HasColumnName("skill_level"); - b.Property("RoleId") + b.Property("SkillName") .IsRequired() .HasColumnType("text") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("integer") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("integer") - .HasColumnName("severity"); + .HasColumnName("skill_name"); b.HasKey("Id") - .HasName("PK_server_role_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_role_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_role_ban_round_id"); + .HasName("PK_skill"); - b.ToTable("server_role_ban", null, t => - { - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + b.HasIndex("ProfileId", "SkillName") + .IsUnique(); - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); + b.ToTable("skill", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("integer") - .HasColumnName("role_unban_id"); + .HasColumnName("trait_id"); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("BanId") + b.Property("ProfileId") .HasColumnType("integer") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("unban_time"); + .HasColumnName("profile_id"); - b.Property("UnbanningAdmin") - .HasColumnType("uuid") - .HasColumnName("unbanning_admin"); + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); b.HasKey("Id") - .HasName("PK_server_role_unban"); + .HasName("PK_trait"); - b.HasIndex("BanId") + b.HasIndex("ProfileId", "TraitName") .IsUnique(); - b.ToTable("server_role_unban", (string)null); + b.ToTable("trait", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1593,70 +1665,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("unbanning_admin"); b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_unban"); b.HasIndex("BanId") .IsUnique(); - b.ToTable("server_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Skill", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("skill_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ProfileId") - .HasColumnType("integer") - .HasColumnName("profile_id"); - - b.Property("SkillLevel") - .HasColumnType("integer") - .HasColumnName("skill_level"); - - b.Property("SkillName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("skill_name"); - - b.HasKey("Id") - .HasName("PK_skill"); - - b.HasIndex("ProfileId", "SkillName") - .IsUnique(); - - b.ToTable("skill", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("trait_id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ProfileId") - .HasColumnType("integer") - .HasColumnName("profile_id"); - - b.Property("TraitName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("trait_name"); - - b.HasKey("Id") - .HasName("PK_trait"); - - b.HasIndex("ProfileId", "TraitName") - .IsUnique(); - - b.ToTable("trait", (string)null); + b.ToTable("unban", (string)null); }); modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => @@ -1943,6 +1957,123 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("integer") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -2125,70 +2256,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("integer") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -2202,86 +2277,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("integer") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => - { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); - - b.Navigation("Ban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => - { - b.HasOne("Content.Server.Database.ServerBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); - - b.Navigation("Ban"); - }); - modelBuilder.Entity("Content.Server.Database.Skill", b => { b.HasOne("Content.Server.Database.Profile", "Profile") @@ -2306,6 +2301,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -2340,6 +2347,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Flags"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Navigation("BanHits"); @@ -2369,10 +2393,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2423,18 +2443,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs new file mode 100644 index 00000000000..7536236a962 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs @@ -0,0 +1,2048 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20260118084622_OrganMarkings")] + partial class OrganMarkings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("INTEGER") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("INTEGER") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ipintel_cache_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("REAL") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection("ConstructionFavorites") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ServerBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerBanId"); + + b1.ToTable("server_ban"); + + b1.WithOwner() + .HasForeignKey("ServerBanId") + .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ServerRoleBanId") + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ServerRoleBanId"); + + b1.ToTable("server_role_ban"); + + b1.WithOwner() + .HasForeignKey("ServerRoleBanId") + .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("HWId"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs new file mode 100644 index 00000000000..8b1f79f72f7 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class OrganMarkings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "organ_markings", + table: "profile", + type: "jsonb", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "organ_markings", + table: "profile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs new file mode 100644 index 00000000000..804e3aae272 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.Designer.cs @@ -0,0 +1,2044 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20260120200455_BanRefactor")] + partial class BanRefactor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Deadminned") + .HasColumnType("INTEGER") + .HasColumnName("deadminned"); + + b.Property("Suspended") + .HasColumnType("INTEGER") + .HasColumnName("suspended"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_template_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("TEXT") + .HasColumnName("length"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_ban_template"); + + b.ToTable("ban_template", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Blacklist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ipintel_cache_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("REAL") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection("ConstructionFavorites") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("construction_favorites"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("entity_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("ConnectionLogId") + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("hwid_type"); + + b1.HasKey("ConnectionLogId"); + + b1.ToTable("connection_log"); + + b1.WithOwner() + .HasForeignKey("ConnectionLogId") + .HasConstraintName("FK_connection_log_connection_log_connection_log_id"); + }); + + b.Navigation("HWId"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b1.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("last_seen_hwid_type"); + + b1.HasKey("PlayerId"); + + b1.ToTable("player"); + + b1.WithOwner() + .HasForeignKey("PlayerId") + .HasConstraintName("FK_player_player_player_id"); + }); + + b.Navigation("LastSeenHWId"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs new file mode 100644 index 00000000000..f813d109453 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260120200455_BanRefactor.cs @@ -0,0 +1,498 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class BanRefactor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ban", + columns: table => new + { + ban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + type = table.Column(type: "INTEGER", nullable: false), + playtime_at_note = table.Column(type: "TEXT", nullable: false), + ban_time = table.Column(type: "TEXT", nullable: false), + expiration_time = table.Column(type: "TEXT", nullable: true), + reason = table.Column(type: "TEXT", nullable: false), + severity = table.Column(type: "INTEGER", nullable: false), + banning_admin = table.Column(type: "TEXT", nullable: true), + last_edited_by_id = table.Column(type: "TEXT", nullable: true), + last_edited_at = table.Column(type: "TEXT", nullable: true), + exempt_flags = table.Column(type: "INTEGER", nullable: false), + auto_delete = table.Column(type: "INTEGER", nullable: false), + hidden = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban", x => x.ban_id); + table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + table.ForeignKey( + name: "FK_ban_player_banning_admin", + column: x => x.banning_admin, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ban_player_last_edited_by_id", + column: x => x.last_edited_by_id, + principalTable: "player", + principalColumn: "user_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ban_address", + columns: table => new + { + ban_address_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + address = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_address", x => x.ban_address_id); + table.ForeignKey( + name: "FK_ban_address_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_hwid", + columns: table => new + { + ban_hwid_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + hwid = table.Column(type: "BLOB", nullable: false), + hwid_type = table.Column(type: "INTEGER", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id); + table.ForeignKey( + name: "FK_ban_hwid_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_player", + columns: table => new + { + ban_player_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + user_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_player", x => x.ban_player_id); + table.ForeignKey( + name: "FK_ban_player_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_role", + columns: table => new + { + ban_role_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + role_type = table.Column(type: "TEXT", nullable: false), + role_id = table.Column(type: "TEXT", nullable: false), + ban_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_role", x => x.ban_role_id); + table.ForeignKey( + name: "FK_ban_role_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ban_round", + columns: table => new + { + ban_round_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + round_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ban_round", x => x.ban_round_id); + table.ForeignKey( + name: "FK_ban_round_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ban_round_round_round_id", + column: x => x.round_id, + principalTable: "round", + principalColumn: "round_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "unban", + columns: table => new + { + unban_id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ban_id = table.Column(type: "INTEGER", nullable: false), + unbanning_admin = table.Column(type: "TEXT", nullable: true), + unban_time = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_unban", x => x.unban_id); + table.ForeignKey( + name: "FK_unban_ban_ban_id", + column: x => x.ban_id, + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ban_banning_admin", + table: "ban", + column: "banning_admin"); + + migrationBuilder.CreateIndex( + name: "IX_ban_last_edited_by_id", + table: "ban", + column: "last_edited_by_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_address_ban_id", + table: "ban_address", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_hwid_ban_id", + table: "ban_hwid", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_ban_id", + table: "ban_player", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_player_user_id_ban_id", + table: "ban_player", + columns: new[] { "user_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_ban_id", + table: "ban_role", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_role_role_type_role_id_ban_id", + table: "ban_role", + columns: new[] { "role_type", "role_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_ban_id", + table: "ban_round", + column: "ban_id"); + + migrationBuilder.CreateIndex( + name: "IX_ban_round_round_id_ban_id", + table: "ban_round", + columns: new[] { "round_id", "ban_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unban_ban_id", + table: "unban", + column: "ban_id", + unique: true); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id" + ON ban_hwid + (hwid_type, hwid, ban_id); + + CREATE UNIQUE INDEX "IX_ban_address_address_ban_id" + ON ban_address + (address, ban_id); + """); + + migrationBuilder.Sql(""" + -- + -- Insert game bans + -- + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + server_ban_id, 0, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden + FROM + server_ban; + + -- Insert ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, server_ban_id + FROM + server_ban + WHERE + player_user_id IS NOT NULL; + + -- Insert ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, server_ban_id + FROM + server_ban + WHERE + address IS NOT NULL; + + -- Insert ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, server_ban_id + FROM + server_ban + WHERE + hwid IS NOT NULL; + + -- Insert ban unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + ban_id, unbanning_admin, unban_time + FROM server_unban; + + -- Insert ban round records. + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, server_ban_id + FROM + server_ban + WHERE + round_id IS NOT NULL; + + -- + -- Insert role bans + -- This shit is a pain in the ass + -- > Declarative language + -- > Has to write procedural code in it + -- + + -- Create mapping table from role ban -> server ban. + -- We have to manually calculate the new ban IDs by using the sequence. + -- We also want to merge role ban records because the game code previously did that in some UI, + -- and that code is now gone, expecting the DB to do it. + + -- Create a table to store IDs to merge. + CREATE TEMPORARY TABLE _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE); + + -- Create a table to store merged IDs -> new ban IDs + CREATE TEMPORARY TABLE _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE); + + -- Calculate merged role bans. + INSERT INTO + _role_ban_import_merge_map + SELECT + ( + SELECT + sub.server_role_ban_id + FROM + server_role_ban AS sub + LEFT JOIN server_role_unban AS sub_unban + ON sub_unban.ban_id = sub.server_role_ban_id + WHERE + main.reason IS NOT DISTINCT FROM sub.reason + AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id + AND main.address IS NOT DISTINCT FROM sub.address + AND main.hwid IS NOT DISTINCT FROM sub.hwid + AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type + AND main.ban_time = sub.ban_time + AND ( + (main.expiration_time IS NULL) = (sub.expiration_time IS NULL) + OR main.expiration_time = sub.expiration_time + ) + AND main.round_id IS NOT DISTINCT FROM sub.round_id + AND main.severity IS NOT DISTINCT FROM sub.severity + AND main.hidden IS NOT DISTINCT FROM sub.hidden + AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin + AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL) + ORDER BY + sub.server_role_ban_id ASC + LIMIT 1 + ), main.server_role_ban_id + FROM + server_role_ban AS main + LEFT JOIN server_role_unban AS main_unban + ON main_unban.ban_id = main.server_role_ban_id; + + -- Assign new ban IDs for merged IDs. + INSERT OR IGNORE INTO + _role_ban_import_id_map + SELECT + merge_id + (SELECT seq FROM sqlite_sequence WHERE name = 'ban'), + merge_id + FROM + _role_ban_import_merge_map; + + -- I sure fucking wish CTEs could span multiple queries... + + -- Insert new ban records + INSERT INTO + ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden) + SELECT + im.ban_id, 1, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id; + + -- Insert role ban player records. + INSERT INTO + ban_player (user_id, ban_id) + SELECT + player_user_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND player_user_id IS NOT NULL; + + -- Insert role ban address records. + INSERT INTO + ban_address (address, ban_id) + SELECT + address, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND address IS NOT NULL; + + -- Insert role ban HWID records. + INSERT INTO + ban_hwid (hwid, hwid_type, ban_id) + SELECT + hwid, hwid_type, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND hwid IS NOT NULL; + + -- Insert role ban role records. + INSERT INTO + ban_role (role_type, role_id, ban_id) + SELECT + substr(role_id, 1, instr(role_id, ':')-1), + substr(role_id, instr(role_id, ':')+1), + im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = mm.server_role_ban_id + -- Yes, we have some messy ban records which, after merging, end up with duplicate roles. + ON CONFLICT DO NOTHING; + + -- Insert role unban records. + INSERT INTO + unban (ban_id, unbanning_admin, unban_time) + SELECT + im.ban_id, unbanning_admin, unban_time + FROM server_role_unban sru + INNER JOIN _role_ban_import_id_map im + ON im.merge_id = sru.ban_id; + + -- Insert role rounds + INSERT INTO + ban_round (round_id, ban_id) + SELECT + round_id, im.ban_id + FROM + _role_ban_import_id_map im + INNER JOIN _role_ban_import_merge_map mm + ON im.merge_id = mm.merge_id + INNER JOIN server_role_ban srb + ON srb.server_role_ban_id = im.merge_id + WHERE mm.merge_id = mm.server_role_ban_id + AND round_id IS NOT NULL; + """); + + migrationBuilder.AddForeignKey( + name: "FK_server_ban_hit_ban_ban_id", + table: "server_ban_hit", + column: "ban_id", + principalTable: "ban", + principalColumn: "ban_id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.DropForeignKey( + name: "FK_server_ban_hit_server_ban_ban_id", + table: "server_ban_hit"); + + migrationBuilder.DropTable( + name: "server_role_unban"); + + migrationBuilder.DropTable( + name: "server_unban"); + + migrationBuilder.DropTable( + name: "server_role_ban"); + + migrationBuilder.DropTable( + name: "server_ban"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new NotSupportedException("This migration cannot be rolled back"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20260422144529_PlayerAchievementsProgress.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260422144529_PlayerAchievementsProgress.Designer.cs index 30ae466a300..89756f0ac78 100644 --- a/Content.Server.Database/Migrations/Sqlite/20260422144529_PlayerAchievementsProgress.Designer.cs +++ b/Content.Server.Database/Migrations/Sqlite/20260422144529_PlayerAchievementsProgress.Designer.cs @@ -1067,6 +1067,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("jsonb") .HasColumnName("markings"); + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + b.Property("PreferenceId") .HasColumnType("INTEGER") .HasColumnName("preference_id"); diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index b5498c8451f..0c54411ecc2 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteServerDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.6"); modelBuilder.Entity("Content.Server.Database.Admin", b => { @@ -489,6 +489,207 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("assigned_user_id", (string)null); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("PK_ban"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.ToTable("ban", null, t => + { + t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_address_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_address"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_address_ban_id"); + + b.ToTable("ban_address", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.HasKey("Id") + .HasName("PK_ban_hwid"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_hwid_ban_id"); + + b.ToTable("ban_hwid", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_player_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_ban_player"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_player_ban_id"); + + b.HasIndex("UserId", "BanId") + .IsUnique(); + + b.ToTable("ban_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_role_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoleType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_type"); + + b.HasKey("Id") + .HasName("PK_ban_role"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_role_ban_id"); + + b.HasIndex("RoleType", "RoleId", "BanId") + .IsUnique(); + + b.ToTable("ban_role", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_round_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_ban_round"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_ban_round_ban_id"); + + b.HasIndex("RoundId", "BanId") + .IsUnique(); + + b.ToTable("ban_round", (string)null); + }); + modelBuilder.Entity("Content.Server.Database.BanTemplate", b => { b.Property("Id") @@ -1064,6 +1265,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("jsonb") .HasColumnName("markings"); + b.Property("OrganMarkings") + .HasColumnType("jsonb") + .HasColumnName("organ_markings"); + b.Property("PreferenceId") .HasColumnType("INTEGER") .HasColumnName("preference_id"); @@ -1244,91 +1449,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); - - b.Property("AutoDelete") - .HasColumnType("INTEGER") - .HasColumnName("auto_delete"); - - b.Property("BanTime") - .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExemptFlags") - .HasColumnType("INTEGER") - .HasColumnName("exempt_flags"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") - .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); - - b.HasKey("Id") - .HasName("PK_server_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_ban_player_user_id"); - - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_ban_round_id"); - - b.ToTable("server_ban", null, t => - { - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => { b.Property("UserId") @@ -1376,117 +1496,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("server_ban_hit", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + modelBuilder.Entity("Content.Server.Database.Skill", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); + .HasColumnName("skill_id"); - b.Property("Address") - .HasColumnType("TEXT") - .HasColumnName("address"); + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); - b.Property("BanTime") - .HasColumnType("TEXT") - .HasColumnName("ban_time"); - - b.Property("BanningAdmin") - .HasColumnType("TEXT") - .HasColumnName("banning_admin"); - - b.Property("ExpirationTime") - .HasColumnType("TEXT") - .HasColumnName("expiration_time"); - - b.Property("Hidden") + b.Property("SkillLevel") .HasColumnType("INTEGER") - .HasColumnName("hidden"); - - b.Property("LastEditedAt") - .HasColumnType("TEXT") - .HasColumnName("last_edited_at"); - - b.Property("LastEditedById") - .HasColumnType("TEXT") - .HasColumnName("last_edited_by_id"); - - b.Property("PlayerUserId") - .HasColumnType("TEXT") - .HasColumnName("player_user_id"); - - b.Property("PlaytimeAtNote") - .HasColumnType("TEXT") - .HasColumnName("playtime_at_note"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("reason"); + .HasColumnName("skill_level"); - b.Property("RoleId") + b.Property("SkillName") .IsRequired() .HasColumnType("TEXT") - .HasColumnName("role_id"); - - b.Property("RoundId") - .HasColumnType("INTEGER") - .HasColumnName("round_id"); - - b.Property("Severity") - .HasColumnType("INTEGER") - .HasColumnName("severity"); + .HasColumnName("skill_name"); b.HasKey("Id") - .HasName("PK_server_role_ban"); - - b.HasIndex("Address"); - - b.HasIndex("BanningAdmin"); - - b.HasIndex("LastEditedById"); - - b.HasIndex("PlayerUserId") - .HasDatabaseName("IX_server_role_ban_player_user_id"); + .HasName("PK_skill"); - b.HasIndex("RoundId") - .HasDatabaseName("IX_server_role_ban_round_id"); + b.HasIndex("ProfileId", "SkillName") + .IsUnique(); - b.ToTable("server_role_ban", null, t => - { - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); - }); + b.ToTable("skill", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + modelBuilder.Entity("Content.Server.Database.Trait", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") - .HasColumnName("role_unban_id"); + .HasColumnName("trait_id"); - b.Property("BanId") + b.Property("ProfileId") .HasColumnType("INTEGER") - .HasColumnName("ban_id"); - - b.Property("UnbanTime") - .HasColumnType("TEXT") - .HasColumnName("unban_time"); + .HasColumnName("profile_id"); - b.Property("UnbanningAdmin") + b.Property("TraitName") + .IsRequired() .HasColumnType("TEXT") - .HasColumnName("unbanning_admin"); + .HasColumnName("trait_name"); b.HasKey("Id") - .HasName("PK_server_role_unban"); + .HasName("PK_trait"); - b.HasIndex("BanId") + b.HasIndex("ProfileId", "TraitName") .IsUnique(); - b.ToTable("server_role_unban", (string)null); + b.ToTable("trait", (string)null); }); - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + modelBuilder.Entity("Content.Server.Database.Unban", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -1506,66 +1570,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("unbanning_admin"); b.HasKey("Id") - .HasName("PK_server_unban"); + .HasName("PK_unban"); b.HasIndex("BanId") .IsUnique(); - b.ToTable("server_unban", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Skill", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("skill_id"); - - b.Property("ProfileId") - .HasColumnType("INTEGER") - .HasColumnName("profile_id"); - - b.Property("SkillLevel") - .HasColumnType("INTEGER") - .HasColumnName("skill_level"); - - b.Property("SkillName") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("skill_name"); - - b.HasKey("Id") - .HasName("PK_skill"); - - b.HasIndex("ProfileId", "SkillName") - .IsUnique(); - - b.ToTable("skill", (string)null); - }); - - modelBuilder.Entity("Content.Server.Database.Trait", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("trait_id"); - - b.Property("ProfileId") - .HasColumnType("INTEGER") - .HasColumnName("profile_id"); - - b.Property("TraitName") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("trait_name"); - - b.HasKey("Id") - .HasName("PK_trait"); - - b.HasIndex("ProfileId", "TraitName") - .IsUnique(); - - b.ToTable("trait", (string)null); + b.ToTable("unban", (string)null); }); modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => @@ -1850,6 +1860,123 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ban_player_last_edited_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.BanAddress", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Addresses") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_address_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Hwids") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_hwid_ban_ban_id"); + + b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => + { + b1.Property("BanHwidId") + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b1.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("hwid_type"); + + b1.HasKey("BanHwidId"); + + b1.ToTable("ban_hwid"); + + b1.WithOwner() + .HasForeignKey("BanHwidId") + .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id"); + }); + + b.Navigation("Ban"); + + b.Navigation("HWId") + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.BanPlayer", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Players") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_player_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRole", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Roles") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.BanRound", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithMany("Rounds") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_ban_ban_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_ban_round_round_round_id"); + + b.Navigation("Ban"); + + b.Navigation("Round"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.HasOne("Content.Server.Database.Server", "Server") @@ -2032,70 +2159,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Server"); }); - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerBanId"); - - b1.ToTable("server_ban"); - - b1.WithOwner() - .HasForeignKey("ServerBanId") - .HasConstraintName("FK_server_ban_server_ban_server_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => { - b.HasOne("Content.Server.Database.ServerBan", "Ban") + b.HasOne("Content.Server.Database.Ban", "Ban") .WithMany("BanHits") .HasForeignKey("BanId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + .HasConstraintName("FK_server_ban_hit_ban_ban_id"); b.HasOne("Content.Server.Database.ConnectionLog", "Connection") .WithMany("BanHits") @@ -2109,86 +2180,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Connection"); }); - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.HasOne("Content.Server.Database.Player", "CreatedBy") - .WithMany("AdminServerRoleBansCreated") - .HasForeignKey("BanningAdmin") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_banning_admin"); - - b.HasOne("Content.Server.Database.Player", "LastEditedBy") - .WithMany("AdminServerRoleBansLastEdited") - .HasForeignKey("LastEditedById") - .HasPrincipalKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); - - b.HasOne("Content.Server.Database.Round", "Round") - .WithMany() - .HasForeignKey("RoundId") - .HasConstraintName("FK_server_role_ban_round_round_id"); - - b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 => - { - b1.Property("ServerRoleBanId") - .HasColumnType("INTEGER") - .HasColumnName("server_role_ban_id"); - - b1.Property("Hwid") - .IsRequired() - .HasColumnType("BLOB") - .HasColumnName("hwid"); - - b1.Property("Type") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0) - .HasColumnName("hwid_type"); - - b1.HasKey("ServerRoleBanId"); - - b1.ToTable("server_role_ban"); - - b1.WithOwner() - .HasForeignKey("ServerRoleBanId") - .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id"); - }); - - b.Navigation("CreatedBy"); - - b.Navigation("HWId"); - - b.Navigation("LastEditedBy"); - - b.Navigation("Round"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => - { - b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); - - b.Navigation("Ban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerUnban", b => - { - b.HasOne("Content.Server.Database.ServerBan", "Ban") - .WithOne("Unban") - .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_server_unban_server_ban_ban_id"); - - b.Navigation("Ban"); - }); - modelBuilder.Entity("Content.Server.Database.Skill", b => { b.HasOne("Content.Server.Database.Profile", "Profile") @@ -2213,6 +2204,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Profile"); }); + modelBuilder.Entity("Content.Server.Database.Unban", b => + { + b.HasOne("Content.Server.Database.Ban", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.Unban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_unban_ban_ban_id"); + + b.Navigation("Ban"); + }); + modelBuilder.Entity("PlayerRound", b => { b.HasOne("Content.Server.Database.Player", null) @@ -2247,6 +2250,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Flags"); }); + modelBuilder.Entity("Content.Server.Database.Ban", b => + { + b.Navigation("Addresses"); + + b.Navigation("BanHits"); + + b.Navigation("Hwids"); + + b.Navigation("Players"); + + b.Navigation("Roles"); + + b.Navigation("Rounds"); + + b.Navigation("Unban"); + }); + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => { b.Navigation("BanHits"); @@ -2276,10 +2296,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AdminServerBansLastEdited"); - b.Navigation("AdminServerRoleBansCreated"); - - b.Navigation("AdminServerRoleBansLastEdited"); - b.Navigation("AdminWatchlistsCreated"); b.Navigation("AdminWatchlistsDeleted"); @@ -2330,18 +2346,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Rounds"); }); - - modelBuilder.Entity("Content.Server.Database.ServerBan", b => - { - b.Navigation("BanHits"); - - b.Navigation("Unban"); - }); - - modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => - { - b.Navigation("Unban"); - }); #pragma warning restore 612, 618 } } diff --git a/Content.Server.Database/Model.Ban.cs b/Content.Server.Database/Model.Ban.cs new file mode 100644 index 00000000000..7d1ee2ab1a1 --- /dev/null +++ b/Content.Server.Database/Model.Ban.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Content.Shared.Database; +using Microsoft.EntityFrameworkCore; +using NpgsqlTypes; + +// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength + +namespace Content.Server.Database; + +// +// Contains model definitions primarily related to bans. +// + +internal static class ModelBan +{ + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(b => b.CreatedBy) + .WithMany(pl => pl.AdminServerBansCreated) + .HasForeignKey(b => b.BanningAdmin) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasOne(b => b.LastEditedBy) + .WithMany(pl => pl.AdminServerBansLastEdited) + .HasForeignKey(b => b.LastEditedById) + .HasPrincipalKey(pl => pl.UserId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.UserId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .OwnsOne(bp => bp.HWId) + .Property(hwid => hwid.Hwid) + .HasColumnName("hwid"); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(bp => new { bp.RoundId, bp.BanId }) + .IsUnique(); + + // Following indices have to be made manually by migration, due to limitations in EF Core: + // https://github.com/dotnet/efcore/issues/11336 + // https://github.com/npgsql/efcore.pg/issues/2567 + // modelBuilder.Entity() + // .HasIndex(bp => new { bp.Address, bp.BanId }) + // .IsUnique(); + // modelBuilder.Entity() + // .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid }) + // .IsUnique(); + // (postgres only) + // modelBuilder.Entity() + // .HasIndex(ba => ba.Address) + // .IncludeProperties(ba => ba.BanId) + // .IsUnique() + // .HasMethod("gist") + // .HasOperators("inet_ops"); + + modelBuilder.Entity() + .ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0")); + } +} + +/// +/// Specifies a ban of some kind. +/// +/// +/// +/// Bans come in two types: and , +/// distinguished with . +/// +/// +/// Bans have one or more "matching data", these being , , +/// and entities. If a player's connection info matches any of these, +/// the ban's effects will apply to that player. +/// +/// +/// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually +/// ("unbanned") by an admin, which is stored as an entity existing for this ban. +/// +/// +public sealed class Ban +{ + public int Id { get; set; } + + /// + /// Whether this is a role or server ban. + /// + public required BanType Type { get; set; } + + public TimeSpan PlaytimeAtNote { get; set; } + + /// + /// The time when the ban was applied by an administrator. + /// + public DateTime BanTime { get; set; } + + /// + /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. + /// + public DateTime? ExpirationTime { get; set; } + + /// + /// The administrator-stated reason for applying the ban. + /// + public string Reason { get; set; } = null!; + + /// + /// The severity of the incident + /// + public NoteSeverity Severity { get; set; } + + /// + /// User ID of the admin that initially applied the ban. + /// + [ForeignKey(nameof(CreatedBy))] + public Guid? BanningAdmin { get; set; } + + public Player? CreatedBy { get; set; } + + /// + /// User ID of the admin that last edited the note + /// + [ForeignKey(nameof(LastEditedBy))] + public Guid? LastEditedById { get; set; } + + public Player? LastEditedBy { get; set; } + public DateTime? LastEditedAt { get; set; } + + /// + /// Optional flags that allow adding exemptions to the ban via . + /// + public ServerBanExemptFlags ExemptFlags { get; set; } + + /// + /// Whether this ban should be automatically deleted from the database when it expires. + /// + /// + /// This isn't done automatically by the game, + /// you will need to set up something like a cron job to clear this from your database, + /// using a command like this: + /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" + /// + public bool AutoDelete { get; set; } + + /// + /// Whether to display this ban in the admin remarks (notes) panel + /// + public bool Hidden { get; set; } + + /// + /// If present, an administrator has manually repealed this ban. + /// + public Unban? Unban { get; set; } + + public List? Rounds { get; set; } + public List? Players { get; set; } + public List? Addresses { get; set; } + public List? Hwids { get; set; } + public List? Roles { get; set; } + public List? BanHits { get; set; } +} + +/// +/// Base type for entities that specify ban matching data. +/// +public interface IBanSelector +{ + int BanId { get; } + Ban? Ban { get; } +} + +/// +/// Indicates that a ban was related to a round (e.g. placed on that round). +/// +public sealed class BanRound +{ + public int Id { get; set; } + + /// + /// The ID of the ban to which this round was relevant. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } + + /// + /// The ID of the round to which this ban was relevant to. + /// + [ForeignKey(nameof(Round))] + public int RoundId { get; set; } + + public Round? Round { get; set; } +} + +/// +/// Specifies a player that a matches. +/// +public sealed class BanPlayer : IBanSelector +{ + public int Id { get; set; } + + /// + /// The user ID of the banned player. + /// + public Guid UserId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies an IP address range that a matches. +/// +public sealed class BanAddress : IBanSelector +{ + public int Id { get; set; } + + /// + /// The address range being matched. + /// + public required NpgsqlInet Address { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// Specifies a HWID that a matches. +/// +public sealed class BanHwid : IBanSelector +{ + public int Id { get; set; } + + /// + /// The HWID being matched. + /// + public required TypedHwid HWId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// A single role banned among a greater role ban record. +/// +/// +/// s of type should have one or more s +/// to store which roles are actually banned. +/// It is invalid for bans to have entities. +/// +public sealed class BanRole +{ + public int Id { get; set; } + + /// + /// What type of role is being banned. For example Job or Antag. + /// + public required string RoleType { get; set; } + + /// + /// The ID of the role being banned. This is probably something like a prototype. + /// + public required string RoleId { get; set; } + + /// + /// The ID of the ban to which this applies. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + public Ban? Ban { get; set; } +} + +/// +/// An explicit repeal of a by an administrator. +/// Having an entry for a ban neutralizes it. +/// +public sealed class Unban +{ + public int Id { get; set; } + + /// + /// The ID of ban that is being repealed. + /// + [ForeignKey(nameof(Ban))] + public int BanId { get; set; } + + /// + /// The ban that is being repealed. + /// + public Ban? Ban { get; set; } + + /// + /// The admin that repealed the ban. + /// + public Guid? UnbanningAdmin { get; set; } + + /// + /// The time the ban was repealed. + /// + public DateTime UnbanTime { get; set; } +} diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 42b426b81db..5aa51c2cb52 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -10,7 +10,6 @@ using System.Text.Json; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; -using NpgsqlTypes; namespace Content.Server.Database { @@ -32,13 +31,17 @@ protected ServerDbContext(DbContextOptions options) : base(options) public DbSet AdminLogPlayer { get; set; } = null!; public DbSet Whitelist { get; set; } = null!; public DbSet Blacklist { get; set; } = null!; - public DbSet Ban { get; set; } = default!; - public DbSet Unban { get; set; } = default!; + public DbSet Ban { get; set; } = default!; + public DbSet BanRound { get; set; } = default!; + public DbSet BanPlayer { get; set; } = default!; + public DbSet BanAddress { get; set; } = default!; + public DbSet BanHwid { get; set; } = default!; + public DbSet BanRole { get; set; } = default!; + public DbSet Unban { get; set; } = default!; public DbSet BanExemption { get; set; } = default!; public DbSet ConnectionLog { get; set; } = default!; public DbSet ServerBanHit { get; set; } = default!; - public DbSet RoleBan { get; set; } = default!; - public DbSet RoleUnban { get; set; } = default!; + public DbSet PlayTime { get; set; } = default!; public DbSet UploadedResourceLog { get; set; } = default!; public DbSet AdminNotes { get; set; } = null!; @@ -193,43 +196,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId}); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - // Ban exemption can't have flags 0 since that wouldn't exempt anything. // The row should be removed if setting to 0. modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("FlagsNotZero", "flags != 0")); - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.Address); - - modelBuilder.Entity() - .HasIndex(p => p.PlayerUserId); - - modelBuilder.Entity() - .HasIndex(p => p.BanId) - .IsUnique(); - - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL")); - modelBuilder.Entity() .HasIndex(p => p.UserId) .IsUnique(); @@ -344,34 +315,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen")); - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.CreatedBy) - .WithMany(author => author.AdminServerRoleBansCreated) - .HasForeignKey(ban => ban.BanningAdmin) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity() - .HasOne(ban => ban.LastEditedBy) - .WithMany(author => author.AdminServerRoleBansLastEdited) - .HasForeignKey(ban => ban.LastEditedById) - .HasPrincipalKey(author => author.UserId) - .OnDelete(DeleteBehavior.SetNull); - modelBuilder.Entity() .HasOne(w => w.Player) .WithMany(p => p.JobWhitelists) @@ -390,26 +333,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Hwid) - .HasColumnName("hwid"); - - modelBuilder.Entity() - .OwnsOne(p => p.HWId) - .Property(p => p.Type) - .HasDefaultValue(HwidType.Legacy); - modelBuilder.Entity() .OwnsOne(p => p.HWId) .Property(p => p.Hwid) @@ -419,6 +342,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .OwnsOne(p => p.HWId) .Property(p => p.Type) .HasDefaultValue(HwidType.Legacy); + + ModelBan.OnModelCreating(modelBuilder); } public virtual IQueryable SearchLogs(IQueryable query, string searchText) @@ -454,6 +379,7 @@ public class Profile public string Sex { get; set; } = null!; public string Gender { get; set; } = null!; public string Species { get; set; } = null!; + [Column(TypeName = "jsonb")] public JsonDocument? OrganMarkings { get; set; } = null!; [Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!; public string HairName { get; set; } = null!; public string HairColor { get; set; } = null!; @@ -749,10 +675,8 @@ public class Player public List AdminMessagesCreated { get; set; } = null!; public List AdminMessagesLastEdited { get; set; } = null!; public List AdminMessagesDeleted { get; set; } = null!; - public List AdminServerBansCreated { get; set; } = null!; - public List AdminServerBansLastEdited { get; set; } = null!; - public List AdminServerRoleBansCreated { get; set; } = null!; - public List AdminServerRoleBansLastEdited { get; set; } = null!; + public List AdminServerBansCreated { get; set; } = null!; + public List AdminServerBansLastEdited { get; set; } = null!; public List JobWhitelists { get; set; } = null!; } @@ -882,30 +806,6 @@ public class AdminLogPlayer [ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!; } - // Used by SS14.Admin - public interface IBanCommon where TUnban : IUnbanCommon - { - int Id { get; set; } - Guid? PlayerUserId { get; set; } - NpgsqlInet? Address { get; set; } - TypedHwid? HWId { get; set; } - DateTime BanTime { get; set; } - DateTime? ExpirationTime { get; set; } - string Reason { get; set; } - NoteSeverity Severity { get; set; } - Guid? BanningAdmin { get; set; } - TUnban? Unban { get; set; } - } - - // Used by SS14.Admin - public interface IUnbanCommon - { - int Id { get; set; } - int BanId { get; set; } - Guid? UnbanningAdmin { get; set; } - DateTime UnbanTime { get; set; } - } - /// /// Flags for use with . /// @@ -943,138 +843,6 @@ public enum ServerBanExemptFlags // @formatter:on } - /// - /// A ban from playing on the server. - /// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server. - /// - /// - /// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing). - /// - [Table("server_ban"), Index(nameof(PlayerUserId))] - public class ServerBan : IBanCommon - { - public int Id { get; set; } - - [ForeignKey("Round")] - public int? RoundId { get; set; } - public Round? Round { get; set; } - - /// - /// The user ID of the banned player. - /// - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - - /// - /// CIDR IP address range of the ban. The whole range can match the ban. - /// - public NpgsqlInet? Address { get; set; } - - /// - /// Hardware ID of the banned player. - /// - public TypedHwid? HWId { get; set; } - - /// - /// The time when the ban was applied by an administrator. - /// - public DateTime BanTime { get; set; } - - /// - /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. - /// - public DateTime? ExpirationTime { get; set; } - - /// - /// The administrator-stated reason for applying the ban. - /// - public string Reason { get; set; } = null!; - - /// - /// The severity of the incident - /// - public NoteSeverity Severity { get; set; } - - /// - /// User ID of the admin that applied the ban. - /// - [ForeignKey("CreatedBy")] - public Guid? BanningAdmin { get; set; } - - public Player? CreatedBy { get; set; } - - /// - /// User ID of the admin that last edited the note - /// - [ForeignKey("LastEditedBy")] - public Guid? LastEditedById { get; set; } - - public Player? LastEditedBy { get; set; } - - /// - /// When the ban was last edited - /// - public DateTime? LastEditedAt { get; set; } - - /// - /// Optional flags that allow adding exemptions to the ban via . - /// - public ServerBanExemptFlags ExemptFlags { get; set; } - - /// - /// If present, an administrator has manually repealed this ban. - /// - public ServerUnban? Unban { get; set; } - - /// - /// Whether this ban should be automatically deleted from the database when it expires. - /// - /// - /// This isn't done automatically by the game, - /// you will need to set up something like a cron job to clear this from your database, - /// using a command like this: - /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()" - /// - public bool AutoDelete { get; set; } - - /// - /// Whether to display this ban in the admin remarks (notes) panel - /// - public bool Hidden { get; set; } - - public List BanHits { get; set; } = null!; - } - - /// - /// An explicit repeal of a by an administrator. - /// Having an entry for a ban neutralizes it. - /// - [Table("server_unban")] - public class ServerUnban : IUnbanCommon - { - [Column("unban_id")] public int Id { get; set; } - - /// - /// The ID of ban that is being repealed. - /// - public int BanId { get; set; } - - /// - /// The ban that is being repealed. - /// - public ServerBan Ban { get; set; } = null!; - - /// - /// The admin that repealed the ban. - /// - public Guid? UnbanningAdmin { get; set; } - - /// - /// The time the ban repealed. - /// - public DateTime UnbanTime { get; set; } - } - /// /// An exemption for a specific user to a certain type of . /// @@ -1095,7 +863,7 @@ public sealed class ServerBanExemption /// /// The ban flags to exempt this player from. - /// If any bit overlaps , the ban is ignored. + /// If any bit overlaps , the ban is ignored. /// public ServerBanExemptFlags Flags { get; set; } } @@ -1158,54 +926,10 @@ public class ServerBanHit public int BanId { get; set; } public int ConnectionId { get; set; } - public ServerBan Ban { get; set; } = null!; + public Ban Ban { get; set; } = null!; public ConnectionLog Connection { get; set; } = null!; } - [Table("server_role_ban"), Index(nameof(PlayerUserId))] - public sealed class ServerRoleBan : IBanCommon - { - public int Id { get; set; } - public int? RoundId { get; set; } - public Round? Round { get; set; } - public Guid? PlayerUserId { get; set; } - [Required] public TimeSpan PlaytimeAtNote { get; set; } - public NpgsqlInet? Address { get; set; } - public TypedHwid? HWId { get; set; } - - public DateTime BanTime { get; set; } - - public DateTime? ExpirationTime { get; set; } - - public string Reason { get; set; } = null!; - - public NoteSeverity Severity { get; set; } - [ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; } - public Player? CreatedBy { get; set; } - - [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; } - public Player? LastEditedBy { get; set; } - public DateTime? LastEditedAt { get; set; } - - public ServerRoleUnban? Unban { get; set; } - public bool Hidden { get; set; } - - public string RoleId { get; set; } = null!; - } - - [Table("server_role_unban")] - public sealed class ServerRoleUnban : IUnbanCommon - { - [Column("role_unban_id")] public int Id { get; set; } - - public int BanId { get; set; } - public ServerRoleBan Ban { get; set; } = null!; - - public Guid? UnbanningAdmin { get; set; } - - public DateTime UnbanTime { get; set; } - } - [Table("play_time")] public sealed class PlayTime { @@ -1405,31 +1129,31 @@ public sealed class BanTemplate /// /// The reason for the ban. /// - /// + /// public string Reason { get; set; } = ""; /// /// Exemptions granted to the ban. /// - /// + /// public ServerBanExemptFlags ExemptFlags { get; set; } /// /// Severity of the ban /// - /// + /// public NoteSeverity Severity { get; set; } /// /// Ban will be automatically deleted once expired. /// - /// + /// public bool AutoDelete { get; set; } /// /// Ban is not visible to players in the remarks menu. /// - /// + /// public bool Hidden { get; set; } } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index 7499d0b0f59..c3f41b7058e 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -39,10 +39,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // ReSharper disable StringLiteralTypo // Enforce that an address cannot be IPv6-mapped IPv4. // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - modelBuilder.Entity().ToTable(t => - t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); - - modelBuilder.Entity().ToTable( t => + modelBuilder.Entity().ToTable(t => t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address")); modelBuilder.Entity().ToTable(t => diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index 1ce9465847f..a64a2af8d9c 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -17,6 +17,9 @@ public sealed class SqliteServerDbContext : ServerDbContext { public SqliteServerDbContext(DbContextOptions options) : base(options) { +#if USE_SYSTEM_SQLITE + SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3()); +#endif } protected override void OnConfiguring(DbContextOptionsBuilder options) @@ -55,13 +58,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); modelBuilder - .Entity() - .Property(e => e.Address) - .HasColumnType("TEXT") - .HasConversion(ipMaskConverter); - - modelBuilder - .Entity() + .Entity() .Property(e => e.Address) .HasColumnType("TEXT") .HasConversion(ipMaskConverter); @@ -82,6 +79,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(log => log.Markings) .HasConversion(jsonByteArrayConverter); + modelBuilder.Entity() + .Property(log => log.OrganMarkings) + .HasConversion(jsonByteArrayConverter); + // EF core can make this automatically unique on sqlite but not psql. modelBuilder.Entity() .HasIndex(p => p.Address) diff --git a/Content.Server/Access/Systems/AccessOverriderSystem.cs b/Content.Server/Access/Systems/AccessOverriderSystem.cs index 4eaf3c04199..aa34dc2729b 100644 --- a/Content.Server/Access/Systems/AccessOverriderSystem.cs +++ b/Content.Server/Access/Systems/AccessOverriderSystem.cs @@ -148,7 +148,8 @@ private void UpdateUserInterface(EntityUid uid, AccessOverriderComponent compone missingAccess, privilegedIdName, targetLabel, - targetLabelColor); + targetLabelColor, + component.ShowPrivilegedId); _userInterface.SetUiState(uid, AccessOverriderUiKey.Key, newState); } diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs index 1706908e72d..6e7cd6a406a 100644 --- a/Content.Server/Access/Systems/AgentIDCardSystem.cs +++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs @@ -1,20 +1,22 @@ +using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Components; +using Content.Server.Clothing.Systems; +using Content.Server.Implants; using Content.Server.Popups; -using Content.Shared.UserInterface; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; -using Content.Shared.Interaction; -using Content.Shared.StatusIcon; -using Robust.Server.GameObjects; -using Robust.Shared.Prototypes; -using Content.Shared.Roles; -using System.Diagnostics.CodeAnalysis; -using Content.Server.Clothing.Systems; -using Content.Server.Implants; +using Content.Shared.Clothing.Components; using Content.Shared.Implants; +using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Lock; using Content.Shared.PDA; +using Content.Shared.Roles; +using Content.Shared.StatusIcon; +using Content.Shared.UserInterface; +using Content.Shared.VoiceMask; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; namespace Content.Server.Access.Systems { @@ -27,6 +29,7 @@ public sealed class AgentIDCardSystem : SharedAgentIdCardSystem [Dependency] private readonly ChameleonClothingSystem _chameleon = default!; [Dependency] private readonly ChameleonControllerSystem _chamController = default!; [Dependency] private readonly LockSystem _lock = default!; + [Dependency] private readonly SharedJobStatusSystem _jobStatus = default!; public override void Initialize() { @@ -38,6 +41,7 @@ public override void Initialize() SubscribeLocalEvent(OnJobChanged); SubscribeLocalEvent(OnJobIconChanged); SubscribeLocalEvent>(OnChameleonControllerOutfitChangedItem); + SubscribeLocalEvent>(OnVoiceMaskNameChanged); } private void OnChameleonControllerOutfitChangedItem(Entity ent, ref InventoryRelayedEvent args) @@ -76,7 +80,19 @@ private void OnChameleonControllerOutfitChangedItem(Entity if (!proto.TryGetComponent(out var comp, EntityManager.ComponentFactory)) return; - _chameleon.SetSelectedPrototype(ent, comp.IdCard); + if (TryComp(ent, out var chameleonComp) && chameleonComp.CanBeSetByController) + _chameleon.SetSelectedPrototype(ent, comp.IdCard, component: chameleonComp); + } + + private void OnVoiceMaskNameChanged(Entity ent, ref InventoryRelayedEvent args) + { + if (!TryComp(ent, out var idCard)) + return; + + if (!args.Args.VoiceMask.Comp.ChangeIDName) + return; + + _cardSystem.TryChangeFullName(ent, args.Args.NewName, idCard); } private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args) @@ -137,6 +153,8 @@ private void OnJobIconChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDC if (TryFindJobProtoFromIcon(jobIcon, out var job)) _cardSystem.TryChangeJobDepartment(uid, job, idCard); + + _jobStatus.UpdateStatus(Transform(uid).ParentUid); } private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job) diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs index 13f2430b3d9..1e4a0449019 100644 --- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs +++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs @@ -1,30 +1,35 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Chat.Systems; using Content.Server.Containers; using Content.Server.StationRecords.Systems; +using Content.Shared.Access; using Content.Shared.Access.Components; -using static Content.Shared.Access.Components.IdCardConsoleComponent; using Content.Shared.Access.Systems; -using Content.Shared.Access; using Content.Shared.Administration.Logs; +using Content.Shared.CCVar; +using Content.Shared.Chat; using Content.Shared.Construction; using Content.Shared.Containers.ItemSlots; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Roles; using Content.Shared.StationRecords; using Content.Shared.Throwing; using JetBrains.Annotations; using Robust.Server.GameObjects; +using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using static Content.Shared.Access.Components.IdCardConsoleComponent; namespace Content.Server.Access.Systems; [UsedImplicitly] public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem { + [Dependency] private readonly IConfigurationManager _cfgManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly StationRecordsSystem _record = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; @@ -83,7 +88,7 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component { newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, - PrivilegedIdIsAuthorized(uid, component), + PrivilegedIdIsAuthorized(uid, component, out _), false, null, null, @@ -108,7 +113,7 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, - PrivilegedIdIsAuthorized(uid, component), + PrivilegedIdIsAuthorized(uid, component, out _), true, targetIdComponent.FullName, targetIdComponent.LocalizedJobTitle, @@ -130,20 +135,30 @@ private void TryWriteToTargetId(EntityUid uid, string newFullName, string newJobTitle, List> newAccessList, - ProtoId newJobProto, + ProtoId? newJobProto, EntityUid player, IdCardConsoleComponent? component = null) { if (!Resolve(uid, ref component)) return; - if (component.TargetIdSlot.Item is not { Valid: true } targetId || !PrivilegedIdIsAuthorized(uid, component)) + if (component.TargetIdSlot.Item is not { Valid: true } targetId || !PrivilegedIdIsAuthorized(uid, component, out var privilegedId)) return; + // Limit name and job title lengths + var maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength); + var maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength); + + if (newFullName.Length > maxNameLength) + newFullName = newFullName[..maxNameLength]; + + if (newJobTitle.Length > maxIdJobLength) + newJobTitle = newJobTitle[..maxIdJobLength]; + _idCard.TryChangeFullName(targetId, newFullName, player: player); _idCard.TryChangeJobTitle(targetId, newJobTitle, player: player); - if (_prototype.TryIndex(newJobProto, out var job) + if (_prototype.TryIndex(newJobProto, out var job) && _prototype.Resolve(job.Icon, out var jobIcon)) { _idCard.TryChangeJobIcon(targetId, jobIcon, player: player); @@ -165,10 +180,7 @@ private void TryWriteToTargetId(EntityUid uid, return; } - var oldTags = _access.TryGetTags(targetId) ?? new List>(); - oldTags = oldTags.ToList(); - - var privilegedId = component.PrivilegedIdSlot.Item; + var oldTags = _access.TryGetTags(targetId)?.ToList() ?? new List>(); if (oldTags.SequenceEqual(newAccessList)) return; @@ -176,8 +188,7 @@ private void TryWriteToTargetId(EntityUid uid, // I hate that C# doesn't have an option for this and don't desire to write this out the hard way. // var difference = newAccessList.Difference(oldTags); var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet(); - // NULL SAFETY: PrivilegedIdIsAuthorized checked this earlier. - var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet(); + var privilegedPerms = _accessReader.FindAccessTags(privilegedId.Value); if (!difference.IsSubsetOf(privilegedPerms)) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions they could not give/take!"); @@ -190,26 +201,24 @@ private void TryWriteToTargetId(EntityUid uid, /*TODO: ECS SharedIdCardConsoleComponent and then log on card ejection, together with the save. This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/ - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); + _adminLogger.Add(LogType.Action, + $"{player} has modified {targetId} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); } /// /// Returns true if there is an ID in and said ID satisfies the requirements of . /// - /// - /// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant. - /// - private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent? component = null) + private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent component, [NotNullWhen(true)] out EntityUid? id) { - if (!Resolve(uid, ref component)) - return true; + id = null; + if (component.PrivilegedIdSlot.Item == null) + return false; + id = component.PrivilegedIdSlot.Item; if (!TryComp(uid, out var reader)) return true; - var privilegedId = component.PrivilegedIdSlot.Item; - return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader); + return _accessReader.IsAllowed(id.Value, uid, reader); } private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId newJobTitle, JobPrototype? newJobProto) diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index 648080b1fb5..7892a2542ab 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -6,7 +6,9 @@ using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; +using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.Kitchen; using Content.Shared.Popups; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Access/Systems/JobStatusSystem.cs b/Content.Server/Access/Systems/JobStatusSystem.cs new file mode 100644 index 00000000000..d6d38fbe539 --- /dev/null +++ b/Content.Server/Access/Systems/JobStatusSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Access.Systems; + +namespace Content.Server.Access.Systems; + +public sealed class JobStatusSystem : SharedJobStatusSystem; diff --git a/Content.Server/Actions/Commands/AddActionCommand.cs b/Content.Server/Actions/Commands/AddActionCommand.cs index 5b80c92aa91..10b53ad0230 100644 --- a/Content.Server/Actions/Commands/AddActionCommand.cs +++ b/Content.Server/Actions/Commands/AddActionCommand.cs @@ -21,7 +21,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 2) { - shell.WriteError(Loc.GetString(Loc.GetString("cmd-addaction-invalid-args"))); + shell.WriteError(Loc.GetString("cmd-addaction-invalid-args")); return; } diff --git a/Content.Server/Actions/Commands/RemoveActionCommand.cs b/Content.Server/Actions/Commands/RemoveActionCommand.cs index 541f8f88f98..084a4b5de6c 100644 --- a/Content.Server/Actions/Commands/RemoveActionCommand.cs +++ b/Content.Server/Actions/Commands/RemoveActionCommand.cs @@ -17,7 +17,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 2) { - shell.WriteError(Loc.GetString(Loc.GetString("cmd-rmaction-invalid-args"))); + shell.WriteError(Loc.GetString("cmd-rmaction-invalid-args")); return; } diff --git a/Content.Server/Administration/BanList/BanListEui.cs b/Content.Server/Administration/BanList/BanListEui.cs index 2ca126bf164..549a14f6733 100644 --- a/Content.Server/Administration/BanList/BanListEui.cs +++ b/Content.Server/Administration/BanList/BanListEui.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; using Content.Server.Administration.Managers; using Content.Server.Database; using Content.Server.EUI; using Content.Shared.Administration; using Content.Shared.Administration.BanList; +using Content.Shared.Database; using Content.Shared.Eui; using Robust.Shared.Network; @@ -22,8 +25,8 @@ public BanListEui() private Guid BanListPlayer { get; set; } private string BanListPlayerName { get; set; } = string.Empty; - private List Bans { get; } = new(); - private List RoleBans { get; } = new(); + private List Bans { get; } = new(); + private List RoleBans { get; } = new(); public override void Opened() { @@ -54,74 +57,38 @@ private void OnPermsChanged(AdminPermsChangedEventArgs args) private async Task LoadBans(NetUserId userId) { - foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null)) - { - SharedServerUnban? unban = null; - if (ban.Unban is { } unbanDef) - { - var unbanningAdmin = unbanDef.UnbanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); - } - - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; - - if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) - { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); - } - - Bans.Add(new SharedServerBan( - ban.Id, - ban.UserId, - ip, - hwid, - ban.BanTime.UtcDateTime, - ban.ExpirationTime?.UtcDateTime, - ban.Reason, - ban.BanningAdmin == null - ? null - : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, - unban - )); - } + await LoadBansCore(userId, BanType.Server, Bans); + await LoadBansCore(userId, BanType.Role, RoleBans); } - private async Task LoadRoleBans(NetUserId userId) + private async Task LoadBansCore(NetUserId userId, BanType banType, List list) { - foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null)) + foreach (var ban in await _db.GetBansAsync(null, userId, null, null, type: banType)) { - SharedServerUnban? unban = null; + SharedUnban? unban = null; if (ban.Unban is { } unbanDef) { var unbanningAdmin = unbanDef.UnbanningAdmin == null ? null : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username; - unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); + unban = new SharedUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime); } - (string, int cidrMask)? ip = ("*Hidden*", 0); - var hwid = "*Hidden*"; + ImmutableArray<(string, int cidrMask)> ips = [("*Hidden*", 0)]; + ImmutableArray hwids = ["*Hidden*"]; if (_admins.HasAdminFlag(Player, AdminFlags.Pii)) { - ip = ban.Address is { } address - ? (address.address.ToString(), address.cidrMask) - : null; - - hwid = ban.HWId?.ToString(); + ips = [..ban.Addresses.Select(a => (a.address.ToString(), a.cidrMask))]; + hwids = [..ban.HWIds.Select(h => h.ToString())]; } - RoleBans.Add(new SharedServerRoleBan( + + list.Add(new SharedBan( ban.Id, - ban.UserId, - ip, - hwid, + ban.Type, + ban.UserIds, + ips, + hwids, ban.BanTime.UtcDateTime, ban.ExpirationTime?.UtcDateTime, ban.Reason, @@ -129,7 +96,7 @@ private async Task LoadRoleBans(NetUserId userId) ? null : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username, unban, - ban.Role + ban.Roles )); } } @@ -144,7 +111,6 @@ private async Task LoadFromDb() string.Empty; await LoadBans(userId); - await LoadRoleBans(userId); StateDirty(); } diff --git a/Content.Server/Administration/BanPanelEui.cs b/Content.Server/Administration/BanPanelEui.cs index 4a4b7218729..fceb2b57504 100644 --- a/Content.Server/Administration/BanPanelEui.cs +++ b/Content.Server/Administration/BanPanelEui.cs @@ -26,8 +26,8 @@ public sealed class BanPanelEui : BaseEui private string PlayerName { get; set; } = string.Empty; private IPAddress? LastAddress { get; set; } private ImmutableTypedHwid? LastHwid { get; set; } - private const int Ipv4_CIDR = 32; - private const int Ipv6_CIDR = 64; + private const int Ipv4_CIDR = CreateBanInfo.DefaultMaskIpv4; + private const int Ipv6_CIDR = CreateBanInfo.DefaultMaskIpv6; public BanPanelEui() { @@ -73,6 +73,15 @@ private async void BanPlayer(Ban ban) return; } + var isRoleBan = ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0; + + CreateBanInfo banInfo = isRoleBan ? new CreateRoleBanInfo(ban.Reason) : new CreateServerBanInfo(ban.Reason); + + banInfo.WithBanningAdmin(Player.UserId); + banInfo.WithSeverity(ban.Severity); + if (ban.BanDurationMinutes > 0) + banInfo.WithMinutes(ban.BanDurationMinutes); + (IPAddress, int)? addressRange = null; if (ban.IpAddress is not null) { @@ -113,69 +122,46 @@ private async void BanPlayer(Ban ban) targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid; } - if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0) + if (addressRange != null) + banInfo.AddAddressRange(addressRange.Value); + + if (targetUid != null) + banInfo.AddUser(targetUid.Value, ban.Target!); + + banInfo.AddHWId(targetHWid); + + if (isRoleBan) { - var now = DateTimeOffset.UtcNow; - foreach (var role in ban.BannedJobs ?? []) + var roleBanInfo = (CreateRoleBanInfo)banInfo; + foreach (var row in ban.BannedJobs ?? []) { - _banManager.CreateRoleBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - role, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason, - now - ); + roleBanInfo.AddJob(row); } - foreach (var role in ban.BannedAntags ?? []) + foreach (var row in ban.BannedAntags ?? []) { - _banManager.CreateRoleBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - role, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason, - now - ); + roleBanInfo.AddAntag(row); } - Close(); - - return; + _banManager.CreateRoleBan(roleBanInfo); } - - if (ban.Erase && targetUid is not null) + else { - try - { - if (_entities.TrySystem(out AdminSystem? adminSystem)) - adminSystem.Erase(targetUid.Value); - } - catch (Exception e) + if (ban.Erase && targetUid is not null) { - _sawmill.Error($"Error while erasing banned player:\n{e}"); + try + { + if (_entities.TrySystem(out AdminSystem? adminSystem)) + adminSystem.Erase(targetUid.Value); + } + catch (Exception e) + { + _sawmill.Error($"Error while erasing banned player:\n{e}"); + } } - } - _banManager.CreateServerBan( - targetUid, - ban.Target, - Player.UserId, - addressRange, - targetHWid, - ban.BanDurationMinutes, - ban.Severity, - ban.Reason - ); + _banManager.CreateServerBan((CreateServerBanInfo)banInfo); + } Close(); } diff --git a/Content.Server/Administration/Commands/AddBodyPartCommand.cs b/Content.Server/Administration/Commands/AddBodyPartCommand.cs deleted file mode 100644 index a28fa93ad04..00000000000 --- a/Content.Server/Administration/Commands/AddBodyPartCommand.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Administration; -using Content.Shared.Body.Part; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands; - -[AdminCommand(AdminFlags.Admin)] -public sealed class AddBodyPartCommand : LocalizedEntityCommands -{ - [Dependency] private readonly BodySystem _bodySystem = default!; - - public override string Command => "addbodypart"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 4) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - if (!NetEntity.TryParse(args[0], out var childNetId) || !EntityManager.TryGetEntity(childNetId, out var childId)) - { - shell.WriteError(Loc.GetString("shell-invalid-entity-uid", ("uid", args[0]))); - return; - } - - if (!NetEntity.TryParse(args[1], out var parentNetId) || !EntityManager.TryGetEntity(parentNetId, out var parentId)) - { - shell.WriteError(Loc.GetString("shell-invalid-entity-uid", ("uid", args[1]))); - return; - } - - if (Enum.TryParse(args[3], out var partType) && - _bodySystem.TryCreatePartSlotAndAttach(parentId.Value, args[2], childId.Value, partType)) - { - shell.WriteLine($@"Added {childId} to {parentId}."); - } - else - shell.WriteError($@"Could not add {childId} to {parentId}."); - } -} diff --git a/Content.Server/Administration/Commands/AddMechanismCommand.cs b/Content.Server/Administration/Commands/AddMechanismCommand.cs deleted file mode 100644 index e62ddea47ba..00000000000 --- a/Content.Server/Administration/Commands/AddMechanismCommand.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Admin)] - public sealed class AddMechanismCommand : IConsoleCommand - { - [Dependency] private readonly IEntityManager _entManager = default!; - - public string Command => "addmechanism"; - public string Description => "Adds a given entity to a containing body."; - public string Help => "Usage: addmechanism "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); - return; - } - - if (!NetEntity.TryParse(args[0], out var organIdNet) || !_entManager.TryGetEntity(organIdNet, out var organId)) - { - shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); - return; - } - - if (!NetEntity.TryParse(args[1], out var partIdNet) || !_entManager.TryGetEntity(partIdNet, out var partId)) - { - shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); - return; - } - - var bodySystem = _entManager.System(); - - if (bodySystem.AddOrganToFirstValidSlot(partId.Value, organId.Value)) - { - shell.WriteLine($@"Added {organId} to {partId}."); - } - else - { - shell.WriteError($@"Could not add {organId} to {partId}."); - } - } - } -} diff --git a/Content.Server/Administration/Commands/BanCommand.cs b/Content.Server/Administration/Commands/BanCommand.cs index f76cfded814..a6d3b106462 100644 --- a/Content.Server/Administration/Commands/BanCommand.cs +++ b/Content.Server/Administration/Commands/BanCommand.cs @@ -90,7 +90,15 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] var targetUid = located.UserId; var targetHWid = located.LastHWId; - _bans.CreateServerBan(targetUid, target, player?.UserId, null, targetHWid, minutes, severity, reason); + var banInfo = new CreateServerBanInfo(reason); + banInfo.WithBanningAdmin(player?.UserId); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.WithSeverity(severity); + + _bans.CreateServerBan(banInfo); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/BanListCommand.cs b/Content.Server/Administration/Commands/BanListCommand.cs index ea68788deb5..26a8647248a 100644 --- a/Content.Server/Administration/Commands/BanListCommand.cs +++ b/Content.Server/Administration/Commands/BanListCommand.cs @@ -39,7 +39,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); + var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false); if (bans.Count == 0) { diff --git a/Content.Server/Administration/Commands/DSay.cs b/Content.Server/Administration/Commands/DSay.cs index f5e0b327933..60f81fce5dc 100644 --- a/Content.Server/Administration/Commands/DSay.cs +++ b/Content.Server/Administration/Commands/DSay.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; diff --git a/Content.Server/Administration/Commands/DeleteComponent.cs b/Content.Server/Administration/Commands/DeleteComponent.cs deleted file mode 100644 index c569a8375f7..00000000000 --- a/Content.Server/Administration/Commands/DeleteComponent.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Content.Shared.Administration; -using Robust.Shared.Console; - -namespace Content.Server.Administration.Commands -{ - [AdminCommand(AdminFlags.Spawn)] - public sealed class DeleteComponent : LocalizedEntityCommands - { - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "deletecomponent"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - switch (args.Length) - { - case 0: - shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument")); - break; - default: - var name = string.Join(" ", args); - - if (!_compFactory.TryGetRegistration(name, out var registration)) - { - shell.WriteLine(Loc.GetString($"cmd-deletecomponent-no-component-exists", ("name", name))); - break; - } - - var componentType = registration.Type; - var components = EntityManager.GetAllComponents(componentType, true); - - var i = 0; - - foreach (var (uid, component) in components) - { - EntityManager.RemoveComponent(uid, component); - i++; - } - - shell.WriteLine(Loc.GetString($"cmd-deletecomponent-success", ("count", i), ("name", name))); - - break; - } - } - } -} diff --git a/Content.Server/Administration/Commands/DeleteComponentCommand.cs b/Content.Server/Administration/Commands/DeleteComponentCommand.cs new file mode 100644 index 00000000000..250957bf35f --- /dev/null +++ b/Content.Server/Administration/Commands/DeleteComponentCommand.cs @@ -0,0 +1,42 @@ +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Spawn)] +public sealed class DeleteComponentCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "deletecomponent"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length == 0) + { + shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument")); + return; + } + + var name = string.Join(" ", args); + + if (!_compFactory.TryGetRegistration(name, out var registration)) + { + shell.WriteLine(Loc.GetString($"cmd-deletecomponent-no-component-exists", ("name", name))); + return; + } + + var componentType = registration.Type; + var components = EntityManager.GetAllComponents(componentType, true); + + var i = 0; + + foreach (var (uid, component) in components) + { + EntityManager.RemoveComponent(uid, component); + i++; + } + + shell.WriteLine(Loc.GetString($"cmd-deletecomponent-success", ("count", i), ("name", name))); + } +} diff --git a/Content.Server/Administration/Commands/DepartmentBanCommand.cs b/Content.Server/Administration/Commands/DepartmentBanCommand.cs index 15f9859ca10..40faeb74048 100644 --- a/Content.Server/Administration/Commands/DepartmentBanCommand.cs +++ b/Content.Server/Administration/Commands/DepartmentBanCommand.cs @@ -96,13 +96,20 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) var targetUid = located.UserId; var targetHWid = located.LastHWId; - // If you are trying to remove the following variable, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Without it the note list will get needlessly cluttered. - var now = DateTimeOffset.UtcNow; + var banInfo = new CreateRoleBanInfo(reason); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.AddUser(targetUid, located.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(targetHWid); + banInfo.WithSeverity(severity); + foreach (var job in departmentProto.Roles) { - _banManager.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, now); + banInfo.AddJob(job); } + + _banManager.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/LoadGameMapCommand.cs b/Content.Server/Administration/Commands/LoadGameMapCommand.cs index dfe18ea5bca..dc82fccd1e4 100644 --- a/Content.Server/Administration/Commands/LoadGameMapCommand.cs +++ b/Content.Server/Administration/Commands/LoadGameMapCommand.cs @@ -1,7 +1,7 @@ using System.Numerics; using Content.Server.GameTicking; -using Content.Server.Maps; using Content.Shared.Administration; +using Content.Shared.Maps; using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Prototypes; diff --git a/Content.Server/Administration/Commands/OSay.cs b/Content.Server/Administration/Commands/OSay.cs index 2f17bd9d70a..9c5a20ef693 100644 --- a/Content.Server/Administration/Commands/OSay.cs +++ b/Content.Server/Administration/Commands/OSay.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Content.Shared.Database; using Robust.Shared.Console; diff --git a/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs b/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs index 1beaaa928e3..df437fcc2d4 100644 --- a/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs +++ b/Content.Server/Administration/Commands/OpenAdminLogsCommand.cs @@ -10,7 +10,8 @@ public sealed class OpenAdminLogsCommand : LocalizedEntityCommands { [Dependency] private readonly EuiManager _euiManager = default!; - public override string Command => "adminlogs"; + public override string Command => Cmd; + public const string Cmd = "adminlogs"; public override void Execute(IConsoleShell shell, string argStr, string[] args) { diff --git a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs index 5577e134371..35a6d1096aa 100644 --- a/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs +++ b/Content.Server/Administration/Commands/OpenAdminNotesCommand.cs @@ -3,6 +3,7 @@ using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; +using Robust.Shared.Network; namespace Content.Server.Administration.Commands; @@ -46,7 +47,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] return; } - await _adminNotes.OpenEui(player, notedPlayer); + await _adminNotes.OpenEui(player, new NetUserId(notedPlayer)); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/PardonCommand.cs b/Content.Server/Administration/Commands/PardonCommand.cs index 5c4417a966f..eb292663e2e 100644 --- a/Content.Server/Administration/Commands/PardonCommand.cs +++ b/Content.Server/Administration/Commands/PardonCommand.cs @@ -27,7 +27,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] return; } - var ban = await _dbManager.GetServerBanAsync(banId); + var ban = await _dbManager.GetBanAsync(banId); if (ban == null) { @@ -50,7 +50,7 @@ public override async void Execute(IConsoleShell shell, string argStr, string[] return; } - await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now)); + await _dbManager.AddUnbanAsync(new UnbanDef(banId, player?.UserId, DateTimeOffset.Now)); shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId))); } diff --git a/Content.Server/Administration/Commands/RoleBanCommand.cs b/Content.Server/Administration/Commands/RoleBanCommand.cs index c49af32881b..f303f31b868 100644 --- a/Content.Server/Administration/Commands/RoleBanCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanCommand.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.Managers; +using Content.Server.Administration.Managers; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Database; @@ -99,12 +97,29 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) var targetUid = located.UserId; var targetHWid = located.LastHWId; + var banInfo = new CreateRoleBanInfo(reason); + if (minutes > 0) + banInfo.WithMinutes(minutes); + banInfo.AddUser(targetUid, located.Username); + banInfo.WithBanningAdmin(shell.Player?.UserId); + banInfo.AddHWId(targetHWid); + banInfo.WithSeverity(severity); + if (_proto.HasIndex(role)) - _bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); + { + banInfo.AddJob(new ProtoId(role)); + } else if (_proto.HasIndex(role)) - _bans.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow); + { + banInfo.AddAntag(new ProtoId(role)); + } else + { shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role))); + return; + } + + _bans.CreateRoleBan(banInfo); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/RoleBanListCommand.cs b/Content.Server/Administration/Commands/RoleBanListCommand.cs index 8244ded3b20..4abd406cbc0 100644 --- a/Content.Server/Administration/Commands/RoleBanListCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanListCommand.cs @@ -1,10 +1,8 @@ -using System.Linq; -using System.Text; -using Content.Server.Administration.BanList; +using Content.Server.Administration.BanList; using Content.Server.EUI; using Content.Server.Database; using Content.Shared.Administration; -using Robust.Server.Player; +using Content.Shared.Database; using Robust.Shared.Console; namespace Content.Server.Administration.Commands; @@ -48,7 +46,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) if (shell.Player is not { } player) { - var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned); + var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned, type: BanType.Role); if (bans.Count == 0) { @@ -58,7 +56,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) foreach (var ban in bans) { - var msg = $"ID: {ban.Id}: Role: {ban.Role} Reason: {ban.Reason}"; + var msg = $"ID: {ban.Id}: Role(s): {string.Join(",", ban.Roles ?? [])} Reason: {ban.Reason}"; shell.WriteLine(msg); } return; diff --git a/Content.Server/Administration/Commands/ShuttleCommands.cs b/Content.Server/Administration/Commands/ShuttleCommands.cs index 677c38ac4e9..f8f8c201a5b 100644 --- a/Content.Server/Administration/Commands/ShuttleCommands.cs +++ b/Content.Server/Administration/Commands/ShuttleCommands.cs @@ -16,13 +16,13 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { // ReSharper disable once ConvertIfStatementToSwitchStatement if (args.Length == 1 && TimeSpan.TryParseExact(args[0], ContentLocalizationManager.TimeSpanMinutesFormats, LocalizationManager.DefaultCulture, out var timeSpan)) - _roundEndSystem.RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, false); + _roundEndSystem.RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, checkCooldown: false); else if (args.Length == 1) shell.WriteLine(Loc.GetString("shell-timespan-minutes-must-be-correct")); else - _roundEndSystem.RequestRoundEnd(shell.Player?.AttachedEntity, false); + _roundEndSystem.RequestRoundEnd(shell.Player?.AttachedEntity, checkCooldown: false); } } @@ -35,7 +35,7 @@ public sealed class RecallShuttleCommand : LocalizedEntityCommands public override void Execute(IConsoleShell shell, string argStr, string[] args) { - _roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, false); + _roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, forceRecall: true); } } } diff --git a/Content.Server/Administration/Commands/TippyCommand.cs b/Content.Server/Administration/Commands/TippyCommand.cs new file mode 100644 index 00000000000..b79c1607066 --- /dev/null +++ b/Content.Server/Administration/Commands/TippyCommand.cs @@ -0,0 +1,103 @@ +using Content.Shared.Administration; +using Content.Shared.Tips; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Fun)] +public sealed class TippyCommand : LocalizedEntityCommands +{ + [Dependency] private readonly SharedTipsSystem _tips = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + public override string Command => "tippy"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("cmd-tippy-help")); + return; + } + + ICommonSession? targetSession = null; + if (args[0] != "all") + { + if (!_player.TryGetSessionByUsername(args[0], out targetSession)) + { + shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); + return; + } + } + + var msg = args[1]; + + EntProtoId? prototype = null; + if (args.Length > 2) + { + if (args[2] == "null") + prototype = null; + else if (!_prototype.HasIndex(args[2])) + { + shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2]))); + return; + } + else + prototype = args[2]; + } + + var speakTime = _tips.GetSpeechTime(msg); + var slideTime = 3f; + var waddleInterval = 0.5f; + + if (args.Length > 3 && float.TryParse(args[3], out var parsedSpeakTime)) + speakTime = parsedSpeakTime; + + if (args.Length > 4 && float.TryParse(args[4], out var parsedSlideTime)) + slideTime = parsedSlideTime; + + if (args.Length > 5 && float.TryParse(args[5], out var parsedWaddleInterval)) + waddleInterval = parsedWaddleInterval; + + if (targetSession != null) // send to specified player + _tips.SendTippy(targetSession, msg, prototype, speakTime, slideTime, waddleInterval); + else // send to everyone + _tips.SendTippy(msg, prototype, speakTime, slideTime, waddleInterval); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHintOptions( + CompletionHelper.SessionNames(players: _player), + Loc.GetString("cmd-tippy-auto-1")), + 2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")), + 3 => CompletionResult.FromHintOptions( + CompletionHelper.PrototypeIdsLimited(args[2], _prototype), + Loc.GetString("cmd-tippy-auto-3")), + 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")), + 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), + 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), + _ => CompletionResult.Empty + }; + } +} + +[AdminCommand(AdminFlags.Fun)] +public sealed class TipCommand : LocalizedEntityCommands +{ + [Dependency] private readonly SharedTipsSystem _tips = default!; + + public override string Command => "tip"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + _tips.AnnounceRandomTip(); + _tips.RecalculateNextTipTime(); + } +} diff --git a/Content.Server/Administration/Components/AdminMinigunComponent.cs b/Content.Server/Administration/Components/AdminMinigunComponent.cs deleted file mode 100644 index 368d3d3ba76..00000000000 --- a/Content.Server/Administration/Components/AdminMinigunComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Administration.Components; - -[RegisterComponent] -public sealed partial class AdminMinigunComponent : Component -{ - -} diff --git a/Content.Server/Administration/Components/BufferingComponent.cs b/Content.Server/Administration/Components/BufferingComponent.cs deleted file mode 100644 index 1f803765969..00000000000 --- a/Content.Server/Administration/Components/BufferingComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Server.Administration.Systems; - -namespace Content.Server.Administration.Components; - -[RegisterComponent, Access(typeof(BufferingSystem))] -public sealed partial class BufferingComponent : Component -{ - [DataField("minBufferTime")] - public float MinimumBufferTime = 0.5f; - [DataField("maxBufferTime")] - public float MaximumBufferTime = 1.5f; - [DataField("minTimeTilNextBuffer")] - public float MinimumTimeTilNextBuffer = 10.0f; - [DataField("maxTimeTilNextBuffer")] - public float MaximumTimeTilNextBuffer = 120.0f; - [DataField("timeTilNextBuffer")] - public float TimeTilNextBuffer = 15.0f; - [DataField("bufferingIcon")] - public EntityUid? BufferingIcon = null; - [DataField("bufferingTimer")] - public float BufferingTimer = 0.0f; -} diff --git a/Content.Server/Administration/Components/KillSignComponent.cs b/Content.Server/Administration/Components/KillSignComponent.cs deleted file mode 100644 index 11479c32fc4..00000000000 --- a/Content.Server/Administration/Components/KillSignComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Administration.Components; -using Robust.Shared.GameStates; - -namespace Content.Server.Administration.Components; - -[RegisterComponent] -public sealed partial class KillSignComponent : SharedKillSignComponent; diff --git a/Content.Server/Administration/Components/StationInfiniteBatteryTargetComponent.cs b/Content.Server/Administration/Components/StationInfiniteBatteryTargetComponent.cs deleted file mode 100644 index 5543d5ed8c4..00000000000 --- a/Content.Server/Administration/Components/StationInfiniteBatteryTargetComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Server.Administration.Components; - -/// -/// This is used for the admin map-wide/station-wide/grid-wide infinite power trick. -/// -[RegisterComponent] -public sealed partial class StationInfiniteBatteryTargetComponent : Component -{ - -} diff --git a/Content.Server/Administration/Components/SuperBonkComponent.cs b/Content.Server/Administration/Components/SuperBonkComponent.cs index 0ceeccd136d..9c20c4d88a4 100644 --- a/Content.Server/Administration/Components/SuperBonkComponent.cs +++ b/Content.Server/Administration/Components/SuperBonkComponent.cs @@ -6,7 +6,8 @@ namespace Content.Server.Administration.Components; /// /// Component to track the timer for the SuperBonk smite. /// -[RegisterComponent, AutoGenerateComponentPause, Access(typeof(SuperBonkSystem))] +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(SuperBonkSystem))] public sealed partial class SuperBonkComponent : Component { /// diff --git a/Content.Server/Administration/Logs/AdminLogManager.cs b/Content.Server/Administration/Logs/AdminLogManager.cs index e7682cf5595..2587d4b8f91 100644 --- a/Content.Server/Administration/Logs/AdminLogManager.cs +++ b/Content.Server/Administration/Logs/AdminLogManager.cs @@ -12,13 +12,16 @@ using Content.Shared.Mind; using Content.Shared.Players.PlayTimeTracking; using Prometheus; +using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Configuration; +using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Administration.Logs; @@ -338,7 +341,7 @@ public override void Add(LogType type, LogImpact impact, [System.Runtime.Compile Players = players, }; - DoAdminAlerts(players, message, impact); + DoAdminAlerts(players, message, impact, handler); if (preRound) { @@ -380,6 +383,34 @@ private List GetPlayers(Dictionary values, int return players; } + /// + /// Get a list of coordinates from the s values. Will transform all coordinate types + /// to map coordinates! + /// + /// A list of map coordinates that were found in the value input, can return an empty list. + private List GetCoordinates(Dictionary values) + { + List coordList = new(); + EntityManager.TrySystem(out TransformSystem? transform); + + foreach (var value in values.Values) + { + switch (value) + { + case EntityCoordinates entCords: + if (transform != null) + coordList.Add(transform.ToMapCoordinates(entCords)); + continue; + + case MapCoordinates mapCord: + coordList.Add(mapCord); + continue; + } + } + + return coordList; + } + private void AddPlayer(List players, Guid user, int logId) { // The majority of logs have a single player, or maybe two. Instead of allocating a List and @@ -397,10 +428,11 @@ private void AddPlayer(List players, Guid user, int logId) }); } - private void DoAdminAlerts(List players, string message, LogImpact impact) + private void DoAdminAlerts(List players, string message, LogImpact impact, LogStringHandler handler) { var adminLog = false; var logMessage = message; + var playerNetEnts = new List<(NetEntity, string)>(); foreach (var player in players) { @@ -419,6 +451,8 @@ private void DoAdminAlerts(List players, string message, LogImpa ("name", cachedInfo.CharacterName), ("subtype", subtype)); } + if (cachedInfo != null && cachedInfo.NetEntity != null) + playerNetEnts.Add((cachedInfo.NetEntity.Value, cachedInfo.CharacterName)); } if (adminLog) @@ -442,7 +476,73 @@ private void DoAdminAlerts(List players, string message, LogImpa } if (adminLog) + { _chat.SendAdminAlert(logMessage); + + if (CreateTpLinks(playerNetEnts, out var tpLinks)) + _chat.SendAdminAlertNoFormatOrEscape(tpLinks); + + var coords = GetCoordinates(handler.Values); + + if (CreateCordLinks(coords, out var cordLinks)) + _chat.SendAdminAlertNoFormatOrEscape(cordLinks); + } + } + + /// + /// Creates a list of tpto command links of the given players + /// + private bool CreateTpLinks(List<(NetEntity NetEnt, string CharacterName)> players, out string outString) + { + outString = string.Empty; + + if (players.Count == 0) + return false; + + outString = Loc.GetString("admin-alert-tp-to-players-header"); + + for (var i = 0; i < players.Count; i++) + { + var player = players[i]; + outString += $"[cmdlink=\"{EscapeText(player.CharacterName)}\" command=\"tpto {player.NetEnt}\"/]"; + + if (i < players.Count - 1) + outString += ", "; + } + + return true; + } + + /// + /// Creates a list of toto command links for the given map coordinates. + /// + private bool CreateCordLinks(List cords, out string outString) + { + outString = string.Empty; + + if (cords.Count == 0) + return false; + + outString = Loc.GetString("admin-alert-tp-to-coords-header"); + + for (var i = 0; i < cords.Count; i++) + { + var cord = cords[i]; + outString += $"[cmdlink=\"{cord.ToString()}\" command=\"tp {cord.X} {cord.Y} {cord.MapId}\"/]"; + + if (i < cords.Count - 1) + outString += ", "; + } + + return true; + } + + /// + /// Escape the given text to not allow breakouts of the cmdlink tags. + /// + private string EscapeText(string text) + { + return FormattedMessage.EscapeText(text).Replace("\"", "\\\"").Replace("'", "\\'"); } public async Task> All(LogFilter? filter = null, Func>? listProvider = null) diff --git a/Content.Server/Administration/Logs/Converters/DamageSpecifierConverter.cs b/Content.Server/Administration/Logs/Converters/DamageSpecifierConverter.cs new file mode 100644 index 00000000000..b129307e748 --- /dev/null +++ b/Content.Server/Administration/Logs/Converters/DamageSpecifierConverter.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using Content.Shared.Damage; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Server.Administration.Logs.Converters; + +[AdminLogConverter] +public sealed class DamageSpecifierConverter : AdminLogConverter +{ + public override void Write(Utf8JsonWriter writer, DamageSpecifier value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (var (damage, amount) in value.DamageDict) + { + writer.WriteNumber(damage.Id, amount.Double()); + } + writer.WriteEndObject(); + } +} diff --git a/Content.Server/Administration/Managers/BanManager.Notification.cs b/Content.Server/Administration/Managers/BanManager.Notification.cs index ff84887f00d..d627dc508f1 100644 --- a/Content.Server/Administration/Managers/BanManager.Notification.cs +++ b/Content.Server/Administration/Managers/BanManager.Notification.cs @@ -41,14 +41,8 @@ private bool OnDatabaseNotificationEarlyFilter() private async void ProcessBanNotification(BanNotificationData data) { - if ((await _entryManager.ServerEntity).Id == data.ServerId) - { - _sawmill.Verbose("Not processing ban notification: came from this server"); - return; - } - _sawmill.Verbose($"Processing ban notification for ban {data.BanId}"); - var ban = await _db.GetServerBanAsync(data.BanId); + var ban = await _db.GetBanAsync(data.BanId); if (ban == null) { _sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?"); @@ -86,15 +80,5 @@ private sealed class BanNotificationData /// [JsonRequired, JsonPropertyName("ban_id")] public int BanId { get; init; } - - /// - /// The id of the server the ban was made on. - /// This is used to avoid double work checking the ban on the originating server. - /// - /// - /// This is optional in case the ban was made outside a server (SS14.Admin) - /// - [JsonPropertyName("server_id")] - public int? ServerId { get; init; } } } diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs index 17f796e699c..650e1a18723 100644 --- a/Content.Server/Administration/Managers/BanManager.cs +++ b/Content.Server/Administration/Managers/BanManager.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -21,6 +20,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Administration.Managers; @@ -29,7 +29,6 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IServerDbManager _db = default!; - [Dependency] private readonly ServerDbEntryManager _entryManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ILocalizationManager _localizationManager = default!; [Dependency] private readonly ILogManager _logManager = default!; @@ -43,10 +42,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit private ISawmill _sawmill = default!; public const string SawmillId = "admin.bans"; - public const string PrefixAntag = "Antag:"; - public const string PrefixJob = "Job:"; + public const string DbTypeAntag = "Antag"; + public const string DbTypeJob = "Job"; - private readonly Dictionary> _cachedRoleBans = new(); + private readonly Dictionary> _cachedRoleBans = new(); // Cached ban exemption flags are used to handle private readonly Dictionary _cachedBanExemptions = new(); @@ -72,9 +71,15 @@ private async Task CachePlayerData(ICommonSession player, CancellationToken canc var netChannel = player.Channel; ImmutableArray? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId; var modernHwids = netChannel.UserData.ModernHWIds; - var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false); - - var userRoleBans = new List(); + var roleBans = await _db.GetBansAsync( + netChannel.RemoteEndPoint.Address, + player.UserId, + hwId, + modernHwids, + false, + type: BanType.Role); + + var userRoleBans = new List(); foreach (var ban in roleBans) { userRoleBans.Add(ban); @@ -115,43 +120,37 @@ public void Restart() } #region Server Bans - public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason) + public async void CreateServerBan(CreateServerBanInfo banInfo) { - DateTimeOffset? expires = null; - if (minutes > 0) + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Server, null); + + await _db.AddBanAsync(banDef); + + if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules)) { - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + // Reset their last read rules. They probably need a refresher! + foreach (var (userId, _) in banInfo.Users) + { + await _db.SetLastReadRules(userId, null); + } } - _systems.TryGetEntitySystem(out var ticker); - int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; - var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; - - var banDef = new ServerBanDef( - null, - target, - addressRange, - hwid, - DateTimeOffset.Now, - expires, - roundId, - playtime, - reason, - severity, - banningAdmin, - null); - - await _db.AddServerBanAsync(banDef); - if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null) - await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher! - var adminName = banningAdmin == null + var adminName = banInfo.BanningAdmin == null ? Loc.GetString("system-user") - : (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); - var targetName = target is null ? "null" : $"{targetUsername} ({target})"; - var addressRangeString = addressRange != null - ? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}" - : "null"; - var hwidString = hwid?.ToString() ?? "null"; + : (await _db.GetPlayerRecordByUserId(banInfo.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user"); + + var targetName = banInfo.Users.Count == 0 + ? "null" + : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})")); + + var addressRangeString = banInfo.AddressRanges.Count != 0 + ? "null" + : string.Join(", ", banInfo.AddressRanges.Select(a => $"{a.Address}/{a.Mask}")); + + var hwidString = banInfo.HWIds.Count == 0 + ? "null" + : string.Join(", ", banInfo.HWIds); + var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}"; var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii"; @@ -159,12 +158,12 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net var logMessage = Loc.GetString( key, ("admin", adminName), - ("severity", severity), + ("severity", banDef.Severity), ("expires", expiresString), ("name", targetName), ("ip", addressRangeString), ("hwid", hwidString), - ("reason", reason)); + ("reason", banInfo.Reason)); _sawmill.Info(logMessage); _chat.SendAdminAlert(logMessage); @@ -172,7 +171,19 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net KickMatchingConnectedPlayers(banDef, "newly placed ban"); } - private void KickMatchingConnectedPlayers(ServerBanDef def, string source) + private NoteSeverity GetSeverityForServerBan(CreateBanInfo banInfo, CVarDef defaultCVar) + { + if (banInfo.Severity != null) + return banInfo.Severity.Value; + + if (Enum.TryParse(_cfg.GetCVar(defaultCVar), true, out NoteSeverity parsedSeverity)) + return parsedSeverity; + + _sawmill.Error($"CVar {defaultCVar.Name} has invalid ban severity!"); + return NoteSeverity.None; + } + + private void KickMatchingConnectedPlayers(BanDef def, string source) { foreach (var player in _playerManager.Sessions) { @@ -184,7 +195,7 @@ private void KickMatchingConnectedPlayers(ServerBanDef def, string source) } } - private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) + private bool BanMatchesPlayer(ICommonSession player, BanDef ban) { var playerInfo = new BanMatcher.PlayerInfo { @@ -201,7 +212,7 @@ private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban) return BanMatcher.BanMatches(ban, playerInfo); } - private void KickForBanDef(ICommonSession player, ServerBanDef def) + private void KickForBanDef(ICommonSession player, BanDef def) { var message = def.FormatBanMessage(_cfg, _localizationManager); player.Channel.Disconnect(message); @@ -211,108 +222,154 @@ private void KickForBanDef(ICommonSession player, ServerBanDef def) #region Role Bans - // If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin. - // Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset. - public async void CreateRoleBan( - NetUserId? target, - string? targetUsername, - NetUserId? banningAdmin, - (IPAddress, int)? addressRange, - ImmutableTypedHwid? hwid, - ProtoId role, - uint? minutes, - NoteSeverity severity, - string reason, - DateTimeOffset timeOfBan - ) where T : class, IPrototype + public async void CreateRoleBan(CreateRoleBanInfo banInfo) { - string encodedRole; + ImmutableArray roleDefs = + [ + .. ToBanRoleDef(banInfo.JobPrototypes), + .. ToBanRoleDef(banInfo.AntagPrototypes), + ]; - // TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced - // this check has consciously avoided refactoring Job and Antag prototype. - // Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash. + if (roleDefs.Length == 0) + throw new ArgumentException("Must specify at least one role to ban!"); - //TODO remove this check as part of the above refactor - if (_prototypeManager.HasIndex(role) && _prototypeManager.HasIndex(role)) - { - _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype."); + var (banDef, expires) = await CreateBanDef(banInfo, BanType.Role, roleDefs); - return; - } + await AddRoleBan(banDef); - // Don't trust the input: make sure the job or antag actually exists. - if (_prototypeManager.HasIndex(role)) - encodedRole = PrefixJob + role; - else if (_prototypeManager.HasIndex(role)) - encodedRole = PrefixAntag + role; - else - { - _sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype."); + var length = expires == null + ? Loc.GetString("cmd-roleban-inf") + : Loc.GetString("cmd-roleban-until", ("expires", expires)); - return; + var targetName = banInfo.Users.Count == 0 + ? "null" + : string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})")); + + _chat.SendAdminAlert(Loc.GetString( + "cmd-roleban-success", + ("target", targetName), + ("role", string.Join(", ", roleDefs)), + ("reason", banInfo.Reason), + ("length", length))); + + foreach (var (userId, _) in banInfo.Users) + { + if (_playerManager.TryGetSessionById(userId, out var session)) + SendRoleBans(session); } + } - DateTimeOffset? expires = null; + private async Task<(BanDef Ban, DateTimeOffset? Expires)> CreateBanDef( + CreateBanInfo banInfo, + BanType type, + ImmutableArray? roleBans) + { + if (banInfo.Users.Count == 0 && banInfo.HWIds.Count == 0 && banInfo.AddressRanges.Count == 0) + throw new ArgumentException("Must specify at least one user, HWID, or address range"); - if (minutes > 0) - expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value); + DateTimeOffset? expires = null; + if (banInfo.Duration is { } duration) + expires = DateTimeOffset.Now + duration; - _systems.TryGetEntitySystem(out GameTicker? ticker); - int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId; - var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero; + ImmutableArray roundIds; + if (banInfo.RoundIds.Count > 0) + { + roundIds = [..banInfo.RoundIds]; + } + else if (_systems.TryGetEntitySystem(out var ticker) && ticker.RoundId != 0) + { + roundIds = [ticker.RoundId]; + } + else + { + roundIds = []; + } - var banDef = new ServerRoleBanDef( + return (new BanDef( null, - target, - addressRange, - hwid, - timeOfBan, + type, + [..banInfo.Users.Select(u => u.UserId)], + [..banInfo.AddressRanges], + [..banInfo.HWIds], + DateTimeOffset.Now, expires, - roundId, - playtime, - reason, - severity, - banningAdmin, + roundIds, + await GetPlayTime(banInfo), + banInfo.Reason, + GetSeverityForServerBan(banInfo, CCVars.ServerBanDefaultSeverity), + banInfo.BanningAdmin, null, - encodedRole); + roles: roleBans), expires); + } + + private async Task GetPlayTime(CreateBanInfo banInfo) + { + var firstPlayer = banInfo.Users.FirstOrNull()?.UserId; + if (firstPlayer == null) + return TimeSpan.Zero; - if (!await AddRoleBan(banDef)) + return (await _db.GetPlayTimes(firstPlayer.Value)) + .Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall) + ?.TimeSpent ?? TimeSpan.Zero; + } + + private IEnumerable ToBanRoleDef(IEnumerable> protoIds) where T : class, IPrototype + { + return protoIds.Select(protoId => { - _chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role))); + // TODO: I have no idea if this check is necessary. The previous code was a complete mess, + // so out of safety I'm leaving this in. + if (_prototypeManager.HasIndex(protoId) && _prototypeManager.HasIndex(protoId)) + { + throw new InvalidOperationException( + $"Creating role ban for {protoId}: cannot create role ban, role is both JobPrototype and AntagPrototype."); + } - return; - } + // Don't trust the input: make sure the role actually exists. + if (!_prototypeManager.HasIndex(protoId)) + throw new UnknownPrototypeException(protoId, typeof(T)); - var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires)); - _chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length))); + return new BanRoleDef(PrototypeKindToDbType(), protoId); + }); + } + + private static string PrototypeKindToDbType() where T : class, IPrototype + { + if (typeof(T) == typeof(JobPrototype)) + return DbTypeJob; - if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session)) - SendRoleBans(session); + if (typeof(T) == typeof(AntagPrototype)) + return DbTypeAntag; + + throw new ArgumentException($"Unknown prototype kind for role bans: {typeof(T)}"); } - private async Task AddRoleBan(ServerRoleBanDef banDef) + private async Task AddRoleBan(BanDef banDef) { - banDef = await _db.AddServerRoleBanAsync(banDef); + banDef = await _db.AddBanAsync(banDef); - if (banDef.UserId != null - && _playerManager.TryGetSessionById(banDef.UserId, out var player) - && _cachedRoleBans.TryGetValue(player, out var cachedBans)) + foreach (var user in banDef.UserIds) { - cachedBans.Add(banDef); + if (_playerManager.TryGetSessionById(user, out var player) + && _cachedRoleBans.TryGetValue(player, out var cachedBans)) + { + cachedBans.Add(banDef); + } } - - return true; } public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) { - var ban = await _db.GetServerRoleBanAsync(banId); + var ban = await _db.GetBanAsync(banId); if (ban == null) { return $"No ban found with id {banId}"; } + if (ban.Type != BanType.Role) + throw new InvalidOperationException("Ban was not a role ban!"); + if (ban.Unban != null) { var response = new StringBuilder("This ban has already been pardoned"); @@ -326,14 +383,17 @@ public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da return response.ToString(); } - await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); + await _db.AddUnbanAsync(new UnbanDef(banId, unbanningAdmin, DateTimeOffset.Now)); - if (ban.UserId is { } player - && _playerManager.TryGetSessionById(player, out var session) - && _cachedRoleBans.TryGetValue(session, out var roleBans)) + foreach (var user in ban.UserIds) { - roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); - SendRoleBans(session); + if (_playerManager.TryGetSessionById(user, out var session) + && _cachedRoleBans.TryGetValue(session, out var roleBans)) + { + roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id); + SendRoleBans(session); + } + } return $"Pardoned ban with id {banId}"; @@ -341,64 +401,69 @@ public async Task PardonRoleBan(int banId, NetUserId? unbanningAdmin, Da public HashSet>? GetJobBans(NetUserId playerUserId) { - return GetRoleBans(playerUserId, PrefixJob); + return GetRoleBans(playerUserId); } public HashSet>? GetAntagBans(NetUserId playerUserId) { - return GetRoleBans(playerUserId, PrefixAntag); + return GetRoleBans(playerUserId); } - private HashSet>? GetRoleBans(NetUserId playerUserId, string prefix) where T : class, IPrototype + private HashSet>? GetRoleBans(NetUserId playerUserId) where T : class, IPrototype { if (!_playerManager.TryGetSessionById(playerUserId, out var session)) return null; - return GetRoleBans(session, prefix); + return GetRoleBans(session); } - private HashSet>? GetRoleBans(ICommonSession playerSession, string prefix) where T : class, IPrototype + private HashSet>? GetRoleBans(ICommonSession playerSession) where T : class, IPrototype { if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans)) return null; + var dbType = PrototypeKindToDbType(); + return roleBans - .Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal)) - .Select(ban => new ProtoId(ban.Role[prefix.Length..])) + .SelectMany(ban => ban.Roles!.Value) + .Where(role => role.RoleType == dbType) + .Select(role => new ProtoId(role.RoleId)) .ToHashSet(); } - public HashSet? GetRoleBans(NetUserId playerUserId) + public HashSet? GetRoleBans(NetUserId playerUserId) { if (!_playerManager.TryGetSessionById(playerUserId, out var session)) return null; return _cachedRoleBans.TryGetValue(session, out var roleBans) - ? roleBans.Select(banDef => banDef.Role).ToHashSet() + ? roleBans.SelectMany(banDef => banDef.Roles ?? []).ToHashSet() : null; } public bool IsRoleBanned(ICommonSession player, List> jobs) { - return IsRoleBanned(player, jobs, PrefixJob); + return IsRoleBanned(player, jobs); } public bool IsRoleBanned(ICommonSession player, List> antags) { - return IsRoleBanned(player, antags, PrefixAntag); + return IsRoleBanned(player, antags); } - private bool IsRoleBanned(ICommonSession player, List> roles, string prefix) where T : class, IPrototype + private bool IsRoleBanned(ICommonSession player, List> roles) where T : class, IPrototype { var bans = GetRoleBans(player.UserId); if (bans is null || bans.Count == 0) return false; + var dbType = PrototypeKindToDbType(); + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (var role in roles) { - if (bans.Contains(prefix + role)) + if (bans.Contains(new BanRoleDef(dbType, role))) return true; } @@ -407,34 +472,10 @@ private bool IsRoleBanned(ICommonSession player, List> roles, stri public void SendRoleBans(ICommonSession pSession) { - var jobBans = GetRoleBans(pSession, PrefixJob); - var jobBansList = new List(jobBans?.Count ?? 0); - - if (jobBans is not null) - { - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (var encodedId in jobBans) - { - jobBansList.Add(encodedId.ToString().Replace(PrefixJob, "")); - } - } - - var antagBans = GetRoleBans(pSession, PrefixAntag); - var antagBansList = new List(antagBans?.Count ?? 0); - - if (antagBans is not null) - { - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (var encodedId in antagBans) - { - antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, "")); - } - } - var bans = new MsgRoleBans() { - JobBans = jobBansList, - AntagBans = antagBansList, + JobBans = (GetRoleBans(pSession) ?? []).ToList(), + AntagBans = (GetRoleBans(pSession) ?? []).ToList(), }; _sawmill.Debug($"Sent role bans to {pSession.Name}"); diff --git a/Content.Server/Administration/Managers/IBanManager.cs b/Content.Server/Administration/Managers/IBanManager.cs index 1912ebe9ecd..633ae968dba 100644 --- a/Content.Server/Administration/Managers/IBanManager.cs +++ b/Content.Server/Administration/Managers/IBanManager.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Content.Shared.Database; using Content.Shared.Roles; @@ -13,6 +14,11 @@ public interface IBanManager public void Initialize(); public void Restart(); + /// + /// Create a server ban in the database, blocking connection for matching players. + /// + void CreateServerBan(CreateServerBanInfo banInfo); + /// /// Bans the specified target, address range and / or HWID. One of them must be non-null /// @@ -23,12 +29,44 @@ public interface IBanManager /// Number of minutes to ban for. 0 and null mean permanent /// Severity of the resulting ban note /// Reason for the ban - public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason); + [Obsolete("Use CreateServerBan(CreateBanInfo) instead")] + public void CreateServerBan(NetUserId? target, + string? targetUsername, + NetUserId? banningAdmin, + (IPAddress, int)? addressRange, + ImmutableTypedHwid? hwid, + uint? minutes, + NoteSeverity severity, + string reason) + { + var info = new CreateServerBanInfo(reason); + if (target != null) + { + ArgumentNullException.ThrowIfNull(targetUsername); + info.AddUser(target.Value, targetUsername); + } + + if (addressRange != null) + info.AddAddressRange(addressRange.Value); + + if (hwid != null) + info.AddHWId(hwid); + + if (minutes > 0) + info.WithMinutes(minutes.Value); + + if (banningAdmin != null) + info.WithBanningAdmin(banningAdmin.Value); + + info.WithSeverity(severity); + + CreateServerBan(info); + } /// /// Gets a list of prefixed prototype IDs with the player's role bans. /// - public HashSet? GetRoleBans(NetUserId playerUserId); + public HashSet? GetRoleBans(NetUserId playerUserId); /// /// Checks if the player is currently banned from any of the listed roles. @@ -57,33 +95,12 @@ public interface IBanManager public HashSet>? GetAntagBans(NetUserId playerUserId); /// - /// Creates a job ban for the specified target, username or GUID + /// Creates a role ban, preventing matching players from playing said roles. /// - /// Target user, username or GUID, null for none - /// The username of the target, if known - /// The responsible admin for the ban - /// The range of IPs that are to be banned, if known - /// The HWID to be banned, if known - /// The role ID to be banned from. Either an AntagPrototype or a JobPrototype - /// Number of minutes to ban for. 0 and null mean permanent - /// Severity of the resulting ban note - /// Reason for the ban - /// Time when the ban was applied, used for grouping role bans - public void CreateRoleBan( - NetUserId? target, - string? targetUsername, - NetUserId? banningAdmin, - (IPAddress, int)? addressRange, - ImmutableTypedHwid? hwid, - ProtoId role, - uint? minutes, - NoteSeverity severity, - string reason, - DateTimeOffset timeOfBan - ) where T : class, IPrototype; + public void CreateRoleBan(CreateRoleBanInfo banInfo); /// - /// Pardons a role ban for the specified target, username or GUID + /// Pardons a role ban by its ID. /// /// The id of the role ban to pardon. /// The admin, if any, that pardoned the role ban. @@ -96,3 +113,287 @@ DateTimeOffset timeOfBan /// Player's session public void SendRoleBans(ICommonSession pSession); } + +/// +/// Base info to fill out in created ban records. +/// +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public abstract class CreateBanInfo +{ + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv4 = 32; + [Access(Other = AccessPermissions.Read)] + public const int DefaultMaskIpv6 = 64; + + internal readonly HashSet<(NetUserId UserId, string UserName)> Users = []; + internal readonly HashSet<(IPAddress Address, int Mask)> AddressRanges = []; + internal readonly HashSet HWIds = []; + internal readonly HashSet RoundIds = []; + internal TimeSpan? Duration; + internal NoteSeverity? Severity; + internal string Reason; + internal NetUserId? BanningAdmin; + + protected CreateBanInfo(string reason) + { + Reason = reason; + } + + /// + /// Add a user to be matched by the ban. + /// + /// + /// Bans can target multiple users at once. + /// + /// The ID of the user. + /// The name of the user (used for logging purposes). + /// The current object, for easy chaining. + public CreateBanInfo AddUser(NetUserId userId, string username) + { + Users.Add((userId, username)); + return this; + } + + /// + /// Add an IP address to be matched by the ban. + /// + /// + /// Bans can target multiple addresses at once. + /// + /// + /// The IP address to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddress(IPAddress? address) + { + if (address == null) + return this; + + return AddAddressRange( + address, + address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange) + { + return AddAddressRange(addressRange.Address, addressRange.Mask); + } + + /// + /// Add an IP address range to be matched by the ban. + /// + /// + /// Bans can target multiple address ranges at once. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddAddressRange(IPAddress address, int mask) + { + AddressRanges.Add((address, mask)); + return this; + } + + /// + /// Add a hardware IP (HWID) to be matched by the ban. + /// + /// + /// Bans can target multiple HWIDs at once. + /// + /// + /// The HWID to add. If null, nothing is done. + /// + /// The current object, for easy chaining. + public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId) + { + if (hwId != null) + HWIds.Add(hwId); + + return this; + } + + /// + /// Add a relevant round ID to this ban. + /// + /// + /// + /// If not specified, the current round ID is used for the ban. + /// Therefore, the first call to this function will replace the round ID, + /// and further calls will add additional round IDs. + /// + /// + /// Bans can target multiple round IDs at once. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo AddRoundId(int roundId) + { + RoundIds.Add(roundId); + return this; + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(int minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithMinutes((uint)minutes); + } + + /// + /// Set how long the ban will last, in minutes. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban, in minutes. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive number. + /// + public CreateBanInfo WithMinutes(uint minutes) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes); + return WithDuration(TimeSpan.FromMinutes(minutes)); + } + + /// + /// Set how long the ban will last. + /// + /// + /// If no duration is specified, the ban is permanent. + /// + /// The duration of the ban. + /// The current object, for easy chaining. + /// + /// Thrown if is not a positive amount of time. + /// + public CreateBanInfo WithDuration(TimeSpan duration) + { + if (duration <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero."); + + Duration = duration; + return this; + } + + /// + /// Set the severity of the ban. + /// + /// + /// If no severity is specified, the default is specified through server configuration. + /// + /// + /// The current object, for easy chaining. + public CreateBanInfo WithSeverity(NoteSeverity severity) + { + Severity = severity; + return this; + } + + /// + /// Set the reason for the ban. + /// + /// + /// This replaces the value given via the object constructor. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithReason(string reason) + { + Reason = reason; + return this; + } + + /// + /// Specify the admin responsible for placing the ban. + /// + /// The current object, for easy chaining. + public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin) + { + BanningAdmin = banningAdmin; + return this; + } +} + +/// +/// Stores info to create server ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateServerBanInfo : CreateBanInfo +{ + /// The reason for the server ban. + public CreateServerBanInfo(string reason) : base(reason) + { + } +} + +/// +/// Stores info to create role ban records. +/// +/// +[Access(typeof(BanManager), Other = AccessPermissions.Execute)] +public sealed class CreateRoleBanInfo : CreateBanInfo +{ + internal readonly HashSet> AntagPrototypes = []; + internal readonly HashSet> JobPrototypes = []; + + /// The reason for the role ban. + public CreateRoleBanInfo(string reason) : base(reason) + { + } + + /// + /// Add an antag role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddAntag(ProtoId protoId) + { + AntagPrototypes.Add(protoId); + return this; + } + + /// + /// Add a job role that will be unavailable for banned players. + /// + /// + /// + /// Bans can have multiple roles at once. + /// + /// + /// While not checked in this function, adding a ban with invalid role IDs will cause a + /// when actually creating the ban. + /// + /// + /// The current object, for easy chaining. + public CreateRoleBanInfo AddJob(ProtoId protoId) + { + JobPrototypes.Add(protoId); + return this; + } +} diff --git a/Content.Server/Administration/Notes/AdminNotesEui.cs b/Content.Server/Administration/Notes/AdminNotesEui.cs index d1297b251d7..5ecb9c774dc 100644 --- a/Content.Server/Administration/Notes/AdminNotesEui.cs +++ b/Content.Server/Administration/Notes/AdminNotesEui.cs @@ -22,7 +22,7 @@ public AdminNotesEui() IoCManager.InjectDependencies(this); } - private Guid NotedPlayer { get; set; } + private NetUserId NotedPlayer { get; set; } private string NotedPlayerName { get; set; } = string.Empty; private bool HasConnectedBefore { get; set; } private Dictionary<(int, NoteType), SharedAdminNote> Notes { get; set; } = new(); @@ -112,7 +112,7 @@ public override async void HandleMessage(EuiMessageBase msg) } } - public async Task ChangeNotedPlayer(Guid notedPlayer) + public async Task ChangeNotedPlayer(NetUserId notedPlayer) { NotedPlayer = notedPlayer; await LoadFromDb(); @@ -120,7 +120,7 @@ public async Task ChangeNotedPlayer(Guid notedPlayer) private void NoteModified(SharedAdminNote note) { - if (note.Player != NotedPlayer) + if (!note.Players.Contains(NotedPlayer)) return; Notes[(note.Id, note.NoteType)] = note; @@ -129,7 +129,7 @@ private void NoteModified(SharedAdminNote note) private void NoteDeleted(SharedAdminNote note) { - if (note.Player != NotedPlayer) + if (!note.Players.Contains(NotedPlayer)) return; Notes.Remove((note.Id, note.NoteType)); diff --git a/Content.Server/Administration/Notes/AdminNotesExtensions.cs b/Content.Server/Administration/Notes/AdminNotesExtensions.cs index 349c7ff3bdf..e2ec62ed618 100644 --- a/Content.Server/Administration/Notes/AdminNotesExtensions.cs +++ b/Content.Server/Administration/Notes/AdminNotesExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Linq; using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; @@ -11,7 +13,7 @@ public static SharedAdminNote ToShared(this IAdminRemarksRecord note) NoteSeverity? severity = null; var secret = false; NoteType type; - string[]? bannedRoles = null; + ImmutableArray? bannedRoles = null; string? unbannedByName = null; DateTime? unbannedTime = null; bool? seen = null; @@ -30,13 +32,13 @@ public static SharedAdminNote ToShared(this IAdminRemarksRecord note) type = NoteType.Message; seen = adminMessage.Seen; break; - case ServerBanNoteRecord ban: + case BanNoteRecord { Type: BanType.Server } ban: type = NoteType.ServerBan; severity = ban.Severity; unbannedTime = ban.UnbanTime; unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user"); break; - case ServerRoleBanNoteRecord roleBan: + case BanNoteRecord { Type: BanType.Role } roleBan: type = NoteType.RoleBan; severity = roleBan.Severity; bannedRoles = roleBan.Roles; @@ -48,14 +50,14 @@ public static SharedAdminNote ToShared(this IAdminRemarksRecord note) } // There may be bans without a user, but why would we ever be converting them to shared notes? - if (note.Player is null) - throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note"); + if (note.Players.Length == 0) + throw new ArgumentNullException(nameof(note), "Player user ID cannot be empty for a note"); return new SharedAdminNote( note.Id, - note.Player!.UserId, - note.Round?.Id, - note.Round?.Server.Name, + [..note.Players.Select(p => p.UserId)], + [..note.Rounds.Select(r => r.Id)], + note.Rounds.SingleOrDefault()?.Server.Name, // TODO: Show all server names? note.PlaytimeAtNote, type, note.Message, diff --git a/Content.Server/Administration/Notes/AdminNotesManager.cs b/Content.Server/Administration/Notes/AdminNotesManager.cs index 412b1911710..f10cd4e3f9c 100644 --- a/Content.Server/Administration/Notes/AdminNotesManager.cs +++ b/Content.Server/Administration/Notes/AdminNotesManager.cs @@ -52,7 +52,7 @@ public bool CanView(ICommonSession admin) return _admins.HasAdminFlag(admin, AdminFlags.ViewNotes); } - public async Task OpenEui(ICommonSession admin, Guid notedPlayer) + public async Task OpenEui(ICommonSession admin, NetUserId notedPlayer) { var ui = new AdminNotesEui(); _euis.OpenEui(ui, admin); @@ -144,8 +144,8 @@ public async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType var note = new SharedAdminNote( noteId, - (NetUserId) player, - roundId, + [(NetUserId) player], + roundId.HasValue ? [roundId.Value] : [], serverName, playtime, type, @@ -172,8 +172,7 @@ public async Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType NoteType.Note => (await _db.GetAdminNote(id))?.ToShared(), NoteType.Watchlist => (await _db.GetAdminWatchlist(id))?.ToShared(), NoteType.Message => (await _db.GetAdminMessage(id))?.ToShared(), - NoteType.ServerBan => (await _db.GetServerBanAsNoteAsync(id))?.ToShared(), - NoteType.RoleBan => (await _db.GetServerRoleBanAsNoteAsync(id))?.ToShared(), + NoteType.ServerBan or NoteType.RoleBan => (await _db.GetBanAsNoteAsync(id))?.ToShared(), _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type") }; } @@ -200,11 +199,8 @@ public async Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession de case NoteType.Message: await _db.DeleteAdminMessage(noteId, deletedBy.UserId, deletedAt); break; - case NoteType.ServerBan: - await _db.HideServerBanFromNotes(noteId, deletedBy.UserId, deletedAt); - break; - case NoteType.RoleBan: - await _db.HideServerRoleBanFromNotes(noteId, deletedBy.UserId, deletedAt); + case NoteType.ServerBan or NoteType.RoleBan: + await _db.HideBanFromNotes(noteId, deletedBy.UserId, deletedAt); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); @@ -280,15 +276,10 @@ public async Task ModifyAdminRemark(int noteId, NoteType type, ICommonSession ed case NoteType.Message: await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime); break; - case NoteType.ServerBan: + case NoteType.ServerBan or NoteType.RoleBan: if (severity is null) throw new ArgumentException("Severity cannot be null for a ban", nameof(severity)); - await _db.EditServerBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); - break; - case NoteType.RoleBan: - if (severity is null) - throw new ArgumentException("Severity cannot be null for a role ban", nameof(severity)); - await _db.EditServerRoleBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); + await _db.EditBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt); break; default: throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type"); diff --git a/Content.Server/Administration/Notes/IAdminNotesManager.cs b/Content.Server/Administration/Notes/IAdminNotesManager.cs index f54f8a21bda..4e992ba30b1 100644 --- a/Content.Server/Administration/Notes/IAdminNotesManager.cs +++ b/Content.Server/Administration/Notes/IAdminNotesManager.cs @@ -2,6 +2,7 @@ using Content.Server.Database; using Content.Shared.Administration.Notes; using Content.Shared.Database; +using Robust.Shared.Network; using Robust.Shared.Player; namespace Content.Server.Administration.Notes; @@ -16,7 +17,7 @@ public interface IAdminNotesManager bool CanDelete(ICommonSession admin); bool CanEdit(ICommonSession admin); bool CanView(ICommonSession admin); - Task OpenEui(ICommonSession admin, Guid notedPlayer); + Task OpenEui(ICommonSession admin, NetUserId notedPlayer); Task OpenUserNotesEui(ICommonSession player); Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime); Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy); diff --git a/Content.Server/Administration/PlayerPanelEui.cs b/Content.Server/Administration/PlayerPanelEui.cs index 31acd33bf12..13a5d42a4ed 100644 --- a/Content.Server/Administration/PlayerPanelEui.cs +++ b/Content.Server/Administration/PlayerPanelEui.cs @@ -2,10 +2,10 @@ using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Administration.Notes; -using Content.Server.Administration.Systems; using Content.Server.Database; using Content.Server.EUI; using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Content.Shared.Database; using Content.Shared.Eui; using Content.Shared.Follower; @@ -186,11 +186,8 @@ public async void SetPlayerState() { _whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId); // This won't get associated ip or hwid bans but they were not placed on this account anyways - _bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count; - // Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally - // The only way to distinguish whether a role ban is the same is to compare the ban time. - // This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now. - _roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count(); + _bans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null)).Count; + _roleBans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null, type: BanType.Role)).Count(); } else { diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index f1f09d4b503..34b41e0ec19 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -6,7 +6,9 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; +using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; @@ -14,6 +16,7 @@ using Content.Server.RoundEnd; using Content.Shared.Administration.Managers; using Content.Shared.CCVar; +using Content.Shared.Database; using Content.Shared.GameTicking.Components; using Content.Shared.Prototypes; using Robust.Server.ServerStatus; @@ -58,6 +61,9 @@ public sealed partial class ServerApi : IPostInjectInit [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IPlayerLocator _locator = default!; + [Dependency] private readonly IBanManager _bans = default!; + [Dependency] private readonly IServerDbManager _db = default!; private string _token = string.Empty; private ISawmill _sawmill = default!; @@ -76,6 +82,7 @@ void IPostInjectInit.PostInject() RegisterActorHandler(HttpMethod.Post, "/admin/actions/round/end", ActionRoundEnd); RegisterActorHandler(HttpMethod.Post, "/admin/actions/round/restartnow", ActionRoundRestartNow); RegisterActorHandler(HttpMethod.Post, "/admin/actions/kick", ActionKick); + RegisterActorHandler(HttpMethod.Post, "/admin/actions/ban", ActionBan); RegisterActorHandler(HttpMethod.Post, "/admin/actions/add_game_rule", ActionAddGameRule); RegisterActorHandler(HttpMethod.Post, "/admin/actions/end_game_rule", ActionEndGameRule); RegisterActorHandler(HttpMethod.Post, "/admin/actions/force_preset", ActionForcePreset); @@ -307,6 +314,75 @@ await RespondError(context, }); } + /// + /// Bans a player. + /// + private async Task ActionBan(IStatusHandlerContext context, Actor actor) + { + var body = await ReadJson(context); + if (body == null) + return; + + await RunOnMainThread(async () => + { + var located = await _locator.LookupIdByNameOrIdAsync(body.Guid.ToString()); + + if (located == null) + { + await RespondError( + context, + ErrorCode.PlayerNotFound, + HttpStatusCode.UnprocessableContent, + "Player not found"); + return; + } + + var bans = await _db.GetBansAsync(userId: located.UserId, + address: null, + hwId: null, + modernHWIds: null, + includeUnbanned: false); + if (bans.Count > 0) + { + await RespondError( + context, + ErrorCode.PlayerAlreadyBanned, + HttpStatusCode.Conflict, + "Player is already banned."); + return; + } + + var reason = body.Reason ?? "No reason supplied"; + var info = new CreateServerBanInfo(reason); + + info.AddHWId(located.LastHWId); + info.AddUser(located.UserId, located.Username); + info.WithSeverity(body.Severity); + if (body.Minutes != null && body.Minutes != 0) + { + info.WithMinutes(body.Minutes.Value); + } + + info.WithBanningAdmin(new NetUserId(actor.Guid)); + + // Add the ip if the user is currently connected. + if (_playerManager.TryGetSessionById(new NetUserId(body.Guid), out var player)) + { + info.AddAddress(player.Channel.RemoteEndPoint.Address); + } + else + { + // We fallback into using the located player ip. + info.AddAddress(located.LastAddress); + } + + _bans.CreateServerBan(info); + await RespondOk(context); + + _sawmill.Info($"Banned player {located.Username} ({located.UserId}) for {reason} lasting {body.Minutes ?? 0} minutes by {FormatLogActor(actor)}"); + }); + } + /// /// Kicks a player. /// @@ -616,6 +692,14 @@ private sealed class KickActionBody public string? Reason { get; init; } } + private sealed class BanActionBody + { + public required Guid Guid { get; init; } + public string? Reason { get; init; } + public NoteSeverity Severity { get; init; } + public uint? Minutes { get; init; } + } + private sealed class GameRuleActionBody { public required string GameRuleId { get; init; } @@ -657,6 +741,7 @@ private enum ErrorCode PlayerNotFound = 4, GameRuleNotFound = 5, BadRequest = 6, + PlayerAlreadyBanned = 7, } #endregion diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index b511bfc6450..6b7ecc47710 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -13,6 +13,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Content.Server.Imperial.Medieval.Lycantropy; +using Content.Shared.Roles.Components; namespace Content.Server.Administration.Systems; @@ -30,6 +31,8 @@ public sealed partial class AdminVerbSystem private static readonly EntProtoId DefaultThiefRule = "Thief"; private static readonly EntProtoId DefaultChangelingRule = "Changeling"; private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn"; + private static readonly EntProtoId DefaultWizardRule = "Wizard"; + private static readonly EntProtoId DefaultNinjaRule = "NinjaSpawn"; private static readonly ProtoId PirateGearId = "PirateGear"; // All antag verbs have names so invokeverb works. @@ -190,7 +193,38 @@ private void AddAntagVerbs(GetVerbsEvent args) Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")), }; - if (HasComp(args.Target)) // only humanoids can be cloned + var wizardName = Loc.GetString("admin-verb-text-make-wizard"); + Verb wizard = new() + { + Text = wizardName, + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Wizard"), + Act = () => + { + // Wizard has no rule components as of writing, but I gotta put something here to satisfy the machine so just make it wizard mind rule :) + _antag.ForceMakeAntag(targetPlayer, DefaultWizardRule); + }, + Impact = LogImpact.High, + Message = string.Join(": ", wizardName, Loc.GetString("admin-verb-make-wizard")), + }; + args.Verbs.Add(wizard); + + var ninjaName = Loc.GetString("admin-verb-text-make-space-ninja"); + Verb ninja = new() + { + Text = ninjaName, + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"), + Act = () => + { + _antag.ForceMakeAntag(targetPlayer, DefaultNinjaRule); + }, + Impact = LogImpact.High, + Message = string.Join(": ", ninjaName, Loc.GetString("admin-verb-make-space-ninja")), + }; + args.Verbs.Add(ninja); + + if (HasComp(args.Target)) // only humanoids can be cloned args.Verbs.Add(paradox); // Imperial Medieval start diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 79c616322ae..63b4b5646b1 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -1,13 +1,13 @@ +using System.Linq; +using System.Numerics; using System.Threading; -using Content.Server.Administration.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Electrocution; using Content.Server.Explosion.EntitySystems; using Content.Server.GhostKick; -using Content.Server.Medical; using Content.Server.Nutrition.EntitySystems; +using Content.Server.Physics.Components; using Content.Server.Pointing.Components; using Content.Server.Polymorph.Systems; using Content.Server.Popups; @@ -19,19 +19,22 @@ using Content.Shared.Actions; using Content.Shared.Administration; using Content.Shared.Administration.Components; +using Content.Shared.Administration.Systems; using Content.Shared.Atmos.Components; +using Content.Shared.Body; using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Content.Shared.Clumsy; using Content.Shared.Clothing.Components; +using Content.Shared.Clumsy; using Content.Shared.Cluwne; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Electrocution; +using Content.Shared.Gibbing; using Content.Shared.Gravity; using Content.Shared.Interaction.Components; using Content.Shared.Inventory; +using Content.Shared.Medical; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -54,6 +57,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Spawners; using Robust.Shared.Utility; using Timer = Robust.Shared.Timing.Timer; @@ -63,7 +67,7 @@ public sealed partial class AdminVerbSystem { private readonly ProtoId LizardSmite = "AdminLizardSmite"; private readonly ProtoId VulpkaninSmite = "AdminVulpSmite"; - + [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; @@ -90,6 +94,8 @@ public sealed partial class AdminVerbSystem [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SuperBonkSystem _superBonkSystem = default!; [Dependency] private readonly SlipperySystem _slipperySystem = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; private readonly EntProtoId _actionViewLawsProtoId = "ActionViewLaws"; private readonly ProtoId _crewsimovLawset = "Crewsimov"; @@ -126,7 +132,7 @@ private void AddSmiteVerbs(GetVerbsEvent args) 4, 1, 2, args.Target, maxTileBreak: 0), // it gibs, damage doesn't need to be high. CancellationToken.None); - _bodySystem.GibBody(args.Target); + _gibbing.Gib(args.Target); }, Impact = LogImpact.Extreme, Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console. @@ -143,7 +149,6 @@ private void AddSmiteVerbs(GetVerbsEvent args) { _sharedGodmodeSystem.EnableGodmode(args.Target); // So they don't suffocate. EnsureComp(args.Target); - RemComp(args.Target); // So they can be dragged around. var xform = Transform(args.Target); _popupSystem.PopupEntity(Loc.GetString("admin-smite-chess-self"), args.Target, args.Target, PopupType.LargeCaution); @@ -226,6 +231,7 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"), Act = () => { + var totalDamage = _damageable.GetTotalDamage((args.Target, damageable)); int damageToDeal; if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Critical, out var criticalThreshold)) { @@ -233,11 +239,11 @@ private void AddSmiteVerbs(GetVerbsEvent args) if (!_mobThresholdSystem.TryGetThresholdForState(args.Target, MobState.Dead, out var deadThreshold)) return;// whelp. - damageToDeal = deadThreshold.Value.Int() - (int)damageable.TotalDamage; + damageToDeal = deadThreshold.Value.Int() - (int)totalDamage; } else { - damageToDeal = criticalThreshold.Value.Int() - (int)damageable.TotalDamage; + damageToDeal = criticalThreshold.Value.Int() - (int)totalDamage; } if (damageToDeal <= 0) @@ -273,7 +279,7 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"), Act = () => { - _creamPieSystem.SetCreamPied(args.Target, creamPied, true); + _creamPieSystem.SetCreamPied((args.Target, creamPied), true); }, Impact = LogImpact.Extreme, Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description")) @@ -304,7 +310,6 @@ private void AddSmiteVerbs(GetVerbsEvent args) args.Verbs.Add(bloodRemoval); } - // bobby... if (TryComp(args.Target, out var body)) { var vomitOrgansName = Loc.GetString("admin-smite-vomit-organs-name").ToLowerInvariant(); @@ -316,14 +321,14 @@ private void AddSmiteVerbs(GetVerbsEvent args) Act = () => { _vomitSystem.Vomit(args.Target, -1000, -1000); // You feel hollow! - var organs = _bodySystem.GetBodyOrganEntityComps((args.Target, body)); + _bodySystem.TryGetOrgansWithComponent((args.Target, body), out var organs); var baseXform = Transform(args.Target); foreach (var organ in organs) { if (HasComp(organ.Owner) || HasComp(organ.Owner)) continue; - _transformSystem.PlaceNextTo((organ.Owner, organ.Comp1), (args.Target, baseXform)); + _transformSystem.PlaceNextTo((organ.Owner, organ.Comp), (args.Target, baseXform)); } _popupSystem.PopupEntity(Loc.GetString("admin-smite-vomit-organs-self"), args.Target, @@ -345,9 +350,11 @@ private void AddSmiteVerbs(GetVerbsEvent args) Act = () => { var baseXform = Transform(args.Target); - foreach (var part in _bodySystem.GetBodyChildrenOfType(args.Target, BodyPartType.Hand)) + var parts = new HashSet>() { "HandRight", "HandLeft" }; + _bodySystem.TryGetOrgansWithComponent((args.Target, body), out var organs); + foreach (var organ in organs.Where(it => it.Comp.Category is { } category && parts.Contains(category))) { - _transformSystem.AttachToGridOrMap(part.Id); + _transformSystem.AttachToGridOrMap(organ); } _popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-hands-self"), args.Target, args.Target, PopupType.LargeCaution); @@ -368,9 +375,11 @@ private void AddSmiteVerbs(GetVerbsEvent args) Act = () => { var baseXform = Transform(args.Target); - foreach (var part in _bodySystem.GetBodyChildrenOfType(args.Target, BodyPartType.Hand, body)) + var parts = new HashSet>() { "HandRight", "HandLeft" }; + _bodySystem.TryGetOrgansWithComponent((args.Target, body), out var organs); + foreach (var organ in organs.Where(it => it.Comp.Category is { } category && parts.Contains(category))) { - _transformSystem.AttachToGridOrMap(part.Id); + _transformSystem.AttachToGridOrMap(organ); break; } _popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-hands-self"), args.Target, @@ -391,7 +400,8 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"), Act = () => { - foreach (var entity in _bodySystem.GetBodyOrganEntityComps((args.Target, body))) + _bodySystem.TryGetOrgansWithComponent((args.Target, body), out var organs); + foreach (var entity in organs) { QueueDel(entity.Owner); } @@ -412,7 +422,8 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"), Act = () => { - foreach (var entity in _bodySystem.GetBodyOrganEntityComps((args.Target, body))) + _bodySystem.TryGetOrgansWithComponent((args.Target, body), out var organs); + foreach (var entity in organs) { QueueDel(entity.Owner); } @@ -572,13 +583,32 @@ private void AddSmiteVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon"), Act = () => { - EnsureComp(args.Target); + EnsureComp(args.Target, out var comp); + comp.HideFromOwner = false; // We set it to false anyway, in case the hidden smite was used beforehand. + Dirty(args.Target, comp); }, Impact = LogImpact.Extreme, Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description")) }; args.Verbs.Add(killSign); + var hiddenKillSignName = Loc.GetString("admin-smite-kill-sign-hidden-name").ToLowerInvariant(); + Verb hiddenKillSign = new() + { + Text = hiddenKillSignName, + Category = VerbCategory.Smite, + Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon-hidden"), + Act = () => + { + EnsureComp(args.Target, out var comp); + comp.HideFromOwner = true; + Dirty(args.Target, comp); + }, + Impact = LogImpact.Extreme, + Message = string.Join(": ", hiddenKillSignName, Loc.GetString("admin-smite-kill-sign-hidden-description")) + }; + args.Verbs.Add(hiddenKillSign); + var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant(); Verb cluwne = new() { @@ -1006,5 +1036,63 @@ private void AddSmiteVerbs(GetVerbsEvent args) Message = string.Join(": ", siliconName, Loc.GetString("admin-smite-silicon-laws-bound-description")) }; args.Verbs.Add(silicon); + + var homingRodName = Loc.GetString("admin-smite-homing-rod-name").ToLowerInvariant(); + Verb homingRod = new() + { + Text = homingRodName, + Category = VerbCategory.Smite, + Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Security/target.rsi"), "target_s"), + Act = () => + { + var speed = 25f; // It don't miss brother. + var distance = 350f; + HomingLaunchSequence(args.Target, "ImmovableRodKeepTiles", distance, speed); // todo: swap the proto for an EntityTable GetSpawns once rod rule rework + }, + Impact = LogImpact.Extreme, + Message = string.Join(": ", homingRodName, Loc.GetString("admin-smite-homing-rod-description")) + }; + args.Verbs.Add(homingRod); + + var homingRodSlowName = Loc.GetString("admin-smite-homing-rod-slow-name").ToLowerInvariant(); + Verb homingRodSlow = new() + { + Text = homingRodSlowName, + Category = VerbCategory.Smite, + Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Security/target.rsi"), "target_c"), + Act = () => + { + var speed = 5f; // slightly faster than default sprint speed 4.5 + if (TryComp(args.Target, out var movement)) + speed = movement.CurrentSprintSpeed + 0.001f;// run + var distance = 200f; // its kinda slow so were just gonna cheat a bit. + HomingLaunchSequence(args.Target, "ImmovableRodKeepTiles", distance, speed); + }, + Impact = LogImpact.Extreme, + Message = string.Join(": ", homingRodSlowName, Loc.GetString("admin-smite-homing-rod-slow-description")) + }; + args.Verbs.Add(homingRodSlow); + } + + public void HomingLaunchSequence(EntityUid target, EntProtoId proto, float distance, float speed) + { + // ToDo: Reuse some spawning code from whereever the rod rule ends up. + // I would do it now but theres a massive rod rewrite, and I don't wanna poke it for this. + // find reasonable spawn location (use gamerule and find rod?) but respect map not on grid etc etc + + var offset = new Random(target.Id).NextAngle().RotateVec(new Vector2(distance, 0)); + var spawnCoords = _transformSystem.GetMapCoordinates(target).Offset(offset); + var rod = Spawn(proto, spawnCoords); + // Here we abuse the ChasingWalkComp by making it skip targetting logic and dialling its frequency up + EnsureComp(rod, out var chasingComp); + chasingComp.NextChangeVectorTime = TimeSpan.MaxValue; // we just want it to never change + chasingComp.ChasingEntity = target; + chasingComp.ImpulseInterval = .1f; // skrrt skrrrrrrt skrrrt + chasingComp.RotateWithImpulse = true; + chasingComp.MaxSpeed = speed; + chasingComp.Speed = speed; // tell me lies, tell me sweet little lies. + + if (TryComp(rod, out var despawn)) + despawn.Lifetime = offset.Length() / speed * 3; // exists thrice as long as it takes to get to you. } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 0e99f7b926e..84df4b9a08a 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -1,12 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using Content.Server.Administration.Components; using Content.Server.Cargo.Components; using Content.Server.Doors.Systems; using Content.Server.Hands.Systems; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Server.Stack; using Content.Server.Station.Systems; using Content.Server.Weapons.Ranged.Systems; @@ -14,10 +11,11 @@ using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Administration; +using Content.Shared.Administration.Components; +using Content.Shared.Administration.Systems; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Construction.Components; -using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Database; using Content.Shared.Doors.Components; @@ -25,6 +23,8 @@ using Content.Shared.Imperial.Medieval.Skills; using Content.Shared.Inventory; using Content.Shared.PDA; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Stacks; using Content.Shared.Station.Components; using Content.Shared.Verbs; @@ -51,7 +51,7 @@ public sealed partial class AdminVerbSystem [Dependency] private readonly AdminTestArenaSystem _adminTestArenaSystem = default!; [Dependency] private readonly StationJobsSystem _stationJobsSystem = default!; [Dependency] private readonly JointSystem _jointSystem = default!; - [Dependency] private readonly BatterySystem _batterySystem = default!; + [Dependency] private readonly SharedBatterySystem _batterySystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly GunSystem _gun = default!; @@ -69,7 +69,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb bolt = new() { - Text = bolts.BoltsDown ? "Unbolt" : "Bolt", + Text = Loc.GetString(bolts.BoltsDown ? "admin-verbs-unbolt" : "admin-verbs-bolt"), Category = VerbCategory.Tricks, Icon = bolts.BoltsDown ? new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/unbolt.png")) @@ -91,7 +91,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb emergencyAccess = new() { - Text = airlockComp.EmergencyAccess ? "Emergency Access Off" : "Emergency Access On", + Text = Loc.GetString(airlockComp.EmergencyAccess ? "admin-verbs-emergency-access-off" : "admin-verbs-emergency-access-on"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/emergency_access.png")), Act = () => @@ -111,7 +111,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb rejuvenate = new() { - Text = "Rejuvenate", + Text = Loc.GetString("admin-verbs-rejuvenate"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/rejuvenate.png")), Act = () => @@ -129,7 +129,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb makeIndestructible = new() { - Text = "Make Indestructible", + Text = Loc.GetString("admin-verbs-make-indestructible"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/plus.svg.192dpi.png")), Act = () => @@ -146,7 +146,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb makeVulnerable = new() { - Text = "Make Vulnerable", + Text = Loc.GetString("admin-verbs-make-vulnerable"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/plus.svg.192dpi.png")), Act = () => @@ -164,12 +164,12 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb refillBattery = new() { - Text = "Refill Battery", + Text = Loc.GetString("admin-verbs-refill-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")), Act = () => { - _batterySystem.SetCharge(args.Target, battery.MaxCharge, battery); + _batterySystem.SetCharge((args.Target, battery), battery.MaxCharge); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-refill-battery-description"), @@ -179,12 +179,12 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb drainBattery = new() { - Text = "Drain Battery", + Text = Loc.GetString("admin-verbs-drain-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")), Act = () => { - _batterySystem.SetCharge(args.Target, 0, battery); + _batterySystem.SetCharge((args.Target, battery), 0); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-drain-battery-description"), @@ -194,15 +194,16 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb infiniteBattery = new() { - Text = "Infinite Battery", + Text = Loc.GetString("admin-verbs-infinite-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")), Act = () => { var recharger = EnsureComp(args.Target); - recharger.AutoRecharge = true; recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. - recharger.AutoRechargePause = false; // No delay. + recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay. + Dirty(args.Target, recharger); + _batterySystem.RefreshChargeRate((args.Target, battery)); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-infinite-battery-object-description"), @@ -215,7 +216,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb blockUnanchor = new() { - Text = "Block Unanchoring", + Text = Loc.GetString("admin-verbs-block-unanchoring"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/anchor.svg.192dpi.png")), Act = () => @@ -233,7 +234,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb refillInternalsO2 = new() { - Text = "Refill Internals Oxygen", + Text = Loc.GetString("admin-verbs-refill-internals-oxygen"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/oxygen.rsi"), "icon"), Act = () => @@ -248,7 +249,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb refillInternalsN2 = new() { - Text = "Refill Internals Nitrogen", + Text = Loc.GetString("admin-verbs-refill-internals-nitrogen"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/red.rsi"), "icon"), Act = () => @@ -263,7 +264,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb refillInternalsPlasma = new() { - Text = "Refill Internals Plasma", + Text = Loc.GetString("admin-verbs-refill-internals-plasma"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/plasma.rsi"), "icon"), Act = () => @@ -281,7 +282,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb refillInternalsO2 = new() { - Text = "Refill Internals Oxygen", + Text = Loc.GetString("admin-verbs-refill-internals-oxygen"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/oxygen.rsi"), "icon"), Act = () => RefillEquippedTanks(args.User, Gas.Oxygen), @@ -293,7 +294,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb refillInternalsN2 = new() { - Text = "Refill Internals Nitrogen", + Text = Loc.GetString("admin-verbs-refill-internals-nitrogen"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/red.rsi"), "icon"), Act = () => RefillEquippedTanks(args.User, Gas.Nitrogen), @@ -305,7 +306,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb refillInternalsPlasma = new() { - Text = "Refill Internals Plasma", + Text = Loc.GetString("admin-verbs-refill-internals-plasma"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Tanks/plasma.rsi"), "icon"), Act = () => RefillEquippedTanks(args.User, Gas.Plasma), @@ -318,7 +319,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb sendToTestArena = new() { - Text = "Send to test arena", + Text = Loc.GetString("admin-verbs-send-to-test-arena"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")), @@ -339,7 +340,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb grantAllAccess = new() { - Text = "Grant All Access", + Text = Loc.GetString("admin-verbs-grant-all-access"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/id_cards.rsi"), "centcom"), Act = () => @@ -354,7 +355,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb revokeAllAccess = new() { - Text = "Revoke All Access", + Text = Loc.GetString("admin-verbs-revoke-all-access"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/id_cards.rsi"), "default"), Act = () => @@ -372,7 +373,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb grantAllAccess = new() { - Text = "Grant All Access", + Text = Loc.GetString("admin-verbs-grant-all-access"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/id_cards.rsi"), "centcom"), Act = () => @@ -387,7 +388,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb revokeAllAccess = new() { - Text = "Revoke All Access", + Text = Loc.GetString("admin-verbs-revoke-all-access"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/id_cards.rsi"), "default"), Act = () => @@ -405,15 +406,15 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb adjustStack = new() { - Text = "Adjust Stack", + Text = Loc.GetString("admin-verbs-adjust-stack"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/adjust-stack.png")), Act = () => { // Unbounded intentionally. - _quickDialog.OpenDialog(player, "Adjust stack", $"Amount (max {_stackSystem.GetMaxCount(stack)})", (int newAmount) => + _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-adjust-stack"), Loc.GetString("admin-verbs-dialog-adjust-stack-amount", ("max", _stackSystem.GetMaxCount(stack))), (int newAmount) => { - _stackSystem.SetCount(args.Target, newAmount, stack); + _stackSystem.SetCount((args.Target, stack), newAmount); }); }, Impact = LogImpact.Medium, @@ -424,12 +425,12 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb fillStack = new() { - Text = "Fill Stack", + Text = Loc.GetString("admin-verbs-fill-stack"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")), Act = () => { - _stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack); + _stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack)); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-fill-stack-description"), @@ -440,12 +441,12 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb rename = new() { - Text = "Rename", + Text = Loc.GetString("admin-verbs-rename"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/rename.png")), Act = () => { - _quickDialog.OpenDialog(player, "Rename", "Name", (string newName) => + _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-dialog-rename-title"), Loc.GetString("admin-verbs-dialog-rename-name"), (string newName) => { _metaSystem.SetEntityName(args.Target, newName); }); @@ -458,12 +459,12 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb redescribe = new() { - Text = "Redescribe", + Text = Loc.GetString("admin-verbs-redescribe"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/redescribe.png")), Act = () => { - _quickDialog.OpenDialog(player, "Redescribe", "Description", (LongString newDescription) => + _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-dialog-redescribe-title"), Loc.GetString("admin-verbs-dialog-redescribe-description"), (LongString newDescription) => { _metaSystem.SetEntityDescription(args.Target, newDescription.String); }); @@ -476,12 +477,12 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb renameAndRedescribe = new() { - Text = "Redescribe", + Text = Loc.GetString("admin-verbs-rename-and-redescribe"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/rename_and_redescribe.png")), Act = () => { - _quickDialog.OpenDialog(player, "Rename & Redescribe", "Name", "Description", + _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-dialog-rename-and-redescribe-title"), Loc.GetString("admin-verbs-dialog-rename-name"), Loc.GetString("admin-verbs-dialog-redescribe-description"), (string newName, LongString newDescription) => { var meta = MetaData(args.Target); @@ -501,7 +502,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb barJobSlots = new() { - Text = "Bar job slots", + Text = Loc.GetString("admin-verbs-bar-job-slots"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/bar_jobslots.png")), Act = () => @@ -520,7 +521,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb locateCargoShuttle = new() { - Text = "Locate Cargo Shuttle", + Text = Loc.GetString("admin-verbs-locate-cargo-shuttle"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Soft/cargosoft.rsi"), "icon"), Act = () => @@ -543,7 +544,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb refillBattery = new() { - Text = "Refill Battery", + Text = Loc.GetString("admin-verbs-refill-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")), Act = () => @@ -553,7 +554,7 @@ private void AddTricksVerbs(GetVerbsEvent args) if (!HasComp(ent)) continue; var battery = EnsureComp(ent); - _batterySystem.SetCharge(ent, battery.MaxCharge, battery); + _batterySystem.SetCharge((ent, battery), battery.MaxCharge); } }, Impact = LogImpact.Extreme, @@ -564,7 +565,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb drainBattery = new() { - Text = "Drain Battery", + Text = Loc.GetString("admin-verbs-drain-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")), Act = () => @@ -574,7 +575,7 @@ private void AddTricksVerbs(GetVerbsEvent args) if (!HasComp(ent)) continue; var battery = EnsureComp(ent); - _batterySystem.SetCharge(ent, 0, battery); + _batterySystem.SetCharge((ent, battery), 0); } }, Impact = LogImpact.Extreme, @@ -585,7 +586,7 @@ private void AddTricksVerbs(GetVerbsEvent args) Verb infiniteBattery = new() { - Text = "Infinite Battery", + Text = Loc.GetString("admin-verbs-infinite-battery"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")), Act = () => @@ -599,9 +600,8 @@ private void AddTricksVerbs(GetVerbsEvent args) var recharger = EnsureComp(ent); var battery = EnsureComp(ent); - recharger.AutoRecharge = true; recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill. - recharger.AutoRechargePause = false; // No delay. + recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay. } }, Impact = LogImpact.Extreme, @@ -615,7 +615,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb haltMovement = new() { - Text = "Halt Movement", + Text = Loc.GetString("admin-verbs-halt-movement"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/halt.png")), Act = () => @@ -638,7 +638,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb unpauseMap = new() { - Text = "Unpause Map", + Text = Loc.GetString("admin-verbs-unpause-map"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/play.png")), Act = () => @@ -655,7 +655,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb pauseMap = new() { - Text = "Pause Map", + Text = Loc.GetString("admin-verbs-pause-map"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/pause.png")), Act = () => @@ -675,7 +675,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb snapJoints = new() { - Text = "Snap Joints", + Text = Loc.GetString("admin-verbs-snap-joints"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/snap_joints.png")), Act = () => @@ -693,7 +693,7 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb minigunFire = new() { - Text = "Make Minigun", + Text = Loc.GetString("admin-verbs-make-minigun"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Guns/HMGs/minigun.rsi"), "icon"), Act = () => @@ -712,18 +712,18 @@ private void AddTricksVerbs(GetVerbsEvent args) { Verb setCapacity = new() { - Text = "Set Bullet Amount", + Text = Loc.GetString("admin-verbs-set-bullet-amount"), Category = VerbCategory.Tricks, Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Fun/caps.rsi"), "mag-6"), Act = () => { - _quickDialog.OpenDialog(player, "Set Bullet Amount", $"Amount (standard {ballisticAmmo.Capacity}):", (string amount) => + _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-dialog-set-bullet-amount-title"), Loc.GetString("admin-verbs-dialog-set-bullet-amount-amount", ("cap", ballisticAmmo.Capacity)), (string amount) => { if (!int.TryParse(amount, out var result)) return; _gun.SetBallisticUnspawned((args.Target, ballisticAmmo), result); - _gun.UpdateBallisticAppearance(args.Target, ballisticAmmo); + _gun.UpdateBallisticAppearance((args.Target, ballisticAmmo)); }); }, Impact = LogImpact.Medium, diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 61e3013bd98..4a2ebbb7c60 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Silicons.Laws; using Content.Server.Station.Systems; using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Configurable; @@ -444,7 +445,7 @@ private void AddDebugVerbs(GetVerbsEvent args) } // Control mob verb - if (_toolshed.ActivePermissionController?.CheckInvokable(new CommandSpec(_toolshed.DefaultEnvironment.GetCommand("mind"), "control"), player, out _) ?? false && + if ((_toolshed.ActivePermissionController?.CheckInvokable(new CommandSpec(_toolshed.DefaultEnvironment.GetCommand("mind"), "control"), player, out _) ?? false) && args.User != args.Target) { Verb verb = new() diff --git a/Content.Server/Administration/Systems/BufferingSystem.cs b/Content.Server/Administration/Systems/BufferingSystem.cs index f3df34e7d2e..71e5c4d5c6f 100644 --- a/Content.Server/Administration/Systems/BufferingSystem.cs +++ b/Content.Server/Administration/Systems/BufferingSystem.cs @@ -1,12 +1,13 @@ using System.Numerics; -using Content.Server.Administration.Components; using Content.Shared.Administration; +using Content.Shared.Administration.Components; +using Content.Shared.Administration.Systems; using Robust.Shared.Map; using Robust.Shared.Random; namespace Content.Server.Administration.Systems; -public sealed class BufferingSystem : EntitySystem +public sealed class BufferingSystem : SharedBufferingSystem { [Dependency] private readonly IRobustRandom _random = default!; diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 2df9aa9fcc9..172e27ee808 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -172,7 +172,7 @@ private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs } // Check if the user has been banned - var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null); + var ban = await _dbManager.GetBanAsync(null, e.Session.UserId, null, null); if (ban != null) { var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason)); @@ -582,7 +582,7 @@ private WebhookPayload GeneratePayload(string messages, string username, string? { GameRunLevel.PreRoundLobby => _gameTicker.RoundId == 0 ? "pre-round lobby after server restart" // first round after server restart has ID == 0 - : $"pre-round lobby for round {_gameTicker.RoundId + 1}", + : $"pre-round lobby for round {_gameTicker.RoundId}", GameRunLevel.InRound => $"round {_gameTicker.RoundId}", GameRunLevel.PostRound => $"post-round {_gameTicker.RoundId}", _ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel), diff --git a/Content.Server/Administration/Systems/RejuvenateSystem.cs b/Content.Server/Administration/Systems/RejuvenateSystem.cs deleted file mode 100644 index d25db20bd93..00000000000 --- a/Content.Server/Administration/Systems/RejuvenateSystem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Content.Shared.Rejuvenate; - -namespace Content.Server.Administration.Systems; - -public sealed class RejuvenateSystem : EntitySystem -{ - public void PerformRejuvenate(EntityUid target) - { - RaiseLocalEvent(target, new RejuvenateEvent()); - } -} diff --git a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs index 3925badc589..b503e9cb963 100644 --- a/Content.Server/Administration/Toolshed/RejuvenateCommand.cs +++ b/Content.Server/Administration/Toolshed/RejuvenateCommand.cs @@ -1,5 +1,5 @@ -using Content.Server.Administration.Systems; -using Content.Shared.Administration; +using Content.Shared.Administration; +using Content.Shared.Administration.Systems; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Errors; diff --git a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs index 94b20c7b77e..812a016b379 100644 --- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs +++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Shared.Advertise.Components; using Content.Shared.Advertise.Systems; +using Content.Shared.Chat; using Content.Shared.UserInterface; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs index 35bb6782aa0..a729cbf9d95 100644 --- a/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs +++ b/Content.Server/Ame/EntitySystems/AmeControllerSystem.cs @@ -152,7 +152,7 @@ private AmeControllerBoundUserInterfaceState GetUiState(EntityUid uid, AmeContro // how much power can be produced at the current settings, in kW // we don't use max. here since this is what is set in the Controller, not what the AME is actually producing float targetedPowerSupply = 0; - if (TryGetAMENodeGroup(uid, out var group)) + if (TryGetAMENodeGroup(uid, out var group) && group.CoreCount > 0) { coreCount = group.CoreCount; targetedPowerSupply = group.CalculatePower(controller.InjectionAmount, group.CoreCount) / 1000; diff --git a/Content.Server/Anomaly/AnomalyScannerSystem.cs b/Content.Server/Anomaly/AnomalyScannerSystem.cs index ba657cf0567..b8bc3b60366 100644 --- a/Content.Server/Anomaly/AnomalyScannerSystem.cs +++ b/Content.Server/Anomaly/AnomalyScannerSystem.cs @@ -41,12 +41,12 @@ public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, An Appearance.SetData(scanner, AnomalyScannerVisuals.HasAnomaly, true, appearanceComp); - var stability = _secretData.IsSecret(anomaly, AnomalySecretData.Stability, secretDataComp) + var stability = _secretData.IsSecret(anomaly, AnomalySecretData.Stability, secretDataComp) && !scannerComp.IgnoreSecret ? AnomalyStabilityVisuals.Stable : _anomaly.GetStabilityVisualOrStable((anomaly, anomalyComp)); Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp); - var severity = _secretData.IsSecret(anomaly, AnomalySecretData.Severity, secretDataComp) + var severity = _secretData.IsSecret(anomaly, AnomalySecretData.Severity, secretDataComp) && !scannerComp.IgnoreSecret ? 0 : anomalyComp.Severity; Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp); @@ -109,13 +109,14 @@ private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) { - var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity) ? 0 : args.Severity; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var component)) { if (component.ScannedAnomaly != args.Anomaly) continue; + var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity) && !component.IgnoreSecret ? 0 : args.Severity; + UpdateScannerUi(uid, component); Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity); } @@ -123,15 +124,16 @@ private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent arg private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) { - var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability) - ? AnomalyStabilityVisuals.Stable - : _anomaly.GetStabilityVisualOrStable(args.Anomaly); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var component)) { if (component.ScannedAnomaly != args.Anomaly) continue; + var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability) && !component.IgnoreSecret + ? AnomalyStabilityVisuals.Stable + : _anomaly.GetStabilityVisualOrStable(args.Anomaly); + UpdateScannerUi(uid, component); Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability); } @@ -154,12 +156,12 @@ private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent arg TryComp(uid, out var appearanceComp); TryComp(args.Anomaly, out var secretDataComp); - var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity, secretDataComp) + var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity, secretDataComp) && !component.IgnoreSecret ? 0 : anomalyComp.Severity; Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp); - var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability, secretDataComp) + var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability, secretDataComp) && !component.IgnoreSecret ? AnomalyStabilityVisuals.Stable : _anomaly.GetStabilityVisualOrStable((args.Anomaly, anomalyComp)); Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp); diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs index 46ad9278f8a..09af0419bbd 100644 --- a/Content.Server/Anomaly/AnomalySystem.Generator.cs +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -100,7 +100,7 @@ public void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn) // no air-blocked areas. if (_atmosphere.IsTileSpace(grid, xform.MapUid, tile) || - _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp)) + _atmosphere.IsTileAirBlockedCached(grid, tile)) { continue; } diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs index 69f18e5eeb0..4955b4e3369 100644 --- a/Content.Server/Anomaly/AnomalySystem.cs +++ b/Content.Server/Anomaly/AnomalySystem.cs @@ -126,6 +126,9 @@ private void OnStartCollide(Entity anomaly, ref StartCollideEv if (_random.Prob(anomaly.Comp.Continuity)) SetBehavior(anomaly, GetRandomBehavior()); } + + var ev = new AnomalyAffectedByParticleEvent(anomaly, args.OtherEntity); + RaiseLocalEvent(anomaly, ref ev); } /// @@ -233,14 +236,19 @@ public FormattedMessage GetScannerMessage(AnomalyScannerComponent component) TryComp(anomaly, out var secret); //Severity - if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P")))); + { + var text = Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } msg.PushNewline(); //Stability - if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-stability-unknown")); else { @@ -251,15 +259,24 @@ public FormattedMessage GetScannerMessage(AnomalyScannerComponent component) stateLoc = Loc.GetString("anomaly-scanner-stability-high"); else stateLoc = Loc.GetString("anomaly-scanner-stability-medium"); + + if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability)) + stateLoc += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(stateLoc); } msg.PushNewline(); //Point output - if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp)))); + { + var text = Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } msg.PushNewline(); msg.PushNewline(); @@ -268,40 +285,63 @@ public FormattedMessage GetScannerMessage(AnomalyScannerComponent component) msg.PushNewline(); //Danger - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType)))); + { + var text = Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } msg.PushNewline(); //Unstable - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType)))); + { + var text = Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } msg.PushNewline(); //Containment - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType)))); + { + var text = Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } msg.PushNewline(); //Transformation - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation-unknown")); else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType)))); + { + var text = Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType))); + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation)) + text += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(text); + } //Behavior msg.PushNewline(); msg.PushNewline(); - msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-title")); + var behaviorTitle = Loc.GetString("anomaly-behavior-title"); + if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior) && component.IgnoreSecret) + behaviorTitle += " " + Loc.GetString("anomaly-secret-admin"); + msg.AddMarkupOrThrow(behaviorTitle); msg.PushNewline(); - if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior)) + if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior) && !component.IgnoreSecret) msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown")); else { diff --git a/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs index 02cd0aafc62..e053377d579 100644 --- a/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs @@ -1,9 +1,9 @@ using Content.Server.Physics.Components; +using Content.Server.Radiation.Systems; using Content.Server.Singularity.Components; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Effects; using Content.Shared.Anomaly.Effects.Components; -using Content.Shared.Radiation.Components; namespace Content.Server.Anomaly.Effects; @@ -12,6 +12,8 @@ namespace Content.Server.Anomaly.Effects; /// public sealed class GravityAnomalySystem : SharedGravityAnomalySystem { + [Dependency] private readonly RadiationSystem _radiation = default!; + /// public override void Initialize() { @@ -22,8 +24,7 @@ public override void Initialize() private void OnSeverityChanged(Entity anomaly, ref AnomalySeverityChangedEvent args) { - if (TryComp(anomaly, out var radSource)) - radSource.Intensity = anomaly.Comp.MaxRadiationIntensity * args.Severity; + _radiation.SetIntensity(anomaly.Owner, anomaly.Comp.MaxRadiationIntensity * args.Severity); if (TryComp(anomaly, out var gravityWell)) { diff --git a/Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs b/Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs index d6039fac015..3cfb9c70253 100644 --- a/Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/InnerBodyAnomalySystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Body.Components; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.Gibbing; using Content.Shared.Mobs; using Content.Shared.Popups; using Content.Shared.Whitelist; @@ -25,7 +26,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly AnomalySystem _anomaly = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; @@ -131,10 +132,7 @@ private void OnAnomalyPulse(Entity ent, ref AnomalyPu private void OnAnomalySupercritical(Entity ent, ref AnomalySupercriticalEvent args) { - if (!TryComp(ent, out var body)) - return; - - _body.GibBody(ent, true, body, splatModifier: 5f); + _gibbing.Gib(ent.Owner); } private void OnSeverityChanged(Entity ent, ref AnomalySeverityChangedEvent args) diff --git a/Content.Server/Anomaly/Effects/ShuffleParticlesAnomalySystem.cs b/Content.Server/Anomaly/Effects/ShuffleParticlesAnomalySystem.cs index 925c826fb5a..21399676915 100644 --- a/Content.Server/Anomaly/Effects/ShuffleParticlesAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/ShuffleParticlesAnomalySystem.cs @@ -1,9 +1,9 @@ using Content.Server.Anomaly.Components; using Content.Shared.Anomaly.Components; -using Robust.Shared.Physics.Events; using Robust.Shared.Random; namespace Content.Server.Anomaly.Effects; + public sealed class ShuffleParticlesAnomalySystem : EntitySystem { [Dependency] private readonly AnomalySystem _anomaly = default!; @@ -12,19 +12,16 @@ public sealed class ShuffleParticlesAnomalySystem : EntitySystem public override void Initialize() { SubscribeLocalEvent(OnPulse); - SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnAffectedByParticle); } - private void OnStartCollide(Entity ent, ref StartCollideEvent args) + private void OnAffectedByParticle(Entity ent, ref AnomalyAffectedByParticleEvent args) { - if (!TryComp(ent, out var anomaly)) - return; - - if (!HasComp(args.OtherEntity)) + if (!TryComp(ent, out var anomalyComp)) return; if (ent.Comp.ShuffleOnParticleHit && _random.Prob(ent.Comp.Prob)) - _anomaly.ShuffleParticlesEffect((ent, anomaly)); + _anomaly.ShuffleParticlesEffect((args.Anomaly, anomalyComp)); } private void OnPulse(Entity ent, ref AnomalyPulseEvent args) diff --git a/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs new file mode 100644 index 00000000000..d59fbc82b4b --- /dev/null +++ b/Content.Server/Antag/AntagMultipleRoleSpawnerSystem.cs @@ -0,0 +1,40 @@ +using Content.Server.Antag.Components; +using Robust.Shared.Random; + +namespace Content.Server.Antag; + +public sealed class AntagMultipleRoleSpawnerSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ILogManager _log = default!; + + private ISawmill _sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSelectEntity); + + _sawmill = _log.GetSawmill("antag_multiple_spawner"); + } + + private void OnSelectEntity(Entity ent, ref AntagSelectEntityEvent args) + { + // If its more than one the logic breaks + if (args.AntagRoles.Count != 1) + { + _sawmill.Fatal($"Antag multiple role spawner had more than one antag ({args.AntagRoles.Count})"); + return; + } + + var role = args.AntagRoles[0]; + + var entProtos = ent.Comp.AntagRoleToPrototypes[role]; + + if (entProtos.Count == 0) + return; // You will just get a normal job + + args.Entity = Spawn(ent.Comp.PickAndTake ? _random.PickAndTake(entProtos) : _random.Pick(entProtos)); + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 81fb9c789fa..68ba36b7f4e 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -13,7 +13,7 @@ using Content.Server.Preferences.Managers; using Content.Server.Roles; using Content.Server.Roles.Jobs; -using Content.Server.Shuttles.Components; +using Content.Server.Shuttles.Systems; using Content.Shared.Administration.Logs; using Content.Shared.Antag; using Content.Shared.Clothing; @@ -54,6 +54,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem + /// Attempt to make this player be a late-join antag. + /// + /// The session to attempt to make antag. + public void TryMakeLateJoinAntag(ICommonSession session) + { // TODO: this really doesn't handle multiple latejoin definitions well // eventually this should probably store the players per definition with some kind of unique identifier. // something to figure out later. @@ -197,9 +207,7 @@ private void OnSpawnComplete(PlayerSpawnCompleteEvent args) if (!TryGetNextAvailableDefinition((uid, antag), out var def, players)) continue; - var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet - - if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect)) + if (TryMakeAntag((uid, antag), session, def.Value)) break; } } @@ -273,7 +281,7 @@ public void ChooseAntags(Entity ent, bool midround = false) { var playerPool = GetPlayerPool(ent, pool, def); - var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0; + var existingAntagCount = ent.Comp.PreSelectedSessions.TryGetValue(def, out var existingAntags) ? existingAntags.Count : 0; var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def) - existingAntagCount; // if there is both a spawner and players getting picked, let it fall back to a spawner. @@ -406,7 +414,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi antagEnt = Spawn(def.EntityPrototype.Value); else { - var getEntEv = new AntagSelectEntityEvent(session, ent); + var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles); // Imperial Medieval AntagRoles arg RaiseLocalEvent(ent, ref getEntEv, true); antagEnt = getEntEv.Entity; } @@ -416,7 +424,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi if (antagEnt is not { } player) { Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player."); - _adminLogger.Add(LogType.AntagSelection,$"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player."); + _adminLogger.Add(LogType.AntagSelection, $"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player."); if (session != null && ent.Comp.RemoveUponFailedSpawn) { ent.Comp.AssignedSessions.Remove(session); @@ -431,7 +439,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi // Therefore any component subscribing to this has to make sure both subscriptions return the same value // or the ghost role raffle location preview will be wrong. - var getPosEv = new AntagSelectLocationEvent(session, ent); + var getPosEv = new AntagSelectLocationEvent(session, ent, player); RaiseLocalEvent(ent, ref getPosEv, true); if (getPosEv.Handled) { @@ -447,7 +455,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi if (!TryComp(player, out var spawnerComp)) { Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent."); - _adminLogger.Add(LogType.AntagSelection,$"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent."); + _adminLogger.Add(LogType.AntagSelection, $"Antag spawner {player} in gamerule {ToPrettyString(ent)} failed due to not having GhostRoleAntagSpawnerComponent."); if (session != null) { ent.Comp.AssignedSessions.Remove(session); @@ -550,21 +558,21 @@ public bool IsSessionValid(Entity ent, ICommonSession? switch (def.MultiAntagSetting) { case AntagAcceptability.None: - { - if (_role.MindIsAntagonist(mind)) - return false; - if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet - return false; - break; - } + { + if (_role.MindIsAntagonist(mind)) + return false; + if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet + return false; + break; + } case AntagAcceptability.NotExclusive: - { - if (_role.MindIsExclusiveAntagonist(mind)) - return false; - if (GetPreSelectedExclusiveAntagSessions(def).Contains(session)) - return false; - break; - } + { + if (_role.MindIsExclusiveAntagonist(mind)) + return false; + if (GetPreSelectedExclusiveAntagSessions(def).Contains(session)) + return false; + break; + } } // todo: expand this to allow for more fine antag-selection logic for game rules. @@ -583,10 +591,10 @@ public bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) if (entity == null) return true; - if (HasComp(entity)) + if (_arrivals.IsOnArrivals((entity.Value, null))) return false; - if (!def.AllowNonHumans && !HasComp(entity)) + if (!def.AllowNonHumans && !HasComp(entity)) return false; if (def.Whitelist != null) @@ -619,10 +627,13 @@ private void OnObjectivesTextGetInfo(Entity ent, ref Ob /// Only raised if the selected player's current entity is invalid. /// [ByRefEvent] -public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule) +public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule, List> AntagRoles) { public readonly ICommonSession? Session = Session; + /// list of antag role prototypes associated with a entity. used by the + public readonly List> AntagRoles = AntagRoles; + public bool Handled => Entity != null; public EntityUid? Entity; @@ -632,12 +643,15 @@ public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity [ByRefEvent] -public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule) +public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule, EntityUid Entity) { public readonly ICommonSession? Session = Session; public bool Handled => Coordinates.Any(); + // the entity of the antagonist + public EntityUid Entity = Entity; + public List Coordinates = new(); } diff --git a/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs b/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs new file mode 100644 index 00000000000..5a9103a82a6 --- /dev/null +++ b/Content.Server/Antag/Components/AntagMultipleRoleSpawnerComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Roles; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag.Components; + +/// +/// Selects and spawns one prototype from a list for each antag prototype selected by the +/// +[RegisterComponent] +public sealed partial class AntagMultipleRoleSpawnerComponent : Component +{ + /// + /// antag prototype -> list of possible entities to spawn for that antag prototype. Will choose from the list randomly once with replacement unless is set to true + /// + [DataField] + public Dictionary, List> AntagRoleToPrototypes; + + /// + /// Should you remove ent prototypes from the list after spawning one. + /// + [DataField] + public bool PickAndTake; +} diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs index 1e8ed8cdf3a..f21dc2471f4 100644 --- a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs +++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs @@ -52,12 +52,12 @@ private void OnComponentInit(EntityUid uid, BlockGameArcadeComponent component, private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args) { if (component.Player == null) - component.Player = args.Actor; + component.Player = args.User; else - component.Spectators.Add(args.Actor); + component.Spectators.Add(args.User); - UpdatePlayerStatus(uid, args.Actor, component); - component.Game?.UpdateNewPlayerUI(args.Actor); + UpdatePlayerStatus(uid, args.User, component); + component.Game?.UpdateNewPlayerUI(args.User); } private void OnAfterUiClose(EntityUid uid, BlockGameArcadeComponent component, BoundUIClosedEvent args) diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs index 3c8a4de2df8..35e1f5b89b1 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Arcade; +using Content.Shared.Dataset; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -23,85 +24,73 @@ public sealed partial class SpaceVillainArcadeComponent : SharedSpaceVillainArca /// /// The sound played when a new session of the SpaceVillain game is begun. /// - [DataField("newGameSound")] + [DataField] public SoundSpecifier NewGameSound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg"); /// /// The sound played when the player chooses to attack. /// - [DataField("playerAttackSound")] + [DataField] public SoundSpecifier PlayerAttackSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_attack.ogg"); /// /// The sound played when the player chooses to heal. /// - [DataField("playerHealSound")] + [DataField] public SoundSpecifier PlayerHealSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_heal.ogg"); /// /// The sound played when the player chooses to regain mana. /// - [DataField("playerChargeSound")] + [DataField] public SoundSpecifier PlayerChargeSound = new SoundPathSpecifier("/Audio/Effects/Arcade/player_charge.ogg"); /// /// The sound played when the player wins. /// - [DataField("winSound")] + [DataField] public SoundSpecifier WinSound = new SoundPathSpecifier("/Audio/Effects/Arcade/win.ogg"); /// /// The sound played when the player loses. /// - [DataField("gameOverSound")] + [DataField] public SoundSpecifier GameOverSound = new SoundPathSpecifier("/Audio/Effects/Arcade/gameover.ogg"); /// /// The prefixes that can be used to create the game name. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleFightVerbs")] - public List PossibleFightVerbs = new() - {"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"}; + [DataField] + public ProtoId PossibleFightVerbs = "SpaceVillainVerbsFight"; /// /// The first names/titles that can be used to construct the name of the villain. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleFirstEnemyNames")] - public List PossibleFirstEnemyNames = new(){ - "the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King", - "the Space", "Lord", "the Great", "Duke", "General" - }; + [DataField] + public ProtoId PossibleFirstEnemyNames = "SpaceVillainNamesEnemyFirst"; /// /// The last names that can be used to construct the name of the villain. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("possibleLastEnemyNames")] - public List PossibleLastEnemyNames = new() - { - "Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid", - "Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn" - }; + [DataField] + public ProtoId PossibleLastEnemyNames = "SpaceVillainNamesEnemyLast"; /// /// The prototypes that can be dispensed as a reward for winning the game. /// - [ViewVariables(VVAccess.ReadWrite)] [DataField] public List PossibleRewards = new(); /// /// The minimum number of prizes the arcade machine can have. /// - [DataField("rewardMinAmount")] + [DataField] public int RewardMinAmount; /// /// The maximum number of prizes the arcade machine can have. /// - [DataField("rewardMaxAmount")] + [DataField] public int RewardMaxAmount; /// diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs index bb717c7012e..021d6f6f111 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs @@ -4,15 +4,18 @@ using Content.Shared.Advertise.Components; using Content.Shared.Arcade; using Content.Shared.Power; +using Content.Shared.Random.Helpers; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Arcade.SpaceVillain; public sealed partial class SpaceVillainArcadeSystem : EntitySystem { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; @@ -52,7 +55,7 @@ public void ProcessWin(EntityUid uid, SpaceVillainArcadeComponent? arcade = null /// A fight-verb. public string GenerateFightVerb(SpaceVillainArcadeComponent arcade) { - return _random.Pick(arcade.PossibleFightVerbs); + return _random.Pick(_prototypeManager.Index(arcade.PossibleFightVerbs)); } /// @@ -61,7 +64,10 @@ public string GenerateFightVerb(SpaceVillainArcadeComponent arcade) /// An enemy-name. public string GenerateEnemyName(SpaceVillainArcadeComponent arcade) { - return $"{_random.Pick(arcade.PossibleFirstEnemyNames)} {_random.Pick(arcade.PossibleLastEnemyNames)}"; + var possibleFirstEnemyNames = _prototypeManager.Index(arcade.PossibleFirstEnemyNames); + var possibleLastEnemyNames = _prototypeManager.Index(arcade.PossibleLastEnemyNames); + + return $"{_random.Pick(possibleFirstEnemyNames)} {_random.Pick(possibleLastEnemyNames)}"; } private void OnComponentInit(EntityUid uid, SpaceVillainArcadeComponent component, ComponentInit args) diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs index ae4c15f2db3..d8acf44da38 100644 --- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs +++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainGame.cs @@ -243,7 +243,7 @@ private bool CheckGameConditions(EntityUid uid, SpaceVillainArcadeComponent arca UpdateUi( uid, Loc.GetString("space-villain-game-player-loses-message"), - Loc.GetString("space-villain-game-enemy-dies-with-player-message ", ("enemyName", _villainName)), + Loc.GetString("space-villain-game-enemy-dies-with-player-message", ("enemyName", _villainName)), true ); _audioSystem.PlayPvs(arcade.GameOverSound, uid, AudioParams.Default.WithVolume(-4f)); diff --git a/Content.Server/Atmos/Commands/AddAtmosCommand.cs b/Content.Server/Atmos/Commands/AddAtmosCommand.cs index d943b5bf066..70911f4d746 100644 --- a/Content.Server/Atmos/Commands/AddAtmosCommand.cs +++ b/Content.Server/Atmos/Commands/AddAtmosCommand.cs @@ -1,7 +1,7 @@ using Content.Server.Administration; -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Shared.Administration; +using Content.Shared.Atmos.Components; using Robust.Shared.Console; using Robust.Shared.Map.Components; diff --git a/Content.Server/Atmos/Commands/PauseAtmosCommand.cs b/Content.Server/Atmos/Commands/PauseAtmosCommand.cs new file mode 100644 index 00000000000..2893743d8b1 --- /dev/null +++ b/Content.Server/Atmos/Commands/PauseAtmosCommand.cs @@ -0,0 +1,69 @@ +using Content.Server.Administration; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Administration; +using Content.Shared.Atmos.Components; +using Robust.Shared.Console; + +namespace Content.Server.Atmos.Commands; + +[AdminCommand(AdminFlags.Debug)] +public sealed class PauseAtmosCommand : LocalizedEntityCommands +{ + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + + public override string Command => "pauseatmos"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var grid = default(EntityUid); + + switch (args.Length) + { + case 0: + if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity, + out var playerxform) || + playerxform.GridUid == null) + { + shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid")); + return; + } + + grid = playerxform.GridUid.Value; + break; + case 1: + if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid)) + { + shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity")); + return; + } + + grid = parsedGrid; + break; + } + + if (!EntityManager.TryGetComponent(grid, out var gridAtmos)) + { + shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere")); + return; + } + + var newEnt = new Entity(grid, gridAtmos); + + _atmosphereSystem.SetAtmosphereSimulation(newEnt, !newEnt.Comp.Simulated); + shell.WriteLine(Loc.GetString("cmd-pauseatmos-set-atmos-simulation", + ("grid", EntityManager.ToPrettyString(grid)), + ("state", newEnt.Comp.Simulated))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHintOptions( + CompletionHelper.Components(args[0], EntityManager), + Loc.GetString("cmd-pauseatmos-completion-grid-pause")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs b/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs index 3314bf08afa..3c31c79e75f 100644 --- a/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs +++ b/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs @@ -1,8 +1,8 @@ using Content.Server.Administration; -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Shared.Administration; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Robust.Shared.Console; using Robust.Shared.Map; diff --git a/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs b/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs new file mode 100644 index 00000000000..554abff4a8a --- /dev/null +++ b/Content.Server/Atmos/Commands/SubstepAtmosCommand.cs @@ -0,0 +1,104 @@ +using Content.Server.Administration; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Administration; +using Content.Shared.Atmos.Components; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Server.Atmos.Commands; + +[AdminCommand(AdminFlags.Debug)] +public sealed class SubstepAtmosCommand : LocalizedEntityCommands +{ + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + + public override string Command => "substepatmos"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var grid = default(EntityUid); + + switch (args.Length) + { + case 0: + if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity, + out var playerxform) || + playerxform.GridUid == null) + { + shell.WriteError(Loc.GetString("cmd-error-no-grid-provided-or-invalid-grid")); + return; + } + + grid = playerxform.GridUid.Value; + break; + case 1: + if (!EntityUid.TryParse(args[0], out var parsedGrid) || !EntityManager.EntityExists(parsedGrid)) + { + shell.WriteError(Loc.GetString("cmd-error-couldnt-parse-entity")); + return; + } + + grid = parsedGrid; + break; + } + + // i'm straight piratesoftwaremaxxing + if (!EntityManager.TryGetComponent(grid, out var gridAtmos)) + { + shell.WriteError(Loc.GetString("cmd-error-no-gridatmosphere")); + return; + } + + if (!EntityManager.TryGetComponent(grid, out var gasTile)) + { + shell.WriteError(Loc.GetString("cmd-error-no-gastileoverlay")); + return; + } + + if (!EntityManager.TryGetComponent(grid, out var mapGrid)) + { + shell.WriteError(Loc.GetString("cmd-error-no-mapgrid")); + return; + } + + var xform = EntityManager.GetComponent(grid); + + if (xform.MapUid == null || xform.MapID == MapId.Nullspace) + { + shell.WriteError(Loc.GetString("cmd-error-no-valid-map")); + return; + } + + var newEnt = + new Entity(grid, + gridAtmos, + gasTile, + mapGrid, + xform); + + if (gridAtmos.Simulated) + { + shell.WriteLine(Loc.GetString("cmd-substepatmos-info-implicitly-paused-simulation", + ("grid", EntityManager.ToPrettyString(grid)))); + } + + _atmosphereSystem.SetAtmosphereSimulation(newEnt, false); + _atmosphereSystem.RunProcessingFull(newEnt, xform.MapUid.Value, _atmosphereSystem.AtmosTickRate); + + shell.WriteLine(Loc.GetString("cmd-substepatmos-info-substepped-grid", ("grid", EntityManager.ToPrettyString(grid)))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHintOptions( + CompletionHelper.Components(args[0], EntityManager), + Loc.GetString("cmd-substepatmos-completion-grid-substep")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Atmos/Components/AirtightComponent.cs b/Content.Server/Atmos/Components/AirtightComponent.cs index 69488a71307..731153be418 100644 --- a/Content.Server/Atmos/Components/AirtightComponent.cs +++ b/Content.Server/Atmos/Components/AirtightComponent.cs @@ -40,15 +40,10 @@ public sealed partial class AirtightComponent : Component // depressurizing a room. However it can also effectively be used as a means of generating gasses for free // TODO ATMOS Mass conservation. Make it actually push/pull air from adjacent tiles instead of destroying & creating, - - // TODO ATMOS Do we need these two fields? + // TODO ATMOS slate for removal. Stuff doesn't use this. [DataField("rotateAirBlocked")] public bool RotateAirBlocked { get; set; } = true; - // TODO ATMOS remove this? What is this even for?? - [DataField("fixAirBlockedDirectionInitialize")] - public bool FixAirBlockedDirectionInitialize { get; set; } = true; - /// /// If true, then the tile that this entity is on will have no air at all if all directions are blocked. /// diff --git a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs deleted file mode 100644 index 2d36d2bd148..00000000000 --- a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Collections.Concurrent; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.Atmos.Serialization; -using Content.Server.NodeContainer.NodeGroups; - -namespace Content.Server.Atmos.Components -{ - /// - /// Internal Atmos class. Use to interact with atmos instead. - /// - [RegisterComponent, Serializable, - Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))] - public sealed partial class GridAtmosphereComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - public bool Simulated { get; set; } = true; - - [ViewVariables] - public bool ProcessingPaused { get; set; } = false; - - [ViewVariables] - public float Timer { get; set; } = 0f; - - [ViewVariables] - public int UpdateCounter { get; set; } = 1; // DO NOT SET TO ZERO BY DEFAULT! It will break roundstart atmos... - - [ViewVariables] - [IncludeDataField(customTypeSerializer:typeof(TileAtmosCollectionSerializer))] - public Dictionary Tiles = new(1000); - - [ViewVariables] - public HashSet MapTiles = new(1000); - - [ViewVariables] - public readonly HashSet ActiveTiles = new(1000); - - [ViewVariables] - public int ActiveTilesCount => ActiveTiles.Count; - - [ViewVariables] - public readonly HashSet ExcitedGroups = new(1000); - - [ViewVariables] - public int ExcitedGroupCount => ExcitedGroups.Count; - - [ViewVariables] - public readonly HashSet HotspotTiles = new(1000); - - [ViewVariables] - public int HotspotTilesCount => HotspotTiles.Count; - - [ViewVariables] - public readonly HashSet SuperconductivityTiles = new(1000); - - [ViewVariables] - public int SuperconductivityTilesCount => SuperconductivityTiles.Count; - - [ViewVariables] - public HashSet HighPressureDelta = new(1000); - - [ViewVariables] - public int HighPressureDeltaCount => HighPressureDelta.Count; - - /// - /// A list of entities that have a and are to - /// be processed by the , if enabled. - /// - /// To prevent massive bookkeeping overhead, this list is processed in-place, - /// with add/remove/find operations helped via a dict. - /// - /// If you want to add/remove/find entities in this list, - /// use the API methods in the Atmospherics API. - [ViewVariables] - public readonly List> DeltaPressureEntities = - new(AtmosphereSystem.DeltaPressurePreAllocateLength); - - /// - /// An index lookup for the list. - /// Used for add/remove/find operations to speed up processing. - /// - public readonly Dictionary DeltaPressureEntityLookup = - new(AtmosphereSystem.DeltaPressurePreAllocateLength); - - /// - /// Integer that indicates the current position in the - /// list that is being processed. - /// - [ViewVariables(VVAccess.ReadOnly)] - public int DeltaPressureCursor; - - /// - /// Queue of entities that need to have damage applied to them. - /// - [ViewVariables] - public readonly ConcurrentQueue DeltaPressureDamageResults = new(); - - [ViewVariables] - public readonly HashSet PipeNets = new(); - - [ViewVariables] - public readonly HashSet> AtmosDevices = new(); - - [ViewVariables] - public readonly Queue CurrentRunTiles = new(); - - [ViewVariables] - public readonly Queue CurrentRunExcitedGroups = new(); - - [ViewVariables] - public readonly Queue CurrentRunPipeNet = new(); - - [ViewVariables] - public readonly Queue> CurrentRunAtmosDevices = new(); - - [ViewVariables] - public readonly HashSet InvalidatedCoords = new(1000); - - [ViewVariables] - public readonly Queue CurrentRunInvalidatedTiles = new(); - - [ViewVariables] - public readonly List PossiblyDisconnectedTiles = new(100); - - [ViewVariables] - public int InvalidatedCoordsCount => InvalidatedCoords.Count; - - [ViewVariables] - public long EqualizationQueueCycleControl { get; set; } - - [ViewVariables] - public AtmosphereProcessingState State { get; set; } = AtmosphereProcessingState.Revalidate; - } -} diff --git a/Content.Server/Atmos/Components/MapAtmosphereComponent.cs b/Content.Server/Atmos/Components/MapAtmosphereComponent.cs deleted file mode 100644 index bdd05e78494..00000000000 --- a/Content.Server/Atmos/Components/MapAtmosphereComponent.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Atmos; -using Content.Shared.Atmos.Components; -using Content.Shared.Atmos.EntitySystems; - -namespace Content.Server.Atmos.Components; - -/// -/// Component that defines the default GasMixture for a map. -/// -[RegisterComponent, Access(typeof(SharedAtmosphereSystem))] -public sealed partial class MapAtmosphereComponent : SharedMapAtmosphereComponent -{ - /// - /// The default GasMixture a map will have. Space mixture by default. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public GasMixture Mixture = GasMixture.SpaceGas; - - /// - /// Whether empty tiles will be considered space or not. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public bool Space = true; - - public SharedGasTileOverlaySystem.GasOverlayData Overlay; -} diff --git a/Content.Server/Atmos/EntitySystems/AirFilterSystem.cs b/Content.Server/Atmos/EntitySystems/AirFilterSystem.cs index c3344c830c0..ad51f57f887 100644 --- a/Content.Server/Atmos/EntitySystems/AirFilterSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AirFilterSystem.cs @@ -1,8 +1,7 @@ using Content.Server.Atmos.Components; -using Content.Server.Atmos.Piping.Components; using Content.Shared.Atmos; -using Robust.Shared.Map; using System.Diagnostics.CodeAnalysis; +using Content.Shared.Atmos.Components; namespace Content.Server.Atmos.EntitySystems; diff --git a/Content.Server/Atmos/EntitySystems/AirtightSystem.cs b/Content.Server/Atmos/EntitySystems/AirtightSystem.cs index 4a108b38a4e..61cd8b280c6 100644 --- a/Content.Server/Atmos/EntitySystems/AirtightSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AirtightSystem.cs @@ -25,13 +25,17 @@ public override void Initialize() private void OnAirtightInit(Entity airtight, ref ComponentInit args) { - // TODO AIRTIGHT what FixAirBlockedDirectionInitialize even for? - if (!airtight.Comp.FixAirBlockedDirectionInitialize) + // If this entity blocks air in all directions (e.g. full tile walls, doors, and windows) + // we can skip some expensive logic. + if (airtight.Comp.InitialAirBlockedDirection == (int)AtmosDirection.All) { + airtight.Comp.CurrentAirBlockedDirection = airtight.Comp.InitialAirBlockedDirection; UpdatePosition(airtight); return; } + // Otherwise we need to determine its current blocked air directions based on rotation + // and check if it's still airtight. var xform = Transform(airtight); airtight.Comp.CurrentAirBlockedDirection = (int) Rotate((AtmosDirection) airtight.Comp.InitialAirBlockedDirection, xform.LocalRotation); @@ -123,7 +127,7 @@ public void UpdatePosition(Entity ent, TransformComponent? xf public void InvalidatePosition(Entity grid, Vector2i pos) { var query = GetEntityQuery(); - _explosionSystem.UpdateAirtightMap(grid, pos, grid, query); + _explosionSystem.UpdateAirtightMap(grid, pos, grid); _atmosphereSystem.InvalidateTile(grid.Owner, pos); } diff --git a/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs index 505c4a3b385..243905f1bcd 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs @@ -1,6 +1,6 @@ using System.Numerics; -using Content.Server.Atmos.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using Content.Shared.CCVar; using JetBrains.Annotations; diff --git a/Content.Server/Atmos/EntitySystems/AtmosExposedSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosExposedSystem.cs deleted file mode 100644 index 39469e993f0..00000000000 --- a/Content.Server/Atmos/EntitySystems/AtmosExposedSystem.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Content.Shared.Atmos; -using Robust.Shared.Map; - -namespace Content.Server.Atmos.EntitySystems -{ - /* doesn't seem to be a use for this at the moment, so it's disabled - public sealed class AtmosExposedSystem : EntitySystem - {} - */ - - [ByRefEvent] - public readonly struct AtmosExposedUpdateEvent - { - public readonly EntityCoordinates Coordinates; - public readonly GasMixture GasMixture; - public readonly TransformComponent Transform; - - public AtmosExposedUpdateEvent(EntityCoordinates coordinates, GasMixture mixture, TransformComponent transform) - { - Coordinates = coordinates; - GasMixture = mixture; - Transform = transform; - } - } - - /// - /// Event that tries to query the mixture a certain entity is exposed to. - /// This is mainly intended for use with entities inside of containers. - /// This event is not raised for entities that are directly parented to the grid. - /// - [ByRefEvent] - public struct AtmosExposedGetAirEvent - { - /// - /// The entity we want to query this for. - /// - public readonly Entity Entity; - - /// - /// The mixture that the entity is exposed to. Output parameter. - /// - public GasMixture? Gas = null; - - /// - /// Whether to excite the mixture, if possible. - /// - public readonly bool Excite = false; - - /// - /// Whether this event has been handled or not. - /// Check this before changing anything. - /// - public bool Handled = false; - - public AtmosExposedGetAirEvent(Entity entity, bool excite = false) - { - Entity = entity; - Excite = excite; - } - } -} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs index 87cfce135d7..7bb2c0c89c4 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs @@ -1,64 +1,58 @@ +using System.Buffers; using System.Diagnostics; -using System.Linq; -using Content.Server.Atmos.Components; -using Content.Server.Atmos.Piping.Components; using Content.Server.NodeContainer.NodeGroups; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Reactions; using JetBrains.Annotations; using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Atmos.EntitySystems; public partial class AtmosphereSystem { - public GasMixture? GetContainingMixture(Entity ent, bool ignoreExposed = false, bool excite = false) - { - if (!Resolve(ent, ref ent.Comp)) - return null; + /* + General API for interacting with AtmosphereSystem. - return GetContainingMixture(ent, ent.Comp.GridUid, ent.Comp.MapUid, ignoreExposed, excite); - } + If you feel like you're stepping on eggshells because you can't access things in AtmosphereSystem, + consider adding a method here instead of making your own way to work around it. + */ - public GasMixture? GetContainingMixture( - Entity ent, - Entity? grid, - Entity? map, - bool ignoreExposed = false, - bool excite = false) + /// + /// Checks if a grid has an atmosphere. + /// + /// The grid to check. + /// True if the grid has an atmosphere, false otherwise. + [PublicAPI] + public bool HasAtmosphere(EntityUid gridUid) { - if (!Resolve(ent, ref ent.Comp)) - return null; - - if (!ignoreExposed && !ent.Comp.Anchored) - { - // Used for things like disposals/cryo to change which air people are exposed to. - var ev = new AtmosExposedGetAirEvent((ent, ent.Comp), excite); - RaiseLocalEvent(ent, ref ev); - if (ev.Handled) - return ev.Gas; - - // TODO ATMOS: recursively iterate up through parents - // This really needs recursive InContainer metadata flag for performance - // And ideally some fast way to get the innermost airtight container. - } - - var position = _transformSystem.GetGridTilePositionOrDefault((ent, ent.Comp)); - return GetTileMixture(grid, map, position, excite); + return _atmosQuery.HasComponent(gridUid); } - public bool HasAtmosphere(EntityUid gridUid) => _atmosQuery.HasComponent(gridUid); - + /// + /// Sets whether a grid is simulated by Atmospherics. + /// + /// The grid to set. + /// Whether the grid should be simulated. + /// >True if the grid's simulated state was changed, false otherwise. + [PublicAPI] public bool SetSimulatedGrid(EntityUid gridUid, bool simulated) { + // TODO ATMOS this event literally has no subscribers. Did this just get silently refactored out? var ev = new SetSimulatedGridMethodEvent(gridUid, simulated); RaiseLocalEvent(gridUid, ref ev); return ev.Handled; } + /// + /// Checks whether a grid is simulated by Atmospherics. + /// + /// The grid to check. + /// >True if the grid is simulated, false otherwise. public bool IsSimulatedGrid(EntityUid gridUid) { var ev = new IsSimulatedGridMethodEvent(gridUid); @@ -67,24 +61,116 @@ public bool IsSimulatedGrid(EntityUid gridUid) return ev.Simulated; } + /// + /// Gets all s on a grid. + /// + /// The grid to get mixtures for. + /// Whether to mark all tiles as active for atmosphere processing. + /// An enumerable of all gas mixtures on the grid. + [PublicAPI] public IEnumerable GetAllMixtures(EntityUid gridUid, bool excite = false) { var ev = new GetAllMixturesMethodEvent(gridUid, excite); RaiseLocalEvent(gridUid, ref ev); - if(!ev.Handled) - return Enumerable.Empty(); + if (!ev.Handled) + return []; DebugTools.AssertNotNull(ev.Mixtures); return ev.Mixtures!; } + /// + /// Invalidates a tile on a grid, marking it for revalidation. + /// + /// Frequently used tile data like are determined once and cached. + /// If this tile's state changes, ex. being added or removed, then this position in the map needs to + /// be updated. + /// + /// Tiles that need to be updated are marked as invalid and revalidated before all other + /// processing stages. + /// + /// The grid entity. + /// The tile to invalidate. + [PublicAPI] public void InvalidateTile(Entity entity, Vector2i tile) { if (_atmosQuery.Resolve(entity.Owner, ref entity.Comp, false)) entity.Comp.InvalidatedCoords.Add(tile); } + /// + /// Gets the gas mixture for a specific tile that an entity is on. + /// + /// The entity to get the tile mixture for. + /// Whether to mark the tile as active for atmosphere processing. + /// A if one could be found, null otherwise. + /// This does not return the that the entity + /// may be contained in, ex. if the entity is currently in a locker/crate with its own + /// . + [PublicAPI] + public GasMixture? GetTileMixture(Entity entity, bool excite = false) + { + if (!Resolve(entity.Owner, ref entity.Comp)) + return null; + + var indices = XformSystem.GetGridTilePositionOrDefault(entity); + return GetTileMixture(entity.Comp.GridUid, entity.Comp.MapUid, indices, excite); + } + + /// + /// Gets the that an entity is contained within. + /// + /// The entity to get the mixture for. + /// If true, will ignore mixtures that the entity is contained in + /// (ex. lockers and cryopods) and just get the tile mixture. + /// If true, will mark the tile as active for atmosphere processing. + /// A if one could be found, null otherwise. + [PublicAPI] + public GasMixture? GetContainingMixture(Entity ent, bool ignoreExposed = false, bool excite = false) + { + if (!Resolve(ent, ref ent.Comp)) + return null; + + return GetContainingMixture(ent, ent.Comp.GridUid, ent.Comp.MapUid, ignoreExposed, excite); + } + + /// + /// Gets the that an entity is contained within. + /// + /// The entity to get the mixture for. + /// The grid that the entity may be on. + /// The map that the entity may be on. + /// If true, will ignore mixtures that the entity is contained in + /// (ex. lockers and cryopods) and just get the tile mixture. + /// If true, will mark the tile as active for atmosphere processing. + /// A if one could be found, null otherwise. + [PublicAPI] + public GasMixture? GetContainingMixture(Entity ent, + Entity? grid, + Entity? map, + bool ignoreExposed = false, + bool excite = false) + { + if (!Resolve(ent, ref ent.Comp)) + return null; + + if (!ignoreExposed && TryGetExposedMixture(ent, out var mixture)) + return mixture; + + var position = XformSystem.GetGridTilePositionOrDefault((ent, ent.Comp)); + return GetTileMixture(grid, map, position, excite); + } + + /// + /// Gets the gas mixtures for a list of tiles on a grid or map. + /// + /// The grid to get mixtures from. + /// The map to get mixtures from. + /// The list of tiles to get mixtures for. + /// Whether to mark the tiles as active for atmosphere processing. + /// >An array of gas mixtures corresponding to the input tiles. + [PublicAPI] public GasMixture?[]? GetTileMixtures( Entity? grid, Entity? map, @@ -95,7 +181,7 @@ public void InvalidateTile(Entity entity, Vector2i til var handled = false; // If we've been passed a grid, try to let it handle it. - if (grid is {} gridEnt && Resolve(gridEnt, ref gridEnt.Comp1)) + if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1)) { if (excite) Resolve(gridEnt, ref gridEnt.Comp2); @@ -128,7 +214,7 @@ public void InvalidateTile(Entity entity, Vector2i til // We either don't have a grid, or the event wasn't handled. // Let the map handle it instead, and also broadcast the event. - if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp)) + if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp)) { mixtures ??= new GasMixture?[tiles.Count]; for (var i = 0; i < tiles.Count; i++) @@ -145,18 +231,11 @@ public void InvalidateTile(Entity entity, Vector2i til { mixtures[i] ??= GasMixture.SpaceGas; } - return mixtures; - } - - public GasMixture? GetTileMixture (Entity entity, bool excite = false) - { - if (!Resolve(entity.Owner, ref entity.Comp)) - return null; - var indices = _transformSystem.GetGridTilePositionOrDefault(entity); - return GetTileMixture(entity.Comp.GridUid, entity.Comp.MapUid, indices, excite); + return mixtures; } + [PublicAPI] public GasMixture? GetTileMixture( Entity? grid, Entity? map, @@ -164,8 +243,8 @@ public void InvalidateTile(Entity entity, Vector2i til bool excite = false) { // If we've been passed a grid, try to let it handle it. - if (grid is {} gridEnt - && Resolve(gridEnt, ref gridEnt.Comp1, false) + if (grid is { } gridEnt + && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp1, false) && gridEnt.Comp1.Tiles.TryGetValue(gridTile, out var tile)) { if (excite) @@ -177,13 +256,137 @@ public void InvalidateTile(Entity entity, Vector2i til return tile.Air; } - if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) + if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) return mapEnt.Comp.Mixture; // Default to a space mixture... This is a space game, after all! return GasMixture.SpaceGas; } + public override void MergeContainingMixture(Entity entity, GasMixture mixture, bool ignoreExposed = false, bool excite = false) + { + if (GetContainingMixture(entity, ignoreExposed, excite) is not { } containingMixture) + return; + + Merge(containingMixture, mixture); + } + + [PublicAPI] + public override void MergeTileMixture(Entity entity, GasMixture mixture, bool excite = false) + { + if (GetTileMixture(entity, excite) is not { } tileMixture) + return; + + Merge(tileMixture, mixture); + } + + public override void AdjustContainingMixture(Entity entity, Gas gas, float mols, bool ignoreExposed = false, bool excite = false) + { + GetContainingMixture(entity, ignoreExposed, excite)?.AdjustMoles(gas, mols); + } + + [PublicAPI] + public override void AdjustTileMixture(Entity entity, Gas gas, float mols, bool excite = false) + { + GetTileMixture(entity, excite)?.AdjustMoles(gas, mols); + } + + /// + /// Retrieves the pressures of all gas mixtures + /// in the given array of s, and stores the results in the + /// provided span. + /// + /// The tiles span to find the pressures of. + /// The span to store the pressures to - this should be the same length + /// as the tile array. + /// Thrown when the length of the provided spans do not match. + /// Note that for or s that are null, + /// this method will return a value close to zero but not exactly zero. + [PublicAPI] + public static void GetBulkTileAtmospherePressures(Span tiles, Span pressures) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(tiles.Length, pressures.Length); + + var len = tiles.Length; + var arr1 = ArrayPool.Shared.Rent(len); + + try + { + var mixtSpan = arr1.AsSpan(0, len); + for (var i = 0; i < tiles.Length; i++) + { + mixtSpan[i] = tiles[i]?.Air; + } + + GetBulkGasMixturePressures(mixtSpan, pressures); + } + finally + { + ArrayPool.Shared.Return(arr1); + } + } + + /// + /// Gets the pressures of a of s. + /// + /// The to get the pressures of. + /// The to store the pressures to - this should be the same length + /// as the mixtures array. + /// Thrown when the length of the provided spans do not match. + /// Note that for GasMixtures that are null, this method will return a value close to zero but not exactly zero. + [PublicAPI] + public static void GetBulkGasMixturePressures(Span mixtures, Span pressures) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(mixtures.Length, pressures.Length); + + var len = mixtures.Length; + + var arr1 = ArrayPool.Shared.Rent(len); + var arr2 = ArrayPool.Shared.Rent(len); + var arr3 = ArrayPool.Shared.Rent(len); + try + { + var mixtVol = arr1.AsSpan(0, len); + var mixtTemp = arr2.AsSpan(0, len); + var mixtMoles = arr3.AsSpan(0, len); + + for (var i = 0; i < len; i++) + { + if (mixtures[i] is not { } mixture) + { + // To prevent any NaN/Div/0 errors, we just bite the bullet + // and set everything to the lowest possible value. + mixtVol[i] = 1; + mixtTemp[i] = 1; + mixtMoles[i] = float.Epsilon; + continue; + } + + mixtVol[i] = mixture.Volume; + mixtTemp[i] = mixture.Temperature; + mixtMoles[i] = mixture.TotalMoles; + } + + // TODO NumericsHelpers need a method that substitutes NaNs with zeros. AVX-512 has one iirc but for 256/128 we need to do some masking bs + NumericsHelpers.Multiply(mixtMoles, Atmospherics.R); + NumericsHelpers.Multiply(mixtMoles, mixtTemp); + NumericsHelpers.Divide(mixtMoles, mixtVol, pressures); + } + finally + { + ArrayPool.Shared.Return(arr1); + ArrayPool.Shared.Return(arr2); + ArrayPool.Shared.Return(arr3); + } + } + + /// + /// Triggers a tile's to react. + /// + /// The grid to react the tile on. + /// The tile to react. + /// The result of the reaction. + [PublicAPI] public ReactionResult ReactTile(EntityUid gridId, Vector2i tile) { var ev = new ReactTileMethodEvent(gridId, tile); @@ -194,7 +397,23 @@ public ReactionResult ReactTile(EntityUid gridId, Vector2i tile) return ev.Result; } - public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, MapGridComponent? mapGridComp = null) + /// + /// Checks if a tile on a grid is air-blocked in the specified directions. + /// This only checks for if the current tile, and only the current tile, is blocking + /// air. + /// + /// The grid to check. + /// The tile on the grid to check. + /// The directions to check for air-blockage. + /// Optional map grid component associated with the grid. + /// True if the tile is air-blocked in the specified directions, false otherwise. + /// This rebuilds airtight data on-the-fly. You should only use this if you've just + /// invalidated airtight data, and you cannot wait one atmostick to revalidate it. + [PublicAPI] + public bool IsTileAirBlocked(EntityUid gridUid, + Vector2i tile, + AtmosDirection directions = AtmosDirection.All, + MapGridComponent? mapGridComp = null) { if (!Resolve(gridUid, ref mapGridComp, false)) return false; @@ -203,15 +422,77 @@ public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection di return data.BlockedDirections.IsFlagSet(directions); } + /// + /// Checks if a tile on a grid is air-blocked in the specified directions, using cached data. + /// This only checks for if the current tile, and only the current tile, is blocking + /// air. + /// + /// The grid to check. + /// The tile on the grid to check. + /// The directions to check for air-blockage. + /// True if the tile is air-blocked in the specified directions, false otherwise. + /// Returns data that is currently cached by Atmospherics. + /// You should always use this method over as it's more performant. + /// If you need to get up-to-date data because you've just invalidated airtight data, + /// use . + [PublicAPI] + public bool IsTileAirBlockedCached(Entity grid, + Vector2i tile, + AtmosDirection directions = AtmosDirection.All) + { + if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) + return false; + + if (!grid.Comp.Tiles.TryGetValue(tile, out var atmosTile)) + return false; + + return atmosTile.AirtightData.BlockedDirections.IsFlagSet(directions); + } + + /// + /// Returns the for a tile on a grid. + /// This represents the directions that the air can currently flow to. + /// + /// The grid entity that the tile belongs to. + /// The coordinates to check. + /// The of the tile, + /// if the grid or tile couldn't be found. + /// Note that this data is cached and is updated at the beginning of every atmostick. + /// As such, any airtight changes that were made may not be reflected in this value until + /// the cache is refreshed in the next processing tick. + [PublicAPI] + public AtmosDirection GetAirflowDirections(Entity grid, Vector2i tile) + { + if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) + return AtmosDirection.Invalid; + + if (!grid.Comp.Tiles.TryGetValue(tile, out var atmosTile)) + return AtmosDirection.Invalid; + + return atmosTile.AdjacentBits; + } + + /// + /// Checks if a tile on a grid or map is space as defined by a tile's definition of space. + /// Some tiles can hold back space and others cannot - for example, plating can hold + /// back space, whereas scaffolding cannot, exposing the map atmosphere beneath. + /// + /// This does not check if the on the tile is space, + /// it only checks the current tile's ability to hold back space. + /// The grid to check. + /// The map to check. + /// The tile to check. + /// True if the tile is space, false otherwise. + [PublicAPI] public bool IsTileSpace(Entity? grid, Entity? map, Vector2i tile) { - if (grid is {} gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false) - && gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos)) + if (grid is { } gridEnt && _atmosQuery.Resolve(gridEnt, ref gridEnt.Comp, false) + && gridEnt.Comp.Tiles.TryGetValue(tile, out var tileAtmos)) { return tileAtmos.Space; } - if (map is {} mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) + if (map is { } mapEnt && _mapAtmosQuery.Resolve(mapEnt, ref mapEnt.Comp, false)) return mapEnt.Comp.Space; // If nothing handled the event, it'll default to true. @@ -219,28 +500,77 @@ public bool IsTileSpace(Entity? grid, Entity + /// Checks if the gas mixture on a tile is "probably safe". + /// Probably safe is defined as having at least air alarm-grade safe pressure and temperature. + /// (more than 260K, less than 360K, and between safe low and high pressure as defined in + /// and ) + /// + /// The grid to check. + /// The map to check. + /// The tile to check. + /// True if the tile's mixture is probably safe, false otherwise. + [PublicAPI] public bool IsTileMixtureProbablySafe(Entity? grid, Entity map, Vector2i tile) { return IsMixtureProbablySafe(GetTileMixture(grid, map, tile)); } + /// + /// Gets the heat capacity of the gas mixture on a tile. + /// + /// The grid to check. + /// The map to check. + /// The tile on the grid/map to check. + /// >The heat capacity of the tile's mixture, or the heat capacity of space if a mixture could not be found. + [PublicAPI] public float GetTileHeatCapacity(Entity? grid, Entity map, Vector2i tile) { return GetHeatCapacity(GetTileMixture(grid, map, tile) ?? GasMixture.SpaceGas); } + /// + /// Gets an enumerator for the adjacent tile mixtures of a tile on a grid. + /// + /// The grid to get adjacent tile mixtures from. + /// The tile to get adjacent mixtures for. + /// Whether to include blocked adjacent tiles. + /// Whether to mark the adjacent tiles as active for atmosphere processing. + /// An enumerator for the adjacent tile mixtures. + [PublicAPI] public TileMixtureEnumerator GetAdjacentTileMixtures(Entity grid, Vector2i tile, bool includeBlocked = false, bool excite = false) { + // TODO ATMOS includeBlocked and excite parameters are unhandled currently. if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return TileMixtureEnumerator.Empty; return !grid.Comp.Tiles.TryGetValue(tile, out var atmosTile) ? TileMixtureEnumerator.Empty - : new(atmosTile.AdjacentTiles); + : new TileMixtureEnumerator(atmosTile.AdjacentTiles); } - public void HotspotExpose(Entity grid, Vector2i tile, float exposedTemperature, float exposedVolume, - EntityUid? sparkSourceUid = null, bool soh = false) + /// + /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met. + /// + /// The grid to expose the tile on. + /// The tile to expose. + /// The temperature of the hotspot to expose. + /// You can think of this as exposing a temperature of a flame. + /// The volume of the hotspot to expose. + /// You can think of this as how big the flame is initially. + /// Bigger flames will ramp a fire faster. + /// Whether to "boost" a fire that's currently on the tile already. + /// Does nothing if the tile isn't already a hotspot. + /// This clamps the temperature and volume of the hotspot to the maximum + /// of the provided parameters and whatever's on the tile. + /// Entity that started the exposure for admin logging. + [PublicAPI] + public void HotspotExpose(Entity grid, + Vector2i tile, + float exposedTemperature, + float exposedVolume, + EntityUid? sparkSourceUid = null, + bool soh = false) { if (!_atmosQuery.Resolve(grid, ref grid.Comp, false)) return; @@ -249,8 +579,26 @@ public void HotspotExpose(Entity grid, Vector2i tile, HotspotExpose(grid.Comp, atmosTile, exposedTemperature, exposedVolume, soh, sparkSourceUid); } - public void HotspotExpose(TileAtmosphere tile, float exposedTemperature, float exposedVolume, - EntityUid? sparkSourceUid = null, bool soh = false) + /// + /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met. + /// + /// The to expose. + /// The temperature of the hotspot to expose. + /// You can think of this as exposing a temperature of a flame. + /// The volume of the hotspot to expose. + /// You can think of this as how big the flame is initially. + /// Bigger flames will ramp a fire faster. + /// Whether to "boost" a fire that's currently on the tile already. + /// Does nothing if the tile isn't already a hotspot. + /// This clamps the temperature and volume of the hotspot to the maximum + /// of the provided parameters and whatever's on the tile. + /// Entity that started the exposure for admin logging. + [PublicAPI] + public void HotspotExpose(TileAtmosphere tile, + float exposedTemperature, + float exposedVolume, + EntityUid? sparkSourceUid = null, + bool soh = false) { if (!_atmosQuery.TryGetComponent(tile.GridIndex, out var atmos)) return; @@ -259,12 +607,25 @@ public void HotspotExpose(TileAtmosphere tile, float exposedTemperature, float e HotspotExpose(atmos, tile, exposedTemperature, exposedVolume, soh, sparkSourceUid); } + /// + /// Extinguishes a hotspot on a tile. + /// + /// The grid to extinguish the hotspot on. + /// The tile on the grid to extinguish the hotspot on. + [PublicAPI] public void HotspotExtinguish(EntityUid gridUid, Vector2i tile) { var ev = new HotspotExtinguishMethodEvent(gridUid, tile); RaiseLocalEvent(gridUid, ref ev); } + /// + /// Checks if a hotspot is active on a tile. + /// + /// The grid to check. + /// The tile on the grid to check. + /// True if a hotspot is active on the tile, false otherwise. + [PublicAPI] public bool IsHotspotActive(EntityUid gridUid, Vector2i tile) { var ev = new IsHotspotActiveMethodEvent(gridUid, tile); @@ -274,11 +635,25 @@ public bool IsHotspotActive(EntityUid gridUid, Vector2i tile) return ev.Result; } + /// + /// Adds a to a grid. + /// + /// The grid to add the pipe net to. + /// The pipe net to add. + /// True if the pipe net was added, false otherwise. + [PublicAPI] public bool AddPipeNet(Entity grid, PipeNet pipeNet) { return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Add(pipeNet); } + /// + /// Removes a from a grid. + /// + /// The grid to remove the pipe net from. + /// The pipe net to remove. + /// True if the pipe net was removed, false otherwise. + [PublicAPI] public bool RemovePipeNet(Entity grid, PipeNet pipeNet) { // Technically this event can be fired even on grids that don't @@ -292,6 +667,13 @@ public bool RemovePipeNet(Entity grid, PipeNet pipeNet return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet); } + /// + /// Adds an entity with an to a grid's list of atmos devices. + /// + /// The grid to add the device to. + /// The device to add. + /// True if the device was added, false otherwise. + [PublicAPI] public bool AddAtmosDevice(Entity grid, Entity device) { DebugTools.Assert(device.Comp.JoinedGrid == null); @@ -307,6 +689,12 @@ public bool AddAtmosDevice(Entity grid, Entity + /// Removes an entity with an from a grid's list of atmos devices. + /// + /// The grid to remove the device from. + /// The device to remove. + /// True if the device was removed, false otherwise. public bool RemoveAtmosDevice(Entity grid, Entity device) { DebugTools.Assert(device.Comp.JoinedGrid == grid); @@ -355,10 +743,6 @@ public bool TryAddDeltaPressureEntity(Entity grid, Ent grid.Comp.DeltaPressureEntityLookup[ent.Owner] = grid.Comp.DeltaPressureEntities.Count; grid.Comp.DeltaPressureEntities.Add(ent); - ent.Comp.CurrentPosition = _map.CoordinatesToTile(grid, - Comp(grid), - xform.Coordinates); - ent.Comp.GridUid = grid.Owner; ent.Comp.InProcessingList = true; @@ -422,23 +806,64 @@ public bool IsDeltaPressureEntityInList(Entity grid, E return contains; } - [ByRefEvent] private record struct SetSimulatedGridMethodEvent - (EntityUid Grid, bool Simulated, bool Handled = false); - - [ByRefEvent] private record struct IsSimulatedGridMethodEvent - (EntityUid Grid, bool Simulated = false, bool Handled = false); - - [ByRefEvent] private record struct GetAllMixturesMethodEvent - (EntityUid Grid, bool Excite = false, IEnumerable? Mixtures = null, bool Handled = false); - - [ByRefEvent] private record struct ReactTileMethodEvent - (EntityUid GridId, Vector2i Tile, ReactionResult Result = default, bool Handled = false); - - [ByRefEvent] private record struct HotspotExtinguishMethodEvent - (EntityUid Grid, Vector2i Tile, bool Handled = false); + /// + /// Applies an exponential moving average to a value, given a new value, an old value, and a time delta. + /// + /// The new value to factor into the average. + /// The old value to factor into the average. + /// The time delta to factor into the average. + /// The time constant to use for the average. + /// Higher values will make the average change more slowly, + /// while lower values will make it change more quickly. + /// The result of the exponential moving average. + [PublicAPI] + public static float ExponentialMovingAverage(float newValue, + float oldValue, + float deltaTime, + float tau = 0.5f) + { + var a = deltaTime / tau; + return a * newValue + (1 - a) * oldValue; + } - [ByRefEvent] private record struct IsHotspotActiveMethodEvent - (EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false); + [ByRefEvent] + private record struct SetSimulatedGridMethodEvent( + EntityUid Grid, + bool Simulated, + bool Handled = false); + + [ByRefEvent] + private record struct IsSimulatedGridMethodEvent( + EntityUid Grid, + bool Simulated = false, + bool Handled = false); + + [ByRefEvent] + private record struct GetAllMixturesMethodEvent( + EntityUid Grid, + bool Excite = false, + IEnumerable? Mixtures = null, + bool Handled = false); + + [ByRefEvent] + private record struct ReactTileMethodEvent( + EntityUid GridId, + Vector2i Tile, + ReactionResult Result = default, + bool Handled = false); + + [ByRefEvent] + private record struct HotspotExtinguishMethodEvent( + EntityUid Grid, + Vector2i Tile, + bool Handled = false); + + [ByRefEvent] + private record struct IsHotspotActiveMethodEvent( + EntityUid Grid, + Vector2i Tile, + bool Result = false, + bool Handled = false); } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs index f86ebcee734..4be0dbfc39a 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs @@ -1,5 +1,6 @@ using Content.Server.Atmos.Components; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Robust.Shared.Map.Components; namespace Content.Server.Atmos.EntitySystems; @@ -46,4 +47,27 @@ public bool RunProcessingStage( return processingPaused; } + + /// + /// Fully runs one entity through the entire Atmos processing loop. + /// + /// The entity to simulate. + /// The that belongs to the grid's map. + /// Elapsed time to simulate. Recommended value is . + public void RunProcessingFull(Entity ent, + Entity mapAtmosphere, + float frameTime) + { + while (ProcessAtmosphere(ent, mapAtmosphere, frameTime) != AtmosphereProcessingCompletionState.Finished) { } + } + + /// + /// Allows or disallows atmosphere simulation on a . + /// + /// The atmosphere to pause or unpause processing. + /// The state to set. True means that the atmosphere is allowed to simulate, false otherwise. + public void SetAtmosphereSimulation(Entity ent, bool simulate) + { + ent.Comp.Simulated = simulate; + } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs index f24f0ae171f..11e7cde2548 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs @@ -25,7 +25,6 @@ public sealed partial class AtmosphereSystem public float AtmosMaxProcessTime { get; private set; } public float AtmosTickRate { get; private set; } public float Speedup { get; private set; } - public float HeatScale { get; private set; } public bool DeltaPressureDamage { get; private set; } public int DeltaPressureParallelProcessPerIteration { get; private set; } public int DeltaPressureParallelBatchSize { get; private set; } @@ -55,7 +54,6 @@ private void InitializeCVars() Subs.CVar(_cfg, CCVars.AtmosMaxProcessTime, value => AtmosMaxProcessTime = value, true); Subs.CVar(_cfg, CCVars.AtmosTickRate, value => AtmosTickRate = value, true); Subs.CVar(_cfg, CCVars.AtmosSpeedup, value => Speedup = value, true); - Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true); Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true); Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = value, true); Subs.CVar(_cfg, CCVars.DeltaPressureDamage, value => DeltaPressureDamage = value, true); diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs index 246c3a571fc..11ebfe2066f 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Commands.cs @@ -19,7 +19,9 @@ private void InitializeCommands() // Fix Grid Atmos command. _consoleHost.RegisterCommand("fixgridatmos", "Makes every tile on a grid have a roundstart gas mix.", - "fixgridatmos ", FixGridAtmosCommand, FixGridAtmosCommandCompletions); + "fixgridatmos ", + FixGridAtmosCommand, + FixGridAtmosCommandCompletions); } private void ShutdownCommands() @@ -36,42 +38,6 @@ private void FixGridAtmosCommand(IConsoleShell shell, string argstr, string[] ar return; } - var mixtures = new GasMixture[9]; - for (var i = 0; i < mixtures.Length; i++) - mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C }; - - // 0: Air - mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard); - mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard); - - // 1: Vaccum - - // 2: Oxygen (GM) - mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner); - - // 3: Nitrogen (GM) - mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner); - - // 4: Plasma (GM) - mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner); - - // 5: Instant Plasmafire (r) - mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner); - mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner); - mixtures[5].Temperature = 5000f; - - // 6: (Walk-In) Freezer - mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer); - mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer); - mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings - - // 7: Nitrogen (101kpa) for vox rooms - mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard); - - // 8: Air (GM) - mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner); - mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner); - foreach (var arg in args) { if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid)) @@ -92,32 +58,80 @@ private void FixGridAtmosCommand(IConsoleShell shell, string argstr, string[] ar continue; } - // Force Invalidate & update air on all tiles - Entity grid = - new(euid.Value, gridAtmosphere, Comp(euid.Value), gridComp, Transform(euid.Value)); + RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp)); + } + } - RebuildGridTiles(grid); + /// + /// Rebuilds all s on a grid to have roundstart gas mixes. + /// + /// Please be responsible with this method. Used only by tests and fixgridatmos. + public void RebuildGridAtmosphere(Entity ent) + { + var mixtures = new GasMixture[9]; + for (var i = 0; i < mixtures.Length; i++) + { + mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C }; + } - var query = GetEntityQuery(); - foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray()) - { - if (tile.Air is not {Immutable: false} air) - continue; - - air.Clear(); - var mixtureId = 0; - var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices); - while (enumerator.MoveNext(out var entUid)) - { - if (query.TryComp(entUid, out var marker)) - mixtureId = marker.Mode; - } - - var mixture = mixtures[mixtureId]; - Merge(air, mixture); - air.Temperature = mixture.Temperature; - } - } + // 0: Air + mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard); + mixtures[0].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard); + + // 1: Vaccum + + // 2: Oxygen (GM) + mixtures[2].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner); + + // 3: Nitrogen (GM) + mixtures[3].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellGasMiner); + + // 4: Plasma (GM) + mixtures[4].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner); + + // 5: Instant Plasmafire (r) + mixtures[5].AdjustMoles(Gas.Oxygen, Atmospherics.MolesCellGasMiner); + mixtures[5].AdjustMoles(Gas.Plasma, Atmospherics.MolesCellGasMiner); + mixtures[5].Temperature = 5000f; + + // 6: (Walk-In) Freezer + mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer); + mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer); + mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings + + // 7: Nitrogen (101kpa) for vox rooms + mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard); + + // 8: Air (GM) + mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner); + mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner); + + + // Force Invalidate & update air on all tiles + Entity grid = + new(ent.Owner, ent.Comp1, Comp(ent), ent.Comp2, Transform(ent)); + + RebuildGridTiles(grid); + + var query = GetEntityQuery(); + foreach (var (indices, tile) in ent.Comp1.Tiles.ToArray()) + { + if (tile.Air is not {Immutable: false} air) + continue; + + air.Clear(); + var mixtureId = 0; + var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(grid, grid, indices); + while (enumerator.MoveNext(out var entUid)) + { + if (query.TryComp(entUid, out var marker)) + mixtureId = marker.Mode; + } + + var mixture = mixtures[mixtureId]; + Merge(air, mixture); + air.Temperature = mixture.Temperature; + } } /// diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs index 9d72b195fef..59ccbbb8c8a 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs @@ -1,5 +1,9 @@ +using System.Buffers; +using System.Collections.Concurrent; using Content.Server.Atmos.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Damage; using Robust.Shared.Random; using Robust.Shared.Threading; @@ -8,6 +12,18 @@ namespace Content.Server.Atmos.EntitySystems; public sealed partial class AtmosphereSystem { + /// + /// A queue that handles scheduling of invalid entities to be removed from the entity processing list. + /// + /// We cannot change the contents of the list while processing it in parallel as this may create + /// a race condition for other thread pool workers working on different parts of the same list (as removing + /// items from the list will do a substitution of items to fill the gap, which can touch ents + /// other threads may be working on). + /// + /// As such, we just delay removal of these entities until after parallel processing. + /// + private readonly ConcurrentQueue> _deltaPressureInvalidEntityQueue = new(); + /// /// The number of pairs of opposing directions we can have. /// This is Atmospherics.Directions / 2, since we always compare opposing directions @@ -17,135 +33,171 @@ public sealed partial class AtmosphereSystem private const int DeltaPressurePairCount = Atmospherics.Directions / 2; /// - /// The length to pre-allocate list/dicts of delta pressure entities on a . - /// - public const int DeltaPressurePreAllocateLength = 1000; - - /// - /// Processes a singular entity, determining the pressures it's experiencing and applying damage based on that. + /// Bulk processes a range of entities on a + /// from a starting index to an ending index, + /// determining the pressures they're experiencing and applying damage based on that. /// - /// The entity to process. /// The that belongs to the entity's GridUid. - private void ProcessDeltaPressureEntity(Entity ent, GridAtmosphereComponent gridAtmosComp) + /// The starting index in the DeltaPressureEntities list to process from. + /// The ending index in the DeltaPressureEntities list to process to. + private void ProcessDeltaPressureEntityBulk(GridAtmosphereComponent gridAtmosComp, int start, int end) { - if (!_random.Prob(ent.Comp.RandomDamageChance)) - return; - /* - To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods - in the NumericsHelpers class. + To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods. + This requires that we load our data into spans to process them in bulk. - This involves loading our values into a span in the form of opposing pairs, - so simple vector operations like min/max/abs can be performed on them. + This code takes advantage of ArrayPool so we can super easily reuse memory per tick + in threading contexts, otherwise this will literally obliterate GC with a nuclear bomb. */ - var tiles = new TileAtmosphere?[Atmospherics.Directions]; - for (var i = 0; i < Atmospherics.Directions; i++) - { - var direction = (AtmosDirection)(1 << i); - var offset = ent.Comp.CurrentPosition.Offset(direction); - tiles[i] = gridAtmosComp.Tiles.GetValueOrDefault(offset); - } + var entList = gridAtmosComp.DeltaPressureEntities; + var len = end - start; - Span pressures = stackalloc float[Atmospherics.Directions]; + const int dirs = Atmospherics.Directions; + // Total number of tiles to gather = number of entities * number of directions. + var lenDirs = len * dirs; + // Total number of opposing direction pairs to process = number of entities * number of opposing direction pairs. + var pairLen = len * DeltaPressurePairCount; - GetBulkTileAtmospherePressures(tiles, pressures); + // Memory is meant to be used. Allocate a bunch of it. + // Note that the arrays returned by ArrayPool are usually larger than requested + // (ASK MY DEBUGGER HOW I KNOW) so we ask Span to give us a slice of the correct length. + var tilesArr = ArrayPool.Shared.Rent(lenDirs); + var tiles = tilesArr.AsSpan(0, lenDirs); - Span opposingGroupA = stackalloc float[DeltaPressurePairCount]; - Span opposingGroupB = stackalloc float[DeltaPressurePairCount]; - Span opposingGroupMax = stackalloc float[DeltaPressurePairCount]; + // I really hate dict lookups in hot loops so we keep AirtightComps on us. + // This is the only array that isn't sliced since we do not have any operation that + // requires SIMD on it, either directly or indirectly + // (SIMD requires that arrays are same length or else indexes/supporting code will likely overstep). + var airtightCompsArr = ArrayPool.Shared.Rent(len); - // Directions are always in pairs: the number of directions is always even - // (we must consider the future where Multi-Z is real) - // Load values into opposing pairs. - for (var i = 0; i < DeltaPressurePairCount; i++) - { - opposingGroupA[i] = pressures[i]; - opposingGroupB[i] = pressures[i + DeltaPressurePairCount]; - } - - // TODO ATMOS: Needs to be changed to batch operations so that more operations can actually be done in parallel. + var groupAArr = ArrayPool.Shared.Rent(pairLen); + var groupBArr = ArrayPool.Shared.Rent(pairLen); + var groupMaxArr = ArrayPool.Shared.Rent(pairLen); - // Need to determine max pressure in opposing directions for absolute pressure calcs. - NumericsHelpers.Max(opposingGroupA, opposingGroupB, opposingGroupMax); + var groupA = groupAArr.AsSpan(0, pairLen); + var groupB = groupBArr.AsSpan(0, pairLen); + var groupMax = groupMaxArr.AsSpan(0, pairLen); - // Calculate pressure differences between opposing directions. - NumericsHelpers.Sub(opposingGroupA, opposingGroupB); - NumericsHelpers.Abs(opposingGroupA); + var pressuresArr = ArrayPool.Shared.Rent(lenDirs); + var pressures = pressuresArr.AsSpan(0, lenDirs); - var maxPressure = 0f; - var maxDelta = 0f; - for (var i = 0; i < DeltaPressurePairCount; i++) + try { - maxPressure = MathF.Max(maxPressure, opposingGroupMax[i]); - maxDelta = MathF.Max(maxDelta, opposingGroupA[i]); - } + // Gather tiles & airtight components. + for (var i = 0; i < len; i++) + { + var ent = entList[start + i]; + + // Ensure that the list still only contains valid airtight entities. + // We cannot remove them here, so we enqueue them for later removal. + if (!_airtightQuery.TryComp(ent, out var airtightComp)) + { + _deltaPressureInvalidEntityQueue.Enqueue(ent); + Log.Error($"DeltaPressure entity without an AirtightComponent found in processing list! Ent: {ent}"); + return; + } + + airtightCompsArr[i] = airtightComp; + var currentPos = airtightComp.LastPosition.Tile; + var tileBase = i * dirs; + + for (var j = 0; j < dirs; j++) + { + var direction = (AtmosDirection)(1 << j); + var offset = currentPos.Offset(direction); + tiles[tileBase + j] = gridAtmosComp.Tiles.GetValueOrDefault(offset); + } + } - EnqueueDeltaPressureDamage(ent, - gridAtmosComp, - maxPressure, - maxDelta); - } + GetBulkTileAtmospherePressures(tiles, pressures); - /// - /// A DeltaPressure helper method that retrieves the pressures of all gas mixtures - /// in the given array of s, and stores the results in the - /// provided span. - /// The tiles array length is limited to Atmosphereics.Directions. - /// - /// The tiles array to find the pressures of. - /// The span to store the pressures to - this should be the same length - /// as the tile array. - /// This is for internal use of the DeltaPressure system - - /// it may not be a good idea to use this generically. - private static void GetBulkTileAtmospherePressures(TileAtmosphere?[] tiles, Span pressures) - { - #if DEBUG - // Just in case someone tries to use this method incorrectly. - if (tiles.Length != pressures.Length || tiles.Length != Atmospherics.Directions) - throw new ArgumentException("Length of arrays must be the same and of Atmospherics.Directions length."); - #endif - - // This hardcoded direction limit is stopping goobers from - // overflowing the stack with massive arrays. - // If this method is pulled into a more generic place, - // it should be replaced with method params. - Span mixtVol = stackalloc float[Atmospherics.Directions]; - Span mixtTemp = stackalloc float[Atmospherics.Directions]; - Span mixtMoles = stackalloc float[Atmospherics.Directions]; - Span atmosR = stackalloc float[Atmospherics.Directions]; - - for (var i = 0; i < tiles.Length; i++) - { - if (tiles[i] is not { Air: { } mixture }) + /* + This entity could be airtight but still be able to contain air on the tile it's on (ex. directional windows). + As such, substitute the pressure of the pressure on top of the entity for the directions that it can accept air from. + (Or rather, don't do so for directions that it blocks air from.) + */ + for (var i = 0; i < len; i++) { - pressures[i] = 0f; - - // To prevent any NaN/Div/0 errors, we just bite the bullet - // and set everything to the lowest possible value. - mixtVol[i] = 1; - mixtTemp[i] = 1; - mixtMoles[i] = float.Epsilon; - atmosR[i] = 1; - continue; + var airtight = airtightCompsArr[i]; + if (airtight.NoAirWhenFullyAirBlocked) + continue; + + var currentPos = airtight.LastPosition.Tile; + var localPressure = 0f; + + // microopting one less nullcheck lmao + if (gridAtmosComp.Tiles.TryGetValue(currentPos, out var tile) && tile.Air is { } mixture) + localPressure = mixture.TotalMoles * Atmospherics.R * mixture.Temperature / mixture.Volume; + + var presBase = i * dirs; + for (var j = 0; j < dirs; j++) + { + var direction = (AtmosDirection)(1 << j); + if (!airtight.AirBlockedDirection.IsFlagSet(direction)) + pressures[presBase + j] = localPressure; + } } - mixtVol[i] = mixture.Volume; - mixtTemp[i] = mixture.Temperature; - mixtMoles[i] = mixture.TotalMoles; - atmosR[i] = Atmospherics.R; - } + /* + In order to perform SIMD ops we load the values into opposing pairs, where: + groupA: North, East, South, West + groupB: South, West, North, East + That way NumericsHelpers can just do vectorized operations on them super easily. + */ + for (var i = 0; i < len; i++) + { + var presBase = i * dirs; + var pairBase = i * DeltaPressurePairCount; + for (var j = 0; j < DeltaPressurePairCount; j++) + { + groupA[pairBase + j] = pressures[presBase + j]; + groupB[pairBase + j] = pressures[presBase + j + DeltaPressurePairCount]; + } + } - /* - Retrieval of single tile pressures requires calling a get method for each tile, - which does a bunch of scalar operations. + // Time to get crankin + NumericsHelpers.Max(groupA, groupB, groupMax); + NumericsHelpers.Sub(groupA, groupB); + NumericsHelpers.Abs(groupA); - So we go ahead and batch-retrieve the pressures of all tiles - and process them in bulk. - */ - NumericsHelpers.Multiply(mixtMoles, atmosR); - NumericsHelpers.Multiply(mixtMoles, mixtTemp); - NumericsHelpers.Divide(mixtMoles, mixtVol, pressures); + // Now go through each entity and determine their max pressure & delta pressure. + // Queue for damage if necessary. + for (var i = 0; i < len; i++) + { + var ent = entList[start + i]; + // It is genuinely a massive pain in the ass to handle skipping in the beginning than it is to get that + // microboost from skipping work. As such, just skip at the very end. + if (!_random.Prob(ent.Comp.RandomDamageChance)) + { + SetIsTakingDamageState(ent, false); + continue; + } + + var pairBase = i * DeltaPressurePairCount; + var maxPressure = 0f; + var maxDelta = 0f; + for (var j = 0; j < DeltaPressurePairCount; j++) + { + // I actually did write a HorizontalMax SIMD method but benchmarking showed that + // it was only superior when n > 4. Since we can only compute the max on 4 elements + // we can't take advantage of our array being big right here. + maxPressure = MathF.Max(maxPressure, groupMax[pairBase + j]); + maxDelta = MathF.Max(maxDelta, groupA[pairBase + j]); + } + + EnqueueDeltaPressureDamage(ent, gridAtmosComp, maxPressure, maxDelta); + } + } + finally + { + ArrayPool.Shared.Return(tilesArr); + ArrayPool.Shared.Return(airtightCompsArr); + ArrayPool.Shared.Return(groupAArr); + ArrayPool.Shared.Return(groupBArr); + ArrayPool.Shared.Return(groupMaxArr); + ArrayPool.Shared.Return(pressuresArr); + } } /// @@ -158,7 +210,7 @@ and process them in bulk. /// containing the queue. /// The current absolute pressure being experienced by the entity. /// The current delta pressure being experienced by the entity. - private static void EnqueueDeltaPressureDamage(Entity ent, + private void EnqueueDeltaPressureDamage(Entity ent, GridAtmosphereComponent gridAtmosComp, float pressure, float delta) @@ -167,7 +219,7 @@ private static void EnqueueDeltaPressureDamage(Entity en var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta; if (!aboveMinPressure && !aboveMinDeltaPressure) { - ent.Comp.IsTakingDamage = false; + SetIsTakingDamageState(ent, false); return; } @@ -185,41 +237,21 @@ private static void EnqueueDeltaPressureDamage(Entity en /// The GridAtmosphereComponent to work with. /// The index in the DeltaPressureEntities list to start from. /// The batch size to use for this job. - private sealed class DeltaPressureParallelJob( + private sealed class DeltaPressureParallelBulkJob( AtmosphereSystem system, GridAtmosphereComponent atmosphere, int startIndex, int cvarBatchSize) - : IParallelRobustJob + : IParallelBulkRobustJob { public int BatchSize => cvarBatchSize; - public void Execute(int index) + public void ExecuteRange(int start, int end) { - // The index is relative to the startIndex (because we can pause and resume computation), - // so we need to add it to the startIndex. - var actualIndex = startIndex + index; - - if (actualIndex >= atmosphere.DeltaPressureEntities.Count) - return; - - var ent = atmosphere.DeltaPressureEntities[actualIndex]; - system.ProcessDeltaPressureEntity(ent, atmosphere); + system.ProcessDeltaPressureEntityBulk(atmosphere, start + startIndex, end + startIndex); } } - /// - /// Struct that holds the result of delta pressure damage processing for an entity. - /// This is only created and enqueued when the entity needs to take damage. - /// - /// The entity to deal damage to. - /// The current absolute pressure the entity is experiencing. - /// The current delta pressure the entity is experiencing. - public readonly record struct DeltaPressureDamageResult( - Entity Ent, - float Pressure, - float DeltaPressure); - /// /// Does damage to an entity depending on the pressure experienced by it, based on the /// entity's . @@ -233,8 +265,21 @@ private void PerformDamage(Entity ent, float pressure, f var maxPressureCapped = Math.Min(maxPressure, ent.Comp.MaxEffectivePressure); var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressureCapped); - _damage.TryChangeDamage(ent, appliedDamage, ignoreResistances: true, interruptsDoAfters: false); - ent.Comp.IsTakingDamage = true; + _damage.ChangeDamage(ent.Owner, appliedDamage, ignoreResistances: true, interruptsDoAfters: false); + SetIsTakingDamageState(ent, true); + } + + /// + /// Helper function to prevent spamming clients with dirty events when the damage state hasn't changed. + /// + /// The entity to check. + /// The value to set. + private void SetIsTakingDamageState(Entity ent, bool toSet) + { + if (ent.Comp.IsTakingDamage == toSet) + return; + ent.Comp.IsTakingDamage = toSet; + Dirty(ent); } /// diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs index 0d622f3067a..6ae251dd29b 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.ExcitedGroup.cs @@ -4,148 +4,199 @@ using Robust.Shared.Map.Components; using Robust.Shared.Utility; -namespace Content.Server.Atmos.EntitySystems +namespace Content.Server.Atmos.EntitySystems; + +public sealed partial class AtmosphereSystem { - public sealed partial class AtmosphereSystem + /* + Handles Excited Groups, an optimization routine executed during LINDA + that groups active tiles together. + + Groups of active tiles that have very low mole deltas between them + are dissolved after a cooldown period, performing a final equalization + on all tiles in the group before deactivating them. + + If tiles are so close together in pressure that the final equalization + would result in negligible gas transfer, the group is dissolved without + performing an equalization. + + This prevents LINDA from constantly transferring tiny amounts of gas + between tiles that are already nearly equalized. + */ + + /// + /// Adds a tile to an , resetting the group's cooldowns in the process. + /// + /// The to add the tile to. + /// The to add. + private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile) { - private void ExcitedGroupAddTile(ExcitedGroup excitedGroup, TileAtmosphere tile) - { - DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); - DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!"); - excitedGroup.Tiles.Add(tile); - tile.ExcitedGroup = excitedGroup; - ExcitedGroupResetCooldowns(excitedGroup); - } + DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); + DebugTools.Assert(tile.ExcitedGroup == null, "Tried to add a tile to an excited group when it's already in another one!"); + excitedGroup.Tiles.Add(tile); + tile.ExcitedGroup = excitedGroup; + ExcitedGroupResetCooldowns(excitedGroup); + } - private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile) - { - DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); - DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!"); - tile.ExcitedGroup = null; - excitedGroup.Tiles.Remove(tile); - } + /// + /// Removes a tile from an . + /// + /// The to remove the tile from. + /// The to remove. + private void ExcitedGroupRemoveTile(ExcitedGroup excitedGroup, TileAtmosphere tile) + { + DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); + DebugTools.Assert(tile.ExcitedGroup == excitedGroup, "Tried to remove a tile from an excited group it's not present in!"); + tile.ExcitedGroup = null; + excitedGroup.Tiles.Remove(tile); + } - private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup) + /// + /// Merges two , transferring all tiles from one to the other. + /// The larger group receives the tiles of the smaller group. + /// The smaller group is then disposed of without deactivating its tiles. + /// + /// The of the grid. + /// The first to merge. + /// The second to merge. + private void ExcitedGroupMerge(GridAtmosphereComponent gridAtmosphere, ExcitedGroup ourGroup, ExcitedGroup otherGroup) + { + DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!"); + DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!"); + DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!"); + DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!"); + var ourSize = ourGroup.Tiles.Count; + var otherSize = otherGroup.Tiles.Count; + + ExcitedGroup winner; + ExcitedGroup loser; + + if (ourSize > otherSize) { - DebugTools.Assert(!ourGroup.Disposed, "Excited group is disposed!"); - DebugTools.Assert(!otherGroup.Disposed, "Excited group is disposed!"); - DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(ourGroup), "Grid Atmosphere does not contain Excited Group!"); - DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(otherGroup), "Grid Atmosphere does not contain Excited Group!"); - var ourSize = ourGroup.Tiles.Count; - var otherSize = otherGroup.Tiles.Count; - - ExcitedGroup winner; - ExcitedGroup loser; - - if (ourSize > otherSize) - { - winner = ourGroup; - loser = otherGroup; - } - else - { - winner = otherGroup; - loser = ourGroup; - } - - foreach (var tile in loser.Tiles) - { - tile.ExcitedGroup = winner; - winner.Tiles.Add(tile); - } - - loser.Tiles.Clear(); - ExcitedGroupDispose(gridAtmosphere, loser); - ExcitedGroupResetCooldowns(winner); + winner = ourGroup; + loser = otherGroup; } - - private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup) + else { - DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); - excitedGroup.BreakdownCooldown = 0; - excitedGroup.DismantleCooldown = 0; + winner = otherGroup; + loser = ourGroup; } - private void ExcitedGroupSelfBreakdown( - Entity ent, - ExcitedGroup excitedGroup) + foreach (var tile in loser.Tiles) { - DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); - DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!"); - var combined = new GasMixture(Atmospherics.CellVolume); - - var tileSize = excitedGroup.Tiles.Count; + tile.ExcitedGroup = winner; + winner.Tiles.Add(tile); + } - if (excitedGroup.Disposed) - return; + loser.Tiles.Clear(); + ExcitedGroupDispose(gridAtmosphere, loser); + ExcitedGroupResetCooldowns(winner); + } - if (tileSize == 0) - { - ExcitedGroupDispose(ent.Comp1, excitedGroup); - return; - } + /// + /// Resets the cooldowns of an excited group. + /// + /// The to reset cooldowns for. + private void ExcitedGroupResetCooldowns(ExcitedGroup excitedGroup) + { + DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); + excitedGroup.BreakdownCooldown = 0; + excitedGroup.DismantleCooldown = 0; + } - foreach (var tile in excitedGroup.Tiles) - { - if (tile?.Air == null) - continue; + /// + /// Performs a final equalization on all tiles in an excited group before deactivating it. + /// + /// The grid. + /// The to equalize and dissolve. + private void ExcitedGroupSelfBreakdown( + Entity ent, + ExcitedGroup excitedGroup) + { + DebugTools.Assert(!excitedGroup.Disposed, "Excited group is disposed!"); + DebugTools.Assert(ent.Comp1.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!"); + var combined = new GasMixture(Atmospherics.CellVolume); - Merge(combined, tile.Air); + var tileSize = excitedGroup.Tiles.Count; - if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space) - continue; + if (excitedGroup.Disposed) + return; - combined.Clear(); - break; - } + if (tileSize == 0) + { + ExcitedGroupDispose(ent.Comp1, excitedGroup); + return; + } - combined.Multiply(1 / (float)tileSize); + // Combine all gasses in the group into a single mixture + // for distribution into each individual tile. + foreach (var tile in excitedGroup.Tiles) + { + if (tile?.Air == null) + continue; - foreach (var tile in excitedGroup.Tiles) - { - if (tile?.Air == null) - continue; + Merge(combined, tile.Air); - tile.Air.CopyFrom(combined); - InvalidateVisuals(ent, tile); - } + // If this tile is space and space is all-consuming, the final equalization + // will result in a vacuum, so we can skip the rest of the equalization. + if (!ExcitedGroupsSpaceIsAllConsuming || !tile.Space) + continue; - excitedGroup.BreakdownCooldown = 0; + combined.Clear(); + break; } - /// - /// This de-activates and removes all tiles in an excited group. - /// - private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup) + combined.Multiply(1 / (float)tileSize); + + // Distribute the combined mixture evenly to all tiles in the group. + foreach (var tile in excitedGroup.Tiles) { - foreach (var tile in excitedGroup.Tiles) - { - tile.ExcitedGroup = null; - RemoveActiveTile(gridAtmosphere, tile); - } + if (tile?.Air == null) + continue; - excitedGroup.Tiles.Clear(); + tile.Air.CopyFrom(combined); + InvalidateVisuals(ent, tile); } - /// - /// This removes an excited group without de-activating its tiles. - /// - private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup) + excitedGroup.BreakdownCooldown = 0; + } + + /// + /// Deactivates and removes all tiles from an excited group without performing a final equalization. + /// Used when an excited group is expected to be nearly equalized already to avoid unnecessary processing. + /// + /// The of the grid. + /// The to dissolve. + private void DeactivateGroupTiles(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup) + { + foreach (var tile in excitedGroup.Tiles) { - if (excitedGroup.Disposed) - return; + tile.ExcitedGroup = null; + RemoveActiveTile(gridAtmosphere, tile); + } + + excitedGroup.Tiles.Clear(); + } - DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!"); + /// + /// Removes and disposes of an excited group without performing any final equalization + /// or deactivation of its tiles. + /// + private void ExcitedGroupDispose(GridAtmosphereComponent gridAtmosphere, ExcitedGroup excitedGroup) + { + if (excitedGroup.Disposed) + return; - excitedGroup.Disposed = true; - gridAtmosphere.ExcitedGroups.Remove(excitedGroup); + DebugTools.Assert(gridAtmosphere.ExcitedGroups.Contains(excitedGroup), "Grid Atmosphere does not contain Excited Group!"); - foreach (var tile in excitedGroup.Tiles) - { - tile.ExcitedGroup = null; - } + excitedGroup.Disposed = true; + gridAtmosphere.ExcitedGroups.Remove(excitedGroup); - excitedGroup.Tiles.Clear(); + foreach (var tile in excitedGroup.Tiles) + { + tile.ExcitedGroup = null; } + + excitedGroup.Tiles.Clear(); } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs index c5451866690..1c99c6e1d76 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -3,6 +3,7 @@ using Content.Server.Atmos.Reactions; using Content.Shared.Atmos; using Content.Shared.Atmos.Reactions; +using JetBrains.Annotations; using Robust.Shared.Prototypes; using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; @@ -12,56 +13,37 @@ public sealed partial class AtmosphereSystem { [Dependency] private readonly IPrototypeManager _protoMan = default!; - private GasReactionPrototype[] _gasReactions = Array.Empty(); - private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases]; + private GasReactionPrototype[] _gasReactions = []; /// /// List of gas reactions ordered by priority. /// public IEnumerable GasReactions => _gasReactions; - /// - /// Cached array of gas specific heats. - /// - public float[] GasSpecificHeats => _gasSpecificHeats; - - public string?[] GasReagents = new string[Atmospherics.TotalNumberOfGases]; - - private void InitializeGases() + public override void InitializeGases() { + base.InitializeGases(); + _gasReactions = _protoMan.EnumeratePrototypes().ToArray(); Array.Sort(_gasReactions, (a, b) => b.Priority.CompareTo(a.Priority)); + } - Array.Resize(ref _gasSpecificHeats, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4)); - - for (var i = 0; i < GasPrototypes.Length; i++) - { - _gasSpecificHeats[i] = GasPrototypes[i].SpecificHeat / HeatScale; - GasReagents[i] = GasPrototypes[i].Reagent; - } + public override float GetMass(GasMixture mix) + { + return GetMass(mix.Moles); } - /// - /// Calculates the heat capacity for a gas mixture. - /// - /// The mixture whose heat capacity should be calculated - /// Whether the internal heat capacity scaling should be applied. This should not be - /// used outside of atmospheric related heat transfer. - /// - public float GetHeatCapacity(GasMixture mixture, bool applyScaling) + public override float GetMass(float[] moles) { - var scale = GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable); + Span tmp = stackalloc float[moles.Length]; + NumericsHelpers.Multiply(moles, GasMolarMasses, tmp); - // By default GetHeatCapacityCalculation() has the heat-scale divisor pre-applied. - // So if we want the un-scaled heat capacity, we have to multiply by the scale. - return applyScaling ? scale : scale * HeatScale; + // Conversion of grams to kilograms. + return NumericsHelpers.HorizontalAdd(tmp) * Atmospherics.gToKg; } - private float GetHeatCapacity(GasMixture mixture) - => GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float GetHeatCapacityCalculation(float[] moles, bool space) + protected override float GetHeatCapacityCalculation(float[] moles, bool space) { // Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms. if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f)) @@ -70,34 +52,32 @@ private float GetHeatCapacityCalculation(float[] moles, bool space) } Span tmp = stackalloc float[moles.Length]; - NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp); + NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp); // Adjust heat capacity by speedup, because this is primarily what // determines how quickly gases heat up/cool. return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity); } - /// - /// Return speedup factor for pumped or flow-based devices that depend on MaxTransferRate. - /// - public float PumpSpeedup() + public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon) { - return Speedup; + Span tmp = stackalloc float[Atmospherics.AdjustedNumberOfGases]; + NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp); + return NumericsHelpers.HorizontalAdd(tmp) > epsilon; } - /// - /// Calculates the thermal energy for a gas mixture. - /// - public float GetThermalEnergy(GasMixture mixture) + public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon) { - return mixture.Temperature * GetHeatCapacity(mixture); + Span tmp = stackalloc float[Atmospherics.AdjustedNumberOfGases]; + NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp); + return NumericsHelpers.HorizontalAdd(tmp) > epsilon; } /// - /// Calculates the thermal energy for a gas mixture, using a cached heat capacity value. + /// Return speedup factor for pumped or flow-based devices that depend on MaxTransferRate. /// - public float GetThermalEnergy(GasMixture mixture, float cachedHeatCapacity) + public float PumpSpeedup() { - return mixture.Temperature * cachedHeatCapacity; + return Speedup; } /// @@ -110,28 +90,6 @@ public void AddHeat(GasMixture mixture, float dQ) mixture.Temperature += dT; } - /// - /// Merges the gas mixture into the gas mixture. - /// The gas mixture is not modified by this method. - /// - public void Merge(GasMixture receiver, GasMixture giver) - { - if (receiver.Immutable) return; - - if (MathF.Abs(receiver.Temperature - giver.Temperature) > Atmospherics.MinimumTemperatureDeltaToConsider) - { - var receiverHeatCapacity = GetHeatCapacity(receiver); - var giverHeatCapacity = GetHeatCapacity(giver); - var combinedHeatCapacity = receiverHeatCapacity + giverHeatCapacity; - if (combinedHeatCapacity > Atmospherics.MinimumHeatCapacity) - { - receiver.Temperature = (GetThermalEnergy(giver, giverHeatCapacity) + GetThermalEnergy(receiver, receiverHeatCapacity)) / combinedHeatCapacity; - } - } - - NumericsHelpers.Add(receiver.Moles, giver.Moles); - } - /// /// Divides a source gas mixture into several recipient mixtures, scaled by their relative volumes. Does not /// modify the source gas mixture. Used for pipe network splitting. Note that the total destination volume @@ -360,6 +318,89 @@ 3. Expand both sides. return (-quadraticB + MathF.Sqrt(quadraticB * quadraticB - 4 * quadraticA * quadraticC)) / (2 * quadraticA); } + /// + /// Determines the fraction of gas to be removed and transferred from a source + /// to a target to reach a target pressure + /// in the target . + /// + /// The source that gas will be removed from. + /// This should always be of higher pressure than the second . + /// The target that will increase in pressure + /// to the target pressure. + /// The target mixture's desired pressure to target. + /// A float representing the dimensionless fraction of gas to transfer from the source + /// to the target. This may return negative if you have your mixtures swapped. + /// Note that this method doesn't take into account the heat capacity of the + /// transferred volume causing a pressure rise in the target . + [PublicAPI] + public static float FractionToMaxPressure(GasMixture mix1, GasMixture mix2, float targetPressure) + { + var molesToTransfer = MolesToMaxPressure(mix1, mix2, targetPressure); + return molesToTransfer / mix1.TotalMoles; + } + + /// + /// Determines the number of moles to be removed and transferred from a source + /// to a target to reach a target pressure + /// in the target . + /// + /// The source that gas will be removed from. + /// This should always be of higher pressure than the second . + /// The target that will increase in pressure + /// to the target pressure. + /// The target mixture's desired pressure to target. + /// The difference in moles required to reach the target pressure. + /// Note that this method doesn't take into account the heat capacity of the + /// transferred volume causing a pressure rise in the target . + [PublicAPI] + public static float MolesToMaxPressure(GasMixture mix1, GasMixture mix2, float targetPressure) + { + /* + Calculate the moles required to reach the target pressure. + The formula is derived from the ideal gas law and the + general Richman's law, under the simplification that all the specific heat capacities are equal. + Derivation can also be seen at + https://github.com/space-wizards/space-station-14/pull/35211/files/a0ae787fe07a4e792570f55b49d9dd8038eb6e4d#r1961183456 + TODO ATMOS Make this properly obey the heat capacity change on the target mixture. + + Derivation is as follows. + Assume A is mix1, B is mix2, C is the combined mixture after transfer. + We can express the number of moles in C: + n_C = n_A + n_B + + We can then determine the temperature of C: + T_C = \frac{T_A n_A c_A + T_B n_B c_B}{n_A c_A + n_B c_B} + + Where c_A and c_B are the specific heats of mixtures A and B, respectively. + We can then express the pressure of C: + P_C = \frac{n_C R T_C}{V_C} + + Using the above equations, we can express P_C as follows: + P_C = \frac{(n_A + n_B) R (\frac{T_a n_A + T_B n_B}{n_A + n_B}}{V_C} + + Which can be reduced to: + P_C = \frac{R (T_A n_A + T_B n_B)}{V_C} + + Solving for n_A gives: + n_A = \frac{P_C V_C - R T_B n_B}{R T_A} + + Using the ideal gas law to substitute: + n_A = \frac{P_C V_C - P_B V_B}{R T_A} + + The output volume doesn't change: + V_B = V_C + + So: + n_A = \frac{(P_C - P_B) V_B}{R T_A} + */ + + var delta = targetPressure - mix2.Pressure; + var requiredMoles = (delta * mix2.Volume) / (mix1.Temperature * Atmospherics.R); + + // Return the fraction of moles to transfer. + return requiredMoles; + } + /// /// Determines the number of moles that need to be removed from a to reach a target pressure threshold. /// @@ -441,15 +482,13 @@ public GasCompareResult CompareExchange(GasMixture sample, GasMixture otherSampl return GasCompareResult.NoExchange; } - /// - /// Performs reactions for a given gas mixture on an optional holder. - /// - public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) + [PublicAPI] + public override ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) { var reaction = ReactionResult.NoReaction; var temperature = mixture.Temperature; var energy = GetThermalEnergy(mixture); - + var initAntiNoblium = mixture.GetMoles(Gas.AntiNoblium); // Imperial Atmos Update foreach (var prototype in GasReactions) { if (energy < prototype.MinimumEnergyRequirement || @@ -457,6 +496,8 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) temperature > prototype.MaximumTemperatureRequirement) continue; + if (initAntiNoblium > Atmospherics.MinimumAntiNoblium && prototype.StoppedAntiNoblium) // Imperial Atmos Update + continue; // Imperial Atmos Update var doReaction = true; for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { @@ -480,6 +521,26 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) return reaction; } + /// + /// Adds an array of moles to a . + /// Guards against negative moles by clamping to zero. + /// + /// The to add moles to. + /// The of moles to add. + /// Thrown when the length of the + /// is not the same as the length of the gas array. + [PublicAPI] + public static void AddMolsToMixture(GasMixture mixture, ReadOnlySpan molsToAdd) + { + // Span length should be as long as the length of the gas array. + // Technically this is a redundant check because NumericsHelpers will do the same thing, + // but eh. + ArgumentOutOfRangeException.ThrowIfNotEqual(mixture.Moles.Length, molsToAdd.Length, nameof(mixture.Moles.Length)); + + NumericsHelpers.Add(mixture.Moles, molsToAdd); + NumericsHelpers.Max(mixture.Moles, 0f); + } + public enum GasCompareResult { NoExchange = -2, diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs index 9720cd38a80..0c37bcefaf7 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs @@ -1,4 +1,3 @@ -using Content.Server.Atmos.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Mobs.Components; @@ -129,7 +128,7 @@ private void HighPressureMovements(Entity gridAtmospher return; // Used by ExperiencePressureDifference to correct push/throw directions from tile-relative to physics world. - var gridWorldRotation = _transformSystem.GetWorldRotation(gridAtmosphere); + var gridWorldRotation = XformSystem.GetWorldRotation(gridAtmosphere); // If we're using monstermos, smooth out the yeet direction to follow the flow if (MonstermosEqualization) @@ -248,7 +247,7 @@ public void ExperiencePressureDifference( // TODO: Technically these directions won't be correct but uhh I'm just here for optimisations buddy not to fix my old bugs. if (throwTarget != EntityCoordinates.Invalid) { - var pos = ((_transformSystem.ToMapCoordinates(throwTarget).Position - _transformSystem.GetWorldPosition(xform)).Normalized() + dirVec).Normalized(); + var pos = ((XformSystem.ToMapCoordinates(throwTarget).Position - XformSystem.GetWorldPosition(xform)).Normalized() + dirVec).Normalized(); _physics.ApplyLinearImpulse(uid, pos * moveForce, body: physics); } else diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs index bf92077b741..b7b992ab54d 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Content.Server.Atmos.Components; using Content.Server.Decals; using Content.Shared.Atmos; @@ -10,199 +11,282 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Atmos.EntitySystems +namespace Content.Server.Atmos.EntitySystems; + +public sealed partial class AtmosphereSystem { - public sealed partial class AtmosphereSystem + /* + Handles Hotspots, which are gas-based tile fires that slowly grow and spread + to adjacent tiles if conditions are met. + + You can think of a hotspot as a small flame on a tile that + grows by consuming a fuel and oxidizer from the tile's air, + with a certain volume and temperature. + + This volume grows bigger and bigger as the fire continues, + until it effectively engulfs the entire tile, at which point + it starts spreading to adjacent tiles by radiating heat. + */ + + /// + /// Collection of hotspot sounds to play. + /// + private static readonly ProtoId DefaultHotspotSounds = "AtmosHotspot"; + + [Dependency] private readonly DecalSystem _decalSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + /// + /// Number of cycles the hotspot system must process before it can play another sound + /// on a hotspot. + /// + private const int HotspotSoundCooldownCycles = 200; + + /// + /// Cooldown counter for hotspot sounds. + /// + private int _hotspotSoundCooldown = 0; + + [ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? HotspotSound = new SoundCollectionSpecifier(DefaultHotspotSounds); + + /// + /// Processes a hotspot on a . + /// + /// The grid entity that belongs to the tile to process. + /// The to process. + private void ProcessHotspot( + Entity ent, + TileAtmosphere tile) { - private static readonly ProtoId DefaultHotspotSounds = "AtmosHotspot"; + var gridAtmosphere = ent.Comp1; - [Dependency] private readonly DecalSystem _decalSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; + // Hotspots that have fizzled out are assigned a new Hotspot struct + // with Valid set to false, so we can just check that here in + // one central place instead of manually removing it everywhere. + if (!tile.Hotspot.Valid) + { + gridAtmosphere.HotspotTiles.Remove(tile); + return; + } - private const int HotspotSoundCooldownCycles = 200; + AddActiveTile(gridAtmosphere, tile); - private int _hotspotSoundCooldown = 0; + // Prevent the hotspot from processing on the same cycle it was created (???) + // TODO ATMOS: Is this even necessary anymore? The queue is kept per processing stage + // and is not updated until tne next cycle, so the condition of a hotspot being created + // and processed in the same cycle is impossible. + if (!tile.Hotspot.SkippedFirstProcess) + { + tile.Hotspot.SkippedFirstProcess = true; + return; + } - [ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds); + if (tile.ExcitedGroup != null) + ExcitedGroupResetCooldowns(tile.ExcitedGroup); - private void ProcessHotspot( - Entity ent, - TileAtmosphere tile) + if (tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist || + tile.Hotspot.Volume <= 1f || + tile.Air == null || + !IsMixtureIgnitable(tile.Air)) { - var gridAtmosphere = ent.Comp1; - if (!tile.Hotspot.Valid) - { - gridAtmosphere.HotspotTiles.Remove(tile); - return; - } + tile.Hotspot = new Hotspot(); + InvalidateVisuals(ent, tile); + return; + } - AddActiveTile(gridAtmosphere, tile); + PerformHotspotExposure(tile); - if (!tile.Hotspot.SkippedFirstProcess) - { - tile.Hotspot.SkippedFirstProcess = true; - return; - } + // This tile has now turned into a full-blown tile-fire. + // Start applying fire effects and spreading to adjacent tiles. + if (tile.Hotspot.Bypassing) + { + tile.Hotspot.State = 3; - if(tile.ExcitedGroup != null) - ExcitedGroupResetCooldowns(tile.ExcitedGroup); + var gridUid = ent.Owner; + var tilePos = tile.GridIndices; - if ((tile.Hotspot.Temperature < Atmospherics.FireMinimumTemperatureToExist) || (tile.Hotspot.Volume <= 1f) - || tile.Air == null || tile.Air.GetMoles(Gas.Oxygen) < 0.5f || (tile.Air.GetMoles(Gas.Plasma) < 0.5f && tile.Air.GetMoles(Gas.Tritium) < 0.5f)) - { - tile.Hotspot = new Hotspot(); - InvalidateVisuals(ent, tile); - return; - } + // Get the existing decals on the tile + var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos); - PerformHotspotExposure(tile); + // Count the burnt decals on the tile + var tileBurntDecals = 0; - if (tile.Hotspot.Bypassing) + foreach (var set in tileDecals) { - tile.Hotspot.State = 3; + if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1) + continue; - var gridUid = ent.Owner; - var tilePos = tile.GridIndices; + tileBurntDecals++; - // Get the existing decals on the tile - var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos); + if (tileBurntDecals > 4) + break; + } - // Count the burnt decals on the tile - var tileBurntDecals = 0; + // Add a random burned decal to the tile only if there are less than 4 of them + if (tileBurntDecals < 4) + { + _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], + new EntityCoordinates(gridUid, tilePos), + out _, + cleanable: true); + } - foreach (var set in tileDecals) + if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread) + { + var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale; + foreach (var otherTile in tile.AdjacentTiles) { - if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1) + // TODO ATMOS: This is sus. Suss this out. + // Spread this fire to other tiles by exposing them to a hotspot if air can flow there. + // Unsure as to why this is sus. + if (otherTile == null) continue; - tileBurntDecals++; - - if (tileBurntDecals > 4) - break; - } - - // Add a random burned decal to the tile only if there are less than 4 of them - if (tileBurntDecals < 4) - _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true); - - if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread) - { - var radiatedTemperature = tile.Air.Temperature * Atmospherics.FireSpreadRadiosityScale; - foreach (var otherTile in tile.AdjacentTiles) - { - // TODO ATMOS: This is sus. Suss this out. - if (otherTile == null) - continue; - - if(!otherTile.Hotspot.Valid) - HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume/4); - } + if (!otherTile.Hotspot.Valid) + HotspotExpose(gridAtmosphere, otherTile, radiatedTemperature, Atmospherics.CellVolume / 4); } } - else - { - tile.Hotspot.State = (byte) (tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1); - } - - if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained) - tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature; + } + else + { + // Little baby fire. Set the sprite state based on the current size of the fire. + tile.Hotspot.State = (byte)(tile.Hotspot.Volume > Atmospherics.CellVolume * 0.4f ? 2 : 1); + } - if (_hotspotSoundCooldown++ == 0 && HotspotSound != null) - { - var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices); + if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained) + tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature; - // A few details on the audio parameters for fire. - // The greater the fire state, the lesser the pitch variation. - // The greater the fire state, the greater the volume. - _audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State)); - } + if (_hotspotSoundCooldown++ == 0 && HotspotSound != null) + { + var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices); + + // A few details on the audio parameters for fire. + // The greater the fire state, the lesser the pitch variation. + // The greater the fire state, the greater the volume. + _audio.PlayPvs(HotspotSound, + coordinates, + HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State) + .WithVolume(-5f + 5f * tile.Hotspot.State)); + } - if (_hotspotSoundCooldown > HotspotSoundCooldownCycles) - _hotspotSoundCooldown = 0; + if (_hotspotSoundCooldown > HotspotSoundCooldownCycles) + _hotspotSoundCooldown = 0; - // TODO ATMOS Maybe destroy location here? - } + // TODO ATMOS Maybe destroy location here? + } - private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, TileAtmosphere tile, - float exposedTemperature, float exposedVolume, bool soh = false, EntityUid? sparkSourceUid = null) - { - if (tile.Air == null) - return; + /// + /// Exposes a tile to a hotspot of given temperature and volume, igniting it if conditions are met. + /// + /// The of the grid the tile is on. + /// The to expose. + /// The temperature of the hotspot to expose. + /// You can think of this as exposing a temperature of a flame. + /// The volume of the hotspot to expose. + /// You can think of this as how big the flame is initially. + /// Bigger flames will ramp a fire faster. + /// Whether to "boost" a fire that's currently on the tile already. + /// Does nothing if the tile isn't already a hotspot. + /// This clamps the temperature and volume of the hotspot to the maximum + /// of the provided parameters and whatever's on the tile. + /// Entity that started the exposure for admin logging. + private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, + TileAtmosphere tile, + float exposedTemperature, + float exposedVolume, + bool soh = false, + EntityUid? sparkSourceUid = null) + { + if (tile.Air == null) + return; - var oxygen = tile.Air.GetMoles(Gas.Oxygen); + if (!IsMixtureOxidizer(tile.Air)) + return; - if (oxygen < 0.5f) - return; + var hydrogen = tile.Air.GetMoles(Gas.Hydrogen); ///Imperial Added Hydrogen + var initialAntiNoblium = tile.Air.GetMoles(Gas.AntiNoblium); // Imperial Atmos Update start + if (initialAntiNoblium > Atmospherics.MinimumAntiNoblium) + return; // Imperial Atmos Update end - var plasma = tile.Air.GetMoles(Gas.Plasma); - var tritium = tile.Air.GetMoles(Gas.Tritium); - var hydrogen = tile.Air.GetMoles(Gas.Hydrogen); ///Imperial Added Hydrogen + var isFlammable = IsMixtureFuel(tile.Air); - if (tile.Hotspot.Valid) + if (tile.Hotspot.Valid) + { + if (soh) { - if (soh) + if (isFlammable) { - if (plasma > 0.5f || tritium > 0.5f || hydrogen > 0.5f) ///Imperial Added Hydrogen - { - if (tile.Hotspot.Temperature < exposedTemperature) - tile.Hotspot.Temperature = exposedTemperature; - if (tile.Hotspot.Volume < exposedVolume) - tile.Hotspot.Volume = exposedVolume; - } + tile.Hotspot.Temperature = MathF.Max(tile.Hotspot.Temperature, exposedTemperature); + tile.Hotspot.Volume = MathF.Max(tile.Hotspot.Volume, exposedVolume); } + } + + return; + } - return; + if (exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature && isFlammable) + { + if (sparkSourceUid.HasValue) + { + _adminLog.Add(LogType.Flammable, + LogImpact.High, + $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: " + + $"{tile.Air.ToPrettyString()}"); } - if ((exposedTemperature > Atmospherics.PlasmaMinimumBurnTemperature) && (plasma > 0.5f || tritium > 0.5f || hydrogen > 0.5f)) ///Imperial Added Hydrogen + tile.Hotspot = new Hotspot { - if (sparkSourceUid.HasValue) - _adminLog.Add(LogType.Flammable, LogImpact.High, $"Heat/spark of {ToPrettyString(sparkSourceUid.Value)} caused atmos ignition of gas: {tile.Air.Temperature.ToString():temperature}K - {oxygen}mol Oxygen, {plasma}mol Plasma, {tritium}mol Tritium, {hydrogen}mol Hydrogen"); ///Imperial Added Hydrogen + Volume = exposedVolume * 25f, + Temperature = exposedTemperature, + SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter, + Valid = true, + State = 1 + }; - tile.Hotspot = new Hotspot - { - Volume = exposedVolume * 25f, - Temperature = exposedTemperature, - SkippedFirstProcess = tile.CurrentCycle > gridAtmosphere.UpdateCounter, - Valid = true, - State = 1 - }; - - AddActiveTile(gridAtmosphere, tile); - gridAtmosphere.HotspotTiles.Add(tile); - } + AddActiveTile(gridAtmosphere, tile); + gridAtmosphere.HotspotTiles.Add(tile); } + } - private void PerformHotspotExposure(TileAtmosphere tile) - { - if (tile.Air == null || !tile.Hotspot.Valid) return; + /// + /// Performs hotspot exposure processing on a . + /// + /// The to process. + private void PerformHotspotExposure(TileAtmosphere tile) + { + if (tile.Air == null || !tile.Hotspot.Valid) + return; - tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume*0.95f; + // Determine if the tile has become a full-blown fire if the volume of the fire has effectively reached + // the volume of the tile's air. + tile.Hotspot.Bypassing = tile.Hotspot.SkippedFirstProcess && tile.Hotspot.Volume > tile.Air.Volume * 0.95f; - if (tile.Hotspot.Bypassing) - { - tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate; - tile.Hotspot.Temperature = tile.Air.Temperature; - } - else - { - var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume); - affected.Temperature = tile.Hotspot.Temperature; - React(affected, tile); - tile.Hotspot.Temperature = affected.Temperature; - tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate; - Merge(tile.Air, affected); - } + // If the tile is effectively a full fire, use the tile's air for reactions, don't bother partitioning. + if (tile.Hotspot.Bypassing) + { + tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate; + tile.Hotspot.Temperature = tile.Air.Temperature; + } + // Otherwise, pull out a fraction of the tile's air (the current hotspot volume) to perform reactions on. + else + { + var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume); + affected.Temperature = tile.Hotspot.Temperature; + React(affected, tile); + tile.Hotspot.Temperature = affected.Temperature; + // Scale the fire based on the type of reaction that occured. + tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate; + Merge(tile.Air, affected); + } - var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume); - _entSet.Clear(); - _lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f); + var fireEvent = new TileFireEvent(tile.Hotspot.Temperature, tile.Hotspot.Volume); + _entSet.Clear(); + _lookup.GetLocalEntitiesIntersecting(tile.GridIndex, tile.GridIndices, _entSet, 0f); - foreach (var entity in _entSet) - { - RaiseLocalEvent(entity, ref fireEvent); - } + foreach (var entity in _entSet) + { + RaiseLocalEvent(entity, ref fireEvent); } } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs index 55b38924c0e..d4a70dfbc0f 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.LINDA.cs @@ -117,8 +117,11 @@ private void ProcessCell( private void Archive(TileAtmosphere tile, int fireCount) { if (tile.Air != null) + { + // TODO ATMOS: This is an extremely large hotspot in LINDA, accounting for 1/5th of its time. + // Please make GasMixture a struct or use a FauxGasMixture with an InlineArray to handle copying this sanely. tile.AirArchived = new GasMixture(tile.Air); - + } tile.ArchivedCycle = fireCount; } @@ -129,9 +132,16 @@ private void LastShareCheck(TileAtmosphere tile) switch (tile.LastShare) { + // Refresh this tile's suspension cooldown if it had significant sharing. case > Atmospherics.MinimumAirToSuspend: ExcitedGroupResetCooldowns(tile.ExcitedGroup); break; + + // If this tile moved a very small amount of air, but not enough to matter, + // we set the dismantle cooldown to 0. + // This dissolves the group without performing an equalization as we expect + // the group to be mostly equalized already if we're moving around miniscule + // amounts of air. case > Atmospherics.MinimumMolesDeltaToMove: tile.ExcitedGroup.DismantleCooldown = 0; break; @@ -190,12 +200,25 @@ public float GetHeatCapacityArchived(TileAtmosphere tile) } /// - /// Shares gas between two tiles. Part of LINDA. + /// Performs a share operation between two tiles, sharing both physical gas and temperature. /// + /// The receiving the share. + /// The sharing its air. + /// The number of s next to the receiver that air can flow to. + /// The pressure difference between the two tiles after sharing. + /// LINDA is an FEA-like solver and this method is basically the core of it. + /// In FEA we divide the problem into infinitesimal parts and try to step towards the desired end state: + /// a steady state where all air is equalized between tiles. + /// To do this we share the tiles air between other tiles over time (as well as the temperature). + /// Note that the timestep is actually a cyclestep, so running the cycles faster leads to a faster equalization. + /// Hilarious, I know. public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int atmosAdjacentTurfs) { - if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer || - tileReceiver.AirArchived == null || tileSharer.AirArchived == null) + // TODO ATMOS: Method needs to timestep over deltaTime instead of per cycle + // TODO ATMOS: Method needs to account for adjacent turfs in the situation where air is moving from receiver to sharer. + // See https://github.com/tgstation/tgstation/pull/63785 + if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer || + tileReceiver.AirArchived == null || tileSharer.AirArchived == null) return 0f; var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature; @@ -214,15 +237,16 @@ public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int a var movedMoles = 0f; var absMovedMoles = 0f; - for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { var thisValue = receiver.Moles[i]; var sharerValue = sharer.Moles[i]; var delta = (thisValue - sharerValue) / (atmosAdjacentTurfs + 1); - if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) continue; + if (!(MathF.Abs(delta) >= Atmospherics.GasMinMoles)) + continue; if (absTemperatureDelta > Atmospherics.MinimumTemperatureDeltaToConsider) { - var gasHeatCapacity = delta * GasSpecificHeats[i]; + var gasHeatCapacity = delta * GasMolarHeatCapacities[i]; if (delta > 0) { heatCapacityToSharer += gasHeatCapacity; @@ -233,8 +257,10 @@ public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int a } } - if (!receiver.Immutable) receiver.Moles[i] -= delta; - if (!sharer.Immutable) sharer.Moles[i] += delta; + if (!receiver.Immutable) + receiver.Moles[i] -= delta; + if (!sharer.Immutable) + sharer.Moles[i] += delta; movedMoles += delta; absMovedMoles += MathF.Abs(delta); } @@ -249,16 +275,21 @@ public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int a // Transfer of thermal energy (via changed heat capacity) between self and sharer. if (!receiver.Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity) { - receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.AirArchived.Temperature) + (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature)) / newHeatCapacity; + receiver.Temperature = + (oldHeatCapacity * receiver.Temperature - + heatCapacityToSharer * tileReceiver.AirArchived.Temperature + + heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) / newHeatCapacity; } if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity) { - sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) + (heatCapacityToSharer * tileReceiver.AirArchived.Temperature)) / newSharerHeatCapacity; + sharer.Temperature = + (oldSharerHeatCapacity * sharer.Temperature - + heatCapacitySharerToThis * tileSharer.AirArchived.Temperature + + heatCapacityToSharer * tileReceiver.AirArchived.Temperature) / newSharerHeatCapacity; } // Thermal energy of the system (self and sharer) is unchanged. - if (MathF.Abs(oldSharerHeatCapacity) > Atmospherics.MinimumHeatCapacity) { if (MathF.Abs(newSharerHeatCapacity / oldSharerHeatCapacity - 1) < 0.1) @@ -268,12 +299,25 @@ public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int a } } - if (!(temperatureDelta > Atmospherics.MinimumTemperatureToMove) && - !(MathF.Abs(movedMoles) > Atmospherics.MinimumMolesDeltaToMove)) return 0f; + // If we didn't move enough air or if the temperature difference is too small, + // we don't consider there to be a pressure difference. + // TODO ATMOS: This is a very weird early return, please figure out why this exists because this logic seems to be double checked + // in a lot of other places (ex. HighPressureDelta). + if (!(absTemperatureDelta > Atmospherics.MinimumTemperatureToMove) && + !(MathF.Abs(movedMoles) > Atmospherics.MinimumMolesDeltaToMove)) + return 0f; + var moles = receiver.TotalMoles; var theirMoles = sharer.TotalMoles; - return (tileReceiver.AirArchived.Temperature * (moles + movedMoles)) - (tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume; + /* + To get the pressure delta: + PV = nRT + P = nRT / V + \Delta P = ((n_1 * T_1) - (n_2 * T_2)) * R / V + */ + return (tileReceiver.AirArchived.Temperature * (moles + movedMoles) - + tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume; } /// diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs index 9b8654af6dc..c6567a4fd55 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs @@ -1,7 +1,8 @@ using Content.Server.Atmos.Components; -using Content.Server.Atmos.Piping.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Piping.Components; using Content.Shared.Maps; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -265,6 +266,7 @@ private void UpdateTileAir( tile.ArchivedCycle = 0; tile.LastShare = 0f; tile.Hotspot = new Hotspot(); + NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices); return; } @@ -275,6 +277,10 @@ private void UpdateTileAir( if (data.FixVacuum) GridFixTileVacuum(tile); + + // Since we assigned the tile a new GasMixture we need to tell any devices + // on this tile that the reference has changed. + NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices); } private void QueueRunTiles( @@ -365,7 +371,6 @@ private bool ProcessExcitedGroups( ExcitedGroupSelfBreakdown(ent, excitedGroup); else if (excitedGroup.DismantleCooldown > Atmospherics.ExcitedGroupsDismantleCycles) DeactivateGroupTiles(gridAtmosphere, excitedGroup); - // TODO ATMOS. What is the point of this? why is this only de-exciting the group? Shouldn't it also dismantle it? if (number++ < LagCheckIterations) continue; @@ -481,16 +486,16 @@ private bool ProcessDeltaPressure(Entity= ComponentLifeStage.Stopping || Paused(owner) || !atmosphere.Simulated) continue; - atmosphere.Timer += frameTime; - - if (atmosphere.Timer < AtmosTime) - continue; - - // We subtract it so it takes lost time into account. - atmosphere.Timer -= AtmosTime; - var map = new Entity(xform.MapUid.Value, _mapAtmosQuery.CompOrNull(xform.MapUid.Value)); - switch (atmosphere.State) + var completionState = ProcessAtmosphere(ent, map, frameTime); + + switch (completionState) { - case AtmosphereProcessingState.Revalidate: - if (!ProcessRevalidate(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - - // Next state depends on whether monstermos equalization is enabled or not. - // Note: We do this here instead of on the tile equalization step to prevent ending it early. - // Therefore, a change to this CVar might only be applied after that step is over. - atmosphere.State = MonstermosEqualization - ? AtmosphereProcessingState.TileEqualize - : AtmosphereProcessingState.ActiveTiles; - continue; - case AtmosphereProcessingState.TileEqualize: - if (!ProcessTileEqualize(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.ActiveTiles; - continue; - case AtmosphereProcessingState.ActiveTiles: - if (!ProcessActiveTiles(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - // Next state depends on whether excited groups are enabled or not. - atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta; - continue; - case AtmosphereProcessingState.ExcitedGroups: - if (!ProcessExcitedGroups(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.HighPressureDelta; - continue; - case AtmosphereProcessingState.HighPressureDelta: - if (!ProcessHighPressureDelta((ent, ent))) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = DeltaPressureDamage - ? AtmosphereProcessingState.DeltaPressure - : AtmosphereProcessingState.Hotspots; - continue; - case AtmosphereProcessingState.DeltaPressure: - if (!ProcessDeltaPressure(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.Hotspots; - continue; - case AtmosphereProcessingState.Hotspots: - if (!ProcessHotspots(ent)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - // Next state depends on whether superconduction is enabled or not. - // Note: We do this here instead of on the tile equalization step to prevent ending it early. - // Therefore, a change to this CVar might only be applied after that step is over. - atmosphere.State = Superconduction - ? AtmosphereProcessingState.Superconductivity - : AtmosphereProcessingState.PipeNet; - continue; - case AtmosphereProcessingState.Superconductivity: - if (!ProcessSuperconductivity(atmosphere)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.PipeNet; - continue; - case AtmosphereProcessingState.PipeNet: - if (!ProcessPipeNets(atmosphere)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.AtmosDevices; + case AtmosphereProcessingCompletionState.Return: + return; + case AtmosphereProcessingCompletionState.Continue: continue; - case AtmosphereProcessingState.AtmosDevices: - if (!ProcessAtmosDevices(ent, map)) - { - atmosphere.ProcessingPaused = true; - return; - } - - atmosphere.ProcessingPaused = false; - atmosphere.State = AtmosphereProcessingState.Revalidate; - - // We reached the end of this atmosphere's update tick. Break out of the switch. + case AtmosphereProcessingCompletionState.Finished: break; } - - // And increase the update counter. - atmosphere.UpdateCounter++; } // We finished processing all atmospheres successfully, therefore we won't be paused next tick. _simulationPaused = false; } + + /// + /// Processes a through its processing stages. + /// + /// The entity to process. + /// The belonging to the + /// 's map. + /// The elapsed time since the last frame. + /// An that represents the completion state. + private AtmosphereProcessingCompletionState ProcessAtmosphere(Entity ent, + Entity mapAtmosphere, + float frameTime) + { + // They call me the deconstructor the way i be deconstructing it + // and by it, i mean... my entity + var (owner, atmosphere, visuals, grid, xform) = ent; + + atmosphere.Timer += frameTime; + + if (atmosphere.Timer < AtmosTime) + return AtmosphereProcessingCompletionState.Continue; + + // We subtract it so it takes lost time into account. + atmosphere.Timer -= AtmosTime; + + switch (atmosphere.State) + { + case AtmosphereProcessingState.Revalidate: + if (!ProcessRevalidate(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + + // Next state depends on whether monstermos equalization is enabled or not. + // Note: We do this here instead of on the tile equalization step to prevent ending it early. + // Therefore, a change to this CVar might only be applied after that step is over. + atmosphere.State = MonstermosEqualization + ? AtmosphereProcessingState.TileEqualize + : AtmosphereProcessingState.ActiveTiles; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.TileEqualize: + if (!ProcessTileEqualize(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.ActiveTiles; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.ActiveTiles: + if (!ProcessActiveTiles(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + // Next state depends on whether excited groups are enabled or not. + atmosphere.State = ExcitedGroups ? AtmosphereProcessingState.ExcitedGroups : AtmosphereProcessingState.HighPressureDelta; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.ExcitedGroups: + if (!ProcessExcitedGroups(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.HighPressureDelta; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.HighPressureDelta: + if (!ProcessHighPressureDelta((ent, ent))) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = DeltaPressureDamage + ? AtmosphereProcessingState.DeltaPressure + : AtmosphereProcessingState.Hotspots; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.DeltaPressure: + if (!ProcessDeltaPressure(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.Hotspots; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.Hotspots: + if (!ProcessHotspots(ent)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + // Next state depends on whether superconduction is enabled or not. + // Note: We do this here instead of on the tile equalization step to prevent ending it early. + // Therefore, a change to this CVar might only be applied after that step is over. + atmosphere.State = Superconduction + ? AtmosphereProcessingState.Superconductivity + : AtmosphereProcessingState.PipeNet; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.Superconductivity: + if (!ProcessSuperconductivity(atmosphere)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.PipeNet; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.PipeNet: + if (!ProcessPipeNets(atmosphere)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.AtmosDevices; + return AtmosphereProcessingCompletionState.Continue; + case AtmosphereProcessingState.AtmosDevices: + if (!ProcessAtmosDevices(ent, mapAtmosphere)) + { + atmosphere.ProcessingPaused = true; + return AtmosphereProcessingCompletionState.Return; + } + + atmosphere.ProcessingPaused = false; + atmosphere.State = AtmosphereProcessingState.Revalidate; + + // We reached the end of this atmosphere's update tick. Break out of the switch. + break; + } + + atmosphere.UpdateCounter++; + + return AtmosphereProcessingCompletionState.Finished; + } } - public enum AtmosphereProcessingState : byte + /// + /// An enum representing the completion state of a 's processing steps. + /// The processing of a spans over multiple stages and sticks, + /// with the method handling the processing having multiple return types. + /// + public enum AtmosphereProcessingCompletionState : byte { - Revalidate, - TileEqualize, - ActiveTiles, - ExcitedGroups, - HighPressureDelta, - DeltaPressure, - Hotspots, - Superconductivity, - PipeNet, - AtmosDevices, - NumStates + /// + /// Method is returning, ex. due to delegating processing to the next tick. + /// + Return, + + /// + /// Method is continuing, ex. due to finishing a single processing stage. + /// + Continue, + + /// + /// Method is finished with the GridAtmosphere. + /// + Finished, } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs index 46a554054b4..2b5979917ab 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Superconductivity.cs @@ -1,5 +1,5 @@ -using Content.Server.Atmos.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Robust.Shared.Map.Components; namespace Content.Server.Atmos.EntitySystems diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs index 36355d7ba03..9c39c4dbb24 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs @@ -1,19 +1,23 @@ using System.Runtime.CompilerServices; -using Content.Server.Atmos.Components; -using Content.Server.Maps; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; -using Content.Shared.Maps; -using Robust.Shared.Map; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Piping.Components; using Robust.Shared.Map.Components; namespace Content.Server.Atmos.EntitySystems; public partial class AtmosphereSystem { + /* + Partial class that stores miscellaneous utility methods for Atmospherics. + */ + /// - /// Gets the particular price of an air mixture. + /// Gets the particular price of a . /// + /// The to get the price of. + /// The price of the gas mixture. public double GetPrice(GasMixture mixture) { float basePrice = 0; // moles of gas * price/mole @@ -26,7 +30,7 @@ public double GetPrice(GasMixture mixture) maxComponent = Math.Max(maxComponent, mixture.Moles[i]); } - // Pay more for gas canisters that are more pure + // Pay more for gas canisters that are purer float purity = 1; if (totalMoles > 0) { @@ -36,12 +40,32 @@ public double GetPrice(GasMixture mixture) return basePrice * purity; } + /// + /// Marks a tile's visual overlay as needing to be redetermined. + /// + /// A tile's overlay (how it looks like, ex. water vapor's texture) + /// is determined via determining how much gas there is on the tile. + /// This is expensive to do for every tile/gas that may have a custom overlay, + /// so its done once and only updated when it needs to be updated. + /// + /// The grid the tile is on. + /// The tile to invalidate. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InvalidateVisuals(Entity grid, Vector2i tile) { _gasTileOverlaySystem.Invalidate(grid, tile); } + /// + /// Marks a tile's visual overlay as needing to be redetermined. + /// + /// A tile's overlay (how it looks like, ex. water vapor's texture) + /// is determined via determining how much gas there is on the tile. + /// This is expensive to do for every tile/gas that may have a custom overlay, + /// so its done once and only updated when it needs to be updated. + /// + /// The grid the tile is on. + /// The tile to invalidate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void InvalidateVisuals( Entity ent, @@ -51,7 +75,7 @@ private void InvalidateVisuals( } /// - /// Gets the volume in liters for a number of tiles, on a specific grid. + /// Gets the volume in liters for a number of tiles, on a specific grid. /// /// The grid in question. /// The amount of tiles. @@ -61,9 +85,18 @@ private float GetVolumeForTiles(MapGridComponent mapGrid, int tiles = 1) return Atmospherics.CellVolume * mapGrid.TileSize * tiles; } - public readonly record struct AirtightData(AtmosDirection BlockedDirections, bool NoAirWhenBlocked, - bool FixVacuum); - + /// + /// Updates the for a + /// immediately. + /// + /// This method is extremely important if you are doing something in Atmospherics + /// that is time-sensitive! is cached and invalidated on + /// a cycle, so airtight changes performed during or after an invalidation will + /// not take effect until the next Atmospherics tick! + /// The entity the grid is on. + /// The the tile is on. + /// The the tile is on. + /// The to update. private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, MapGridComponent grid, TileAtmosphere tile) { var oldBlocked = tile.AirtightData.BlockedDirections; @@ -76,6 +109,15 @@ private void UpdateAirtightData(EntityUid uid, GridAtmosphereComponent atmos, Ma ExcitedGroupDispose(atmos, tile.ExcitedGroup); } + /// + /// Retrieves current for a tile on a grid. + /// This is determined on-the-fly, not from cached data, so it will reflect + /// changes done in the current Atmospherics tick. + /// + /// The entity the grid is on. + /// The the tile is on. + /// The indices of the tile. + /// The current for the tile. private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vector2i tile) { var blockedDirs = AtmosDirection.Invalid; @@ -103,7 +145,7 @@ private AirtightData GetAirtightData(EntityUid uid, MapGridComponent grid, Vecto } /// - /// Pries a tile in a grid. + /// Pries a tile in a grid. /// /// The grid in question. /// The indices of the tile. @@ -114,4 +156,21 @@ private void PryTile(Entity mapGrid, Vector2i tile) _tile.PryTile(tileRef); } + + /// + /// Notifies all subscribing entities on a particular tile that the tile has changed. + /// Atmos devices may store references to tiles, so this is used to properly resync devices + /// after a significant atmos change on that tile, for example a tile getting a new . + /// + /// The grid atmosphere entity. + /// The tile to check for devices on. + private void NotifyDeviceTileChanged(Entity ent, Vector2i tile) + { + var inTile = _mapSystem.GetAnchoredEntities(ent.Owner, ent.Comp2, tile); + var ev = new AtmosDeviceTileChangedEvent(); + foreach (var uid in inTile) + { + RaiseLocalEvent(uid, ref ev); + } + } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs index 00b7e169134..f02928b8e39 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs @@ -1,8 +1,12 @@ +using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; using Content.Server.Fluids.EntitySystems; using Content.Server.NodeContainer.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Damage.Systems; using Content.Shared.Decals; using Content.Shared.Doors.Components; using Content.Shared.Maps; @@ -13,8 +17,6 @@ using Robust.Shared.Map; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; -using System.Linq; -using Content.Shared.Damage; using Robust.Shared.Threading; namespace Content.Server.Atmos.EntitySystems; @@ -35,7 +37,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem [Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly TileSystem _tile = default!; [Dependency] private readonly MapSystem _map = default!; [Dependency] public readonly PuddleSystem Puddle = default!; @@ -64,8 +65,8 @@ public override void Initialize() InitializeGridAtmosphere(); InitializeMap(); - _mapAtmosQuery = GetEntityQuery(); _atmosQuery = GetEntityQuery(); + _mapAtmosQuery = GetEntityQuery(); _airtightQuery = GetEntityQuery(); _firelockQuery = GetEntityQuery(); @@ -125,6 +126,6 @@ public override void Update(float frameTime) private void CacheDecals() { - _burntDecals = _protoMan.EnumeratePrototypes().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray(); + _burntDecals = ProtoMan.EnumeratePrototypes().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray(); } } diff --git a/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs b/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs index f1ffa0dafae..bc5621118d4 100644 --- a/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AutomaticAtmosSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.Atmos.Components; using Content.Server.Shuttles.Systems; +using Content.Shared.Atmos.Components; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Events; diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs index ec508790ba8..6f881a32e70 100644 --- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs +++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs @@ -3,7 +3,8 @@ using Content.Server.Atmos.Components; using Content.Shared.Alert; using Content.Shared.Atmos; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Inventory; @@ -63,17 +64,17 @@ private void OnUpdateResistance(EntityUid uid, PressureProtectionComponent press private void OnPressureProtectionEquipped(EntityUid uid, PressureProtectionComponent pressureProtection, GotEquippedEvent args) { - if (TryComp(args.Equipee, out var barotrauma) && barotrauma.ProtectionSlots.Contains(args.Slot)) + if (TryComp(args.EquipTarget, out var barotrauma) && barotrauma.ProtectionSlots.Contains(args.Slot)) { - UpdateCachedResistances(args.Equipee, barotrauma); + UpdateCachedResistances(args.EquipTarget, barotrauma); } } private void OnPressureProtectionUnequipped(EntityUid uid, PressureProtectionComponent pressureProtection, GotUnequippedEvent args) { - if (TryComp(args.Equipee, out var barotrauma) && barotrauma.ProtectionSlots.Contains(args.Slot)) + if (TryComp(args.EquipTarget, out var barotrauma) && barotrauma.ProtectionSlots.Contains(args.Slot)) { - UpdateCachedResistances(args.Equipee, barotrauma); + UpdateCachedResistances(args.EquipTarget, barotrauma); } } @@ -210,9 +211,10 @@ public override void Update(float frameTime) while (enumerator.MoveNext(out var uid, out var barotrauma, out var damageable)) { var totalDamage = FixedPoint2.Zero; + var damageSpecifier = _damageableSystem.GetAllDamage((uid, damageable)); foreach (var (barotraumaDamageType, _) in barotrauma.Damage.DamageDict) { - if (!damageable.Damage.DamageDict.TryGetValue(barotraumaDamageType, out var damage)) + if (!damageSpecifier.DamageDict.TryGetValue(barotraumaDamageType, out var damage)) continue; totalDamage += damage; } diff --git a/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs index a6cbec0d0c8..669735b90da 100644 --- a/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs +++ b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Atmos.Components; -using Content.Shared.Examine; -using Robust.Shared.Map.Components; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; namespace Content.Server.Atmos.EntitySystems; @@ -14,10 +14,9 @@ namespace Content.Server.Atmos.EntitySystems; /// This system handles the adding and removing of entities to a processing list, /// as well as any field changes via the API. /// -public sealed class DeltaPressureSystem : EntitySystem +public sealed partial class DeltaPressureSystem : SharedDeltaPressureSystem { [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly SharedMapSystem _map = default!; public override void Initialize() { @@ -25,30 +24,16 @@ public override void Initialize() SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnComponentShutdown); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnMoveEvent); - SubscribeLocalEvent(OnGridChanged); } - private void OnMoveEvent(Entity ent, ref MoveEvent args) - { - var xform = Transform(ent); - // May move off-grid, so, might as well protect against that. - if (!TryComp(xform.GridUid, out var mapGridComponent)) - { - return; - } - - ent.Comp.CurrentPosition = _map.CoordinatesToTile(xform.GridUid.Value, mapGridComponent, args.NewPosition); - } - private void OnComponentInit(Entity ent, ref ComponentInit args) { var xform = Transform(ent); if (xform.GridUid == null) return; + EnsureComp(ent); _atmosphereSystem.TryAddDeltaPressureEntity(xform.GridUid.Value, ent); } @@ -61,12 +46,6 @@ private void OnComponentShutdown(Entity ent, ref Compone _atmosphereSystem.TryRemoveDeltaPressureEntity(ent.Comp.GridUid.Value, ent); } - private void OnExamined(Entity ent, ref ExaminedEvent args) - { - if (ent.Comp.IsTakingDamage) - args.PushMarkup(Loc.GetString("window-taking-damage")); - } - private void OnGridChanged(Entity ent, ref GridUidChangedEvent args) { if (args.OldGrid != null) diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 424f52ed581..e50f91429ae 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -1,14 +1,13 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; using Content.Server.Stunnable; -using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; using Content.Server.Damage.Components; using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.IgnitionSource; using Content.Shared.Interaction; @@ -23,12 +22,13 @@ using Content.Shared.Toggleable; using Content.Shared.Weapons.Melee.Events; using Content.Shared.FixedPoint; -using Content.Shared.Hands; +using Content.Shared.Temperature.Components; using Robust.Server.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Server.Atmos.EntitySystems { @@ -49,14 +49,12 @@ public sealed class FlammableSystem : EntitySystem [Dependency] private readonly UseDelaySystem _useDelay = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IGameTiming _timing = default!; private EntityQuery _inventoryQuery; private EntityQuery _physicsQuery; - // This should probably be moved to the component, requires a rewrite, all fires tick at the same time - private const float UpdateTime = 1f; - - private float _timer; + private static readonly TimeSpan UpdateTime = TimeSpan.FromSeconds(1); private readonly Dictionary, float> _fireEvents = new(); @@ -138,6 +136,8 @@ private void IgniteOnCollide(EntityUid uid, IgniteOnCollideComponent component, private void OnMapInit(EntityUid uid, FlammableComponent component, MapInitEvent args) { + component.NextUpdate = _timing.CurTime + UpdateTime; + // Sets up a fixture for flammable collisions. // TODO: Should this be generalized into a general non-hard 'effects' fixture or something? I can't think of other use cases for it. // This doesn't seem great either (lots more collisions generated) but there isn't a better way to solve it either that I can think of. @@ -145,8 +145,8 @@ private void OnMapInit(EntityUid uid, FlammableComponent component, MapInitEvent if (!TryComp(uid, out var body)) return; - _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, hard: false, - collisionMask: (int) CollisionGroup.FullTileLayer, body: body); + _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, density: 0, + hard: false, collisionMask: (int)CollisionGroup.FullTileLayer, body: body); } private void OnInteractUsing(EntityUid uid, FlammableComponent flammable, InteractUsingEvent args) @@ -225,20 +225,14 @@ private void OnCollide(EntityUid uid, FlammableComponent flammable, ref StartCol mass2 = otherPhys.Mass; } - // when the thing on fire is more massive than the other, the following happens: - // - the thing on fire loses a small number of firestacks - // - the other thing gains a large number of firestacks - // so a person on fire engulfs a mouse, but an engulfed mouse barely does anything to a person - var total = mass1 + mass2; - var avg = (flammable.FireStacks + otherFlammable.FireStacks) / total; - - // swap the entity losing stacks depending on whichever has the most firestack kilos - var (src, dest) = flammable.FireStacks * mass1 > otherFlammable.FireStacks * mass2 - ? (-1f, 1f) - : (1f, -1f); - // bring each entity to the same firestack mass, firestacks being scaled by the other's mass - AdjustFireStacks(uid, src * avg * mass2, flammable, ignite: true); - AdjustFireStacks(otherUid, dest * avg * mass1, otherFlammable, ignite: true); + // Get the average of both entity's firestacks * mass + // Then for each entity, we divide the average by their mass and set their firestacks to that value + // An entity with a higher mass will lose some fire and transfer it to the one with lower mass. + var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / 2f; + + // bring each entity to the same firestack mass, firestack amount is scaled by the inverse of the entity's mass + SetFireStacks(uid, avg / mass1, flammable, ignite: true); + SetFireStacks(otherUid, avg / mass2, otherFlammable, ignite: true); } private void OnIsHot(EntityUid uid, FlammableComponent flammable, IsHotEvent args) @@ -371,7 +365,7 @@ private void OnDamageChanged(EntityUid uid, IgniteOnHeatDamageComponent componen if (args.DamageDelta.DamageDict.TryGetValue("Heat", out FixedPoint2 value)) { // Make sure the value is greater than the threshold - if(value <= component.Threshold) + if (value <= component.Threshold) return; // Ignite that sucker @@ -388,21 +382,13 @@ public void Resist(EntityUid uid, if (!Resolve(uid, ref flammable)) return; - if (!flammable.OnFire || !_actionBlockerSystem.CanInteract(uid, null) || flammable.Resisting) + if (!flammable.OnFire || flammable.Resisting || !_actionBlockerSystem.CanInteract(uid, null)) return; - flammable.Resisting = true; + flammable.ResistCompleteTime = _timing.CurTime + flammable.ResistTime; _popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid); - _stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(2f)); - - // TODO FLAMMABLE: Make this not use TimerComponent... - uid.SpawnTimer(2000, () => - { - flammable.Resisting = false; - flammable.FireStacks -= 1f; - UpdateAppearance(uid, flammable); - }); + _stunSystem.TryUpdateParalyzeDuration(uid, flammable.ResistTime); } public override void Update(float frameTime) @@ -422,17 +408,21 @@ public override void Update(float frameTime) } _fireEvents.Clear(); - _timer += frameTime; - - if (_timer < UpdateTime) - return; - - _timer -= UpdateTime; + var curTime = _timing.CurTime; // TODO: This needs cleanup to take off the crust from TemperatureComponent and shit. var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var flammable, out _)) { + if (curTime < flammable.NextUpdate) + continue; + + flammable.NextUpdate += UpdateTime; + + // Check if we finished resisting. + if (curTime > flammable.ResistCompleteTime) + flammable.ResistCompleteTime = null; + // Slowly dry ourselves off if wet. if (flammable.FireStacks < 0) { @@ -473,7 +463,7 @@ public override void Update(float frameTime) _damageableSystem.TryChangeDamage(uid, flammable.Damage * flammable.FireStacks * ev.Multiplier, interruptsDoAfters: false); - AdjustFireStacks(uid, flammable.FirestackFade * (flammable.Resisting ? 10f : 1f), flammable, flammable.OnFire); + AdjustFireStacks(uid, flammable.FirestackFade * (flammable.Resisting ? 15f : 1f), flammable, flammable.OnFire); } else { diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs index 3cbe6575bf2..9dcb4d13a22 100644 --- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs @@ -1,16 +1,13 @@ using System.Linq; using Content.Server.Atmos.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; using Content.Server.Popups; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; using Content.Shared.NodeContainer; using JetBrains.Annotations; using Robust.Server.GameObjects; -using static Content.Shared.Atmos.Components.GasAnalyzerComponent; namespace Content.Server.Atmos.EntitySystems; @@ -103,6 +100,7 @@ private void DisableAnalyzer(Entity entity, EntityUid? use _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), user.Value, user.Value); entity.Comp.Enabled = false; + entity.Comp.User = null; Dirty(entity); _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); RemCompDeferred(entity.Owner); @@ -141,15 +139,15 @@ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nul return false; // check if the user has walked away from what they scanned - if (component.Target.HasValue) + if (component.Target.HasValue && component.User.HasValue) { // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use // this code to determine if the object is still generally in range so that the check is consistent with the code // in OnAfterInteract() and also consistent with interaction code in general. - if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null))) + if (!_interactionSystem.InRangeUnobstructed((component.User.Value, null), (component.Target.Value, null))) { - if (component.User is { } userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId); + if (component.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), component.User.Value, component.User.Value); component.Target = null; } @@ -159,16 +157,8 @@ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nul // Fetch the environmental atmosphere around the scanner. This must be the first entry var tileMixture = _atmo.GetContainingMixture(uid, true); - if (tileMixture != null) - { - gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature, - GenerateGasEntryArray(tileMixture))); - } - else - { - // No gases were found - gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f)); - } + var tileMixtureName = Loc.GetString("gas-analyzer-window-environment-tab-label"); + gasMixList.Add(GenerateGasMixEntry(tileMixtureName, tileMixture)); var deviceFlipped = false; if (component.Target != null) @@ -192,7 +182,7 @@ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nul { if (mixes.Item2 != null) { - gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2))); + gasMixList.Add(GenerateGasMixEntry(mixes.Item1, mixes.Item2)); validTarget = true; } } @@ -215,7 +205,7 @@ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nul var pipeAir = pipeNode.Air.Clone(); pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume); pipeAir.Volume = pipeNode.Volume; - gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir))); + gasMixList.Add(GenerateGasMixEntry(pair.Key, pipeAir)); validTarget = true; } } @@ -242,6 +232,23 @@ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nul return true; } + /// + /// Generates a GasMixEntry for a given GasMixture + /// + public GasMixEntry GenerateGasMixEntry(string name, GasMixture? mixture) + { + if (mixture == null) + return new GasMixEntry(name, 0, 0, 0); + + return new GasMixEntry( + name, + mixture.Volume, + mixture.Pressure, + mixture.Temperature, + GenerateGasEntryArray(mixture) + ); + } + /// /// Generates a GasEntry array for a given GasMixture /// @@ -249,18 +256,17 @@ private GasEntry[] GenerateGasEntryArray(GasMixture? mixture) { var gases = new List(); + if (mixture == null) + return []; + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) { - var gas = _atmo.GetGas(i); + var gas = (Gas)i; - if (mixture?[i] <= UIMinMoles) + if (mixture[i] <= UIMinMoles) continue; - if (mixture != null) - { - var gasName = Loc.GetString(gas.Name); - gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); - } + gases.Add(new GasEntry(gas, mixture[i])); } var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); diff --git a/Content.Server/Atmos/EntitySystems/GasMinerSystem.cs b/Content.Server/Atmos/EntitySystems/GasMinerSystem.cs index f7a3b9eed8a..3042fba0083 100644 --- a/Content.Server/Atmos/EntitySystems/GasMinerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasMinerSystem.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using Content.Server.Atmos.Piping.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Piping.Components; using JetBrains.Annotations; using Robust.Server.GameObjects; diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs index 420db3f7325..fba6c145ae1 100644 --- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs @@ -1,238 +1,169 @@ -using Content.Server.Explosion.EntitySystems; +using System.Numerics; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using Content.Shared.Cargo; +using Content.Shared.Popups; using Content.Shared.Throwing; using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; +using Robust.Shared.Physics.Systems; using Robust.Shared.Random; -using Robust.Shared.Configuration; -using Content.Shared.CCVar; -namespace Content.Server.Atmos.EntitySystems -{ - [UsedImplicitly] - public sealed class GasTankSystem : SharedGasTankSystem - { - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly ExplosionSystem _explosions = default!; - [Dependency] private readonly SharedAudioSystem _audioSys = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ThrowingSystem _throwing = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - - private const float TimerDelay = 0.5f; - private float _timer = 0f; - private const float MinimumSoundValvePressure = 10.0f; - private float _maxExplosionRange; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnParentChange); - SubscribeLocalEvent(OnAnalyzed); - SubscribeLocalEvent(OnGasTankPrice); - Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true); - } +namespace Content.Server.Atmos.EntitySystems; - private void UpdateMaxRange(float value) - { - _maxExplosionRange = value; - } - - public override void UpdateUserInterface(Entity ent) - { - var (owner, component) = ent; - _ui.SetUiState(owner, SharedGasTankUiKey.Key, - new GasTankBoundUserInterfaceState - { - TankPressure = component.Air?.Pressure ?? 0, - }); - } - - private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args) - { - // When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor. - // So this is a shitty fix, where the parent check is just delayed. But this really needs to get fixed - // properly at some point. - component.CheckUser = true; - } +[UsedImplicitly] +public sealed class GasTankSystem : SharedGasTankSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; - public override void Update(float frameTime) - { - base.Update(frameTime); + private const float MinimumSoundValvePressure = 21.3f; // Arbitrary number - _timer += frameTime; + private const float ReleaseArea = 0.0005f; // About 5cm^2 - if (_timer < TimerDelay) - return; + // A vector bias for throwing our gas tanks in radians. Averages about -43 degrees since the sprite is at a 45-degree angle. + private static readonly Vector2 ThrowVector = new (-1.0f, -0.5f); - _timer -= TimerDelay; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnParentChange); + SubscribeLocalEvent(OnAnalyzed); + SubscribeLocalEvent(OnGasTankPrice); + } - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp)) - { - var gasTank = (uid, comp); - if (comp.IsValveOpen && !comp.IsLowPressure && comp.OutputPressure > 0) - { - ReleaseGas(gasTank); - } - - if (comp.CheckUser) - { - comp.CheckUser = false; - if (Transform(uid).ParentUid != comp.User) - { - DisconnectFromInternals(gasTank); - continue; - } - } - - if (comp.Air != null) - { - _atmosphereSystem.React(comp.Air, comp); - } - - CheckStatus(gasTank); - - if ((comp.IsConnected || comp.IsValveOpen) && _ui.IsUiOpen(uid, SharedGasTankUiKey.Key)) - { - UpdateUserInterface(gasTank); - } - } + protected override void DeviceUpdated(Entity entity, ref AtmosDeviceUpdateEvent args) + { + // Release gas if valve is open + // Disconnect from internals if valve is open + if (entity.Comp.ReleaseValveOpen) + { + DisconnectFromInternals(entity); + ReleaseGas(entity, args.dt); } - - private void ReleaseGas(Entity gasTank) + else if (entity.Comp.CheckUser) { - var removed = RemoveAirVolume(gasTank, gasTank.Comp.ValveOutputRate * TimerDelay); - var environment = _atmosphereSystem.GetContainingMixture(gasTank.Owner, false, true); - if (environment != null) + entity.Comp.CheckUser = false; + if (Transform(entity).ParentUid != entity.Comp.User) { - _atmosphereSystem.Merge(environment, removed); + DisconnectFromInternals(entity); } - var strength = removed.TotalMoles * MathF.Sqrt(removed.Temperature); - var dir = _random.NextAngle().ToWorldVec(); - _throwing.TryThrow(gasTank, dir * strength, strength); - if (gasTank.Comp.OutputPressure >= MinimumSoundValvePressure) - _audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank); } - public GasMixture? RemoveAir(Entity gasTank, float amount) - { - var gas = gasTank.Comp.Air?.Remove(amount); - CheckStatus(gasTank); - return gas; - } + Atmos.React(entity.Comp.Air, entity.Comp); - public GasMixture RemoveAirVolume(Entity gasTank, float volume) - { - var component = gasTank.Comp; - if (component.Air == null) - return new GasMixture(volume); + if ((entity.Comp.IsConnected || entity.Comp.ReleaseValveOpen) && UI.IsUiOpen(entity.Owner, SharedGasTankUiKey.Key)) + UpdateUserInterface(entity); + } + + public override void UpdateUserInterface(Entity ent) + { + var (owner, component) = ent; + UI.SetUiState(owner, + SharedGasTankUiKey.Key, + new GasTankBoundUserInterfaceState + { + TankPressure = component.Air.Pressure + }); + } - var molesNeeded = component.OutputPressure * volume / (Atmospherics.R * component.Air.Temperature); + private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args) + { + // When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor. + // So this is a shitty fix, where the parent check is just delayed. But this really needs to get fixed + // properly at some point. + component.CheckUser = true; + } - var air = RemoveAir(gasTank, molesNeeded); + /// + /// Tries to release gas through the pressure release valve. + /// + /// The gas tank entity releasing gas + /// The amount of time since the last update + /// + private void ReleaseGas(Entity entity, float dt) + { + var environment = _atmosphereSystem.GetContainingMixture(entity.Owner, false, true); - if (air != null) - air.Volume = volume; - else - return new GasMixture(volume); + var deltaP = environment == null + ? entity.Comp.Air.Pressure + : entity.Comp.Air.Pressure - environment.Pressure; - return air; - } + // Cap deltaP by the maximum output pressure of the tank. + if (deltaP < entity.Comp.SafetyPressure) + deltaP = Math.Min(entity.Comp.ReleasePressure, deltaP); - public void AssumeAir(Entity ent, GasMixture giver) - { - _atmosphereSystem.Merge(ent.Comp.Air, giver); - CheckStatus(ent); - } + var removed = _atmosphereSystem.FlowGas(entity.Comp.Air, deltaP, dt, ReleaseArea); - public void CheckStatus(Entity ent) - { - var (owner, component) = ent; - if (component.Air == null) - return; + if (removed == null) + return; - var pressure = component.Air.Pressure; + if (environment != null) + _atmosphereSystem.Merge(environment, removed); - if (pressure > component.TankFragmentPressure && _maxExplosionRange > 0) - { - // Give the gas a chance to build up more pressure. - for (var i = 0; i < 3; i++) - { - _atmosphereSystem.React(component.Air, component); - } + // If we wouldn't produce a sound, don't throw or play a sound. + if (removed.Pressure < MinimumSoundValvePressure) + return; - pressure = component.Air.Pressure; - var range = MathF.Sqrt((pressure - component.TankFragmentPressure) / component.TankFragmentScale); + Audio.PlayPvs(entity.Comp.ReleaseSound, entity); - // Let's cap the explosion, yeah? - // !1984 - range = Math.Min(Math.Min(range, GasTankComponent.MaxExplosionRange), _maxExplosionRange); + var strength = Atmos.GetOverPressure(removed) * Atmospherics.kPaToKg_m2; - _explosions.TriggerExplosive(owner, radius: range); + if (strength <= 0) + return; - return; - } + // TODO: I hate throwing system. I shouldn't need to do this boilerplate to get a nice looking throw + var rot = _xform.GetWorldRotation(entity); + var ang = _random.NextAngle(rot + ThrowVector.X, rot + ThrowVector.Y); - if (pressure > component.TankRupturePressure) - { - if (component.Integrity <= 0) - { - var environment = _atmosphereSystem.GetContainingMixture(owner, false, true); - if (environment != null) - _atmosphereSystem.Merge(environment, component.Air); + // We bias by angle to make sure it doesn't rotate too much and flies relatively straight. + _physics.ApplyAngularImpulse(entity, (float)(strength * ang)); - _audioSys.PlayPvs(component.RuptureSound, Transform(owner).Coordinates, AudioParams.Default.WithVariation(0.125f)); + // TODO ATMOS: If we can predict ReleaseGas at some point, we should have this apply an impulse to a person holding this gas tank. + _throwing.TryThrow(entity, ang.ToWorldVec() * strength, strength, doSpin: false); + } - QueueDel(owner); - return; - } + public GasMixture RemoveAirOutput(Entity gasTank, float volume) + { + var mixture = _atmosphereSystem.RemoveVolumeAtPressure(gasTank.Comp.Air, volume, gasTank.Comp.ReleasePressure); + // We resize the volume because lungs breathe in volume rather than being pressure based atm. + // If we don't do this, they won't consume all of the outputted gas or will consume way too much. + mixture.Volume = volume; + return mixture; + } - component.Integrity--; - return; - } + public GasMixture RemoveAir(Entity gasTank, float amount) + { + return gasTank.Comp.Air.Remove(amount); + } - if (pressure > component.TankLeakPressure) - { - if (component.Integrity <= 0) - { - var environment = _atmosphereSystem.GetContainingMixture(owner, false, true); - if (environment == null) - return; - - var leakedGas = component.Air.RemoveRatio(0.25f); - _atmosphereSystem.Merge(environment, leakedGas); - } - else - { - component.Integrity--; - } - - return; - } + protected override void SafetyMeasures(Entity entity) + { + if (entity.Comp.ReleaseValveOpen) + return; - if (component.Integrity < 3) - component.Integrity++; - } + ToggleValve(entity); + if (entity.Comp.SafetyAlert != null) + _popup.PopupEntity(Loc.GetString(entity.Comp.SafetyAlert), entity, PopupType.LargeCaution); - /// - /// Returns the gas mixture for the gas analyzer - /// - private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args) - { - args.GasMixtures ??= new List<(string, GasMixture?)>(); - args.GasMixtures.Add((Name(uid), component.Air)); - } + Dirty(entity); + } - private void OnGasTankPrice(EntityUid uid, GasTankComponent component, ref PriceCalculationEvent args) - { - args.Price += _atmosphereSystem.GetPrice(component.Air); - } + /// + /// Returns the gas mixture for the gas analyzer + /// + private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args) + { + args.GasMixtures ??= new List<(string, GasMixture?)>(); + args.GasMixtures.Add((Name(uid), component.Air)); + } + + private void OnGasTankPrice(EntityUid uid, GasTankComponent component, ref PriceCalculationEvent args) + { + args.Price += _atmosphereSystem.GetPrice(component.Air); } } diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs index 4882e93d230..78ebba8e97c 100644 --- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,6 +1,3 @@ -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Content.Server.Atmos.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; @@ -13,7 +10,6 @@ using Microsoft.Extensions.ObjectPool; using Robust.Server.Player; using Robust.Shared; -using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -21,6 +17,7 @@ using Robust.Shared.Threading; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System.Runtime.CompilerServices; // ReSharper disable once RedundantUsingDirective @@ -32,7 +29,6 @@ public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem [Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!; [Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!; [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!; - [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!; [Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!; [Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!; @@ -85,9 +81,8 @@ public override void Initialize() }; _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true); - Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true); - Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true); + + InitializeCVars(); SubscribeLocalEvent(Reset); SubscribeLocalEvent(OnStartup); @@ -175,7 +170,16 @@ private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax) public GasOverlayData GetOverlayData(GasMixture? mixture) { - var data = new GasOverlayData(0, new byte[VisibleGasId.Length]); + ThermalByte byteTemp; + if (mixture == null) + { + byteTemp = new(); + byteTemp.SetVacuum(); + } + else + byteTemp = new(mixture.Temperature); + + var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp); for (var i = 0; i < VisibleGasId.Length; i++) { @@ -215,15 +219,27 @@ private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayC } var changed = false; + + ThermalByte newByteTemp = new(); + + if (tile.Hotspot.Valid) + newByteTemp.SetTemperature(tile.Hotspot.Temperature); + else if (!tile.Space && tile.Air?.TotalMoles <= 5f) + newByteTemp.SetVacuum(); + else if (!tile.Space && tile.Air != null) + newByteTemp = new(tile.Air.Temperature); + if (oldData.Equals(default)) { changed = true; - oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length]); + oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp); } - else if (oldData.FireState != tile.Hotspot.State) + else if (oldData.FireState != tile.Hotspot.State || + Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average. + (oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution)) // change of special ThermalByte value { changed = true; - oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity); + oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp); } if (tile is {Air: not null, NoGridTile: false}) @@ -465,5 +481,12 @@ public void Execute(int index) } #endregion + + private void InitializeCVars() + { + Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true); + Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true); + Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true); + } } } diff --git a/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs index b3644e88b73..78d74d57c35 100644 --- a/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/HeatExchangerSystem.cs @@ -1,16 +1,9 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.Atmos.Piping.Unary.Components; -using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; -using Content.Server.NodeContainer; -using Content.Shared.Atmos.Piping; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.CCVar; -using Content.Shared.Interaction; -using JetBrains.Annotations; using Robust.Shared.Configuration; namespace Content.Server.Atmos.EntitySystems; @@ -43,7 +36,7 @@ private void OnAtmosUpdate(EntityUid uid, HeatExchangerComponent comp, ref Atmos // make sure that the tile the device is on isn't blocked by a wall or something similar. if (args.Grid is {} grid && _transform.TryGetGridTilePosition(uid, out var tile) - && _atmosphereSystem.IsTileAirBlocked(grid, tile)) + && _atmosphereSystem.IsTileAirBlockedCached(grid, tile)) { return; } diff --git a/Content.Server/Atmos/ExcitedGroup.cs b/Content.Server/Atmos/ExcitedGroup.cs deleted file mode 100644 index 73dc6c23292..00000000000 --- a/Content.Server/Atmos/ExcitedGroup.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Content.Server.Atmos -{ - public sealed class ExcitedGroup - { - [ViewVariables] public bool Disposed = false; - - [ViewVariables] public readonly List Tiles = new(100); - - [ViewVariables] public int DismantleCooldown { get; set; } = 0; - - [ViewVariables] public int BreakdownCooldown { get; set; } = 0; - } -} diff --git a/Content.Server/Atmos/Hotspot.cs b/Content.Server/Atmos/Hotspot.cs deleted file mode 100644 index 987acf73c2d..00000000000 --- a/Content.Server/Atmos/Hotspot.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Content.Server.Atmos -{ - public struct Hotspot - { - [ViewVariables] - public bool Valid; - - [ViewVariables] - public bool SkippedFirstProcess; - - [ViewVariables] - public bool Bypassing; - - [ViewVariables] - public float Temperature; - - [ViewVariables] - public float Volume; - - /// - /// State for the fire sprite. - /// - [ViewVariables] - public byte State; - } -} diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs index 8bd3608dfb1..669f390d695 100644 --- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Atmos.Monitor.Components; -using Content.Server.Atmos.Piping.Components; using Content.Server.DeviceLinking.Systems; using Content.Server.DeviceNetwork.Systems; using Content.Server.Popups; @@ -20,6 +19,7 @@ using Content.Shared.Wires; using Robust.Server.GameObjects; using System.Linq; +using Content.Shared.Atmos.Components; using Content.Shared.DeviceNetwork.Events; using Content.Shared.DeviceNetwork.Components; diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs index 452b3003314..83dafd0a29a 100644 --- a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs @@ -1,16 +1,14 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Components; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.EntitySystems; -using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork.Systems; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Database; @@ -57,6 +55,13 @@ public override void Initialize() SubscribeLocalEvent(OnPacketRecv); SubscribeLocalEvent(OnAtmosDeviceLeaveAtmosphere); SubscribeLocalEvent(OnAtmosDeviceEnterAtmosphere); + SubscribeLocalEvent(OnAtmosDeviceTileChangedEvent); + } + + private void OnAtmosDeviceTileChangedEvent(Entity ent, ref AtmosDeviceTileChangedEvent args) + { + if (!ent.Comp.MonitorsPipeNet) + ent.Comp.TileGas = _atmosphereSystem.GetContainingMixture(ent.Owner, true); } private void OnAtmosDeviceLeaveAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceDisabledEvent args) diff --git a/Content.Server/Atmos/MonstermosInfo.cs b/Content.Server/Atmos/MonstermosInfo.cs deleted file mode 100644 index 810ce71d2c8..00000000000 --- a/Content.Server/Atmos/MonstermosInfo.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Content.Shared.Atmos; - -namespace Content.Server.Atmos -{ - public struct MonstermosInfo - { - [ViewVariables] - public int LastCycle; - - [ViewVariables] - public long LastQueueCycle; - - [ViewVariables] - public long LastSlowQueueCycle; - - [ViewVariables] - public float MoleDelta; - - [ViewVariables] - public float TransferDirectionEast; - - [ViewVariables] - public float TransferDirectionWest; - - [ViewVariables] - public float TransferDirectionNorth; - - [ViewVariables] - public float TransferDirectionSouth; - - [ViewVariables] - public float CurrentTransferAmount; - - [ViewVariables] - public AtmosDirection CurrentTransferDirection; - - [ViewVariables] - public bool FastDone; - - public float this[AtmosDirection direction] - { - get => - direction switch - { - AtmosDirection.East => TransferDirectionEast, - AtmosDirection.West => TransferDirectionWest, - AtmosDirection.North => TransferDirectionNorth, - AtmosDirection.South => TransferDirectionSouth, - _ => throw new ArgumentOutOfRangeException(nameof(direction)) - }; - - set - { - switch (direction) - { - case AtmosDirection.East: - TransferDirectionEast = value; - break; - case AtmosDirection.West: - TransferDirectionWest = value; - break; - case AtmosDirection.North: - TransferDirectionNorth = value; - break; - case AtmosDirection.South: - TransferDirectionSouth = value; - break; - default: - throw new ArgumentOutOfRangeException(nameof(direction)); - } - } - } - - public float this[int index] - { - get => this[(AtmosDirection) (1 << index)]; - set => this[(AtmosDirection) (1 << index)] = value; - } - } -} diff --git a/Content.Server/Atmos/Piping/Binary/Components/GasPassiveGateComponent.cs b/Content.Server/Atmos/Piping/Binary/Components/GasPassiveGateComponent.cs index 57d48b64e95..1e3ffc869ea 100644 --- a/Content.Server/Atmos/Piping/Binary/Components/GasPassiveGateComponent.cs +++ b/Content.Server/Atmos/Piping/Binary/Components/GasPassiveGateComponent.cs @@ -1,20 +1,19 @@ -using Content.Shared.Atmos; +namespace Content.Server.Atmos.Piping.Binary.Components; -namespace Content.Server.Atmos.Piping.Binary.Components +/// +/// Defines a passive gate, which equalizes gas from +/// inlet to outlet, but does not allow gas to flow from outlet to inlet. +/// +[RegisterComponent] +public sealed partial class GasPassiveGateComponent : Component { - [RegisterComponent] - public sealed partial class GasPassiveGateComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("inlet")] - public string InletName { get; set; } = "inlet"; + [DataField("inlet")] + public string InletName = "inlet"; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("outlet")] - public string OutletName { get; set; } = "outlet"; + [DataField("outlet")] + public string OutletName = "outlet"; - [ViewVariables(VVAccess.ReadOnly)] - [DataField("flowRate")] - public float FlowRate { get; set; } = 0; - } + [ViewVariables(VVAccess.ReadOnly)] + [DataField] + public float FlowRate; } diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs index 008d3cb4ce6..abbddd72fe7 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs @@ -1,89 +1,58 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Piping.Binary.Components; -using Content.Server.Atmos.Piping.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; -using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Examine; using JetBrains.Annotations; -namespace Content.Server.Atmos.Piping.Binary.EntitySystems +namespace Content.Server.Atmos.Piping.Binary.EntitySystems; + +[UsedImplicitly] +public sealed class GasPassiveGateSystem : EntitySystem { - [UsedImplicitly] - public sealed class GasPassiveGateSystem : EntitySystem - { - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; - public override void Initialize() - { - base.Initialize(); + public override void Initialize() + { + base.Initialize(); - SubscribeLocalEvent(OnPassiveGateUpdated); - SubscribeLocalEvent(OnExamined); - } + SubscribeLocalEvent(OnPassiveGateUpdated); + SubscribeLocalEvent(OnExamined); + } - private void OnPassiveGateUpdated(EntityUid uid, GasPassiveGateComponent gate, ref AtmosDeviceUpdateEvent args) + private void OnPassiveGateUpdated(EntityUid uid, GasPassiveGateComponent gate, ref AtmosDeviceUpdateEvent args) + { + if (!_nodeContainer.TryGetNodes(uid, gate.InletName, gate.OutletName, out PipeNode? inlet, out PipeNode? outlet)) + return; + + // ReSharper disable thrice InconsistentNaming + var P1 = inlet.Air.Pressure; + var P2 = outlet.Air.Pressure; + var V1 = inlet.Air.Volume; + var pressureDelta = P1 - P2; + + var dt = args.dt; + float dV = 0; + if (pressureDelta > 0 && P1 > 0) { - if (!_nodeContainer.TryGetNodes(uid, gate.InletName, gate.OutletName, out PipeNode? inlet, out PipeNode? outlet)) - return; - - var n1 = inlet.Air.TotalMoles; - var n2 = outlet.Air.TotalMoles; - var P1 = inlet.Air.Pressure; - var P2 = outlet.Air.Pressure; - var V1 = inlet.Air.Volume; - var V2 = outlet.Air.Volume; - var T1 = inlet.Air.Temperature; - var T2 = outlet.Air.Temperature; - var pressureDelta = P1 - P2; - - float dt = args.dt; - float dV = 0; - var denom = (T1*V2 + T2*V1); + var transferFrac = _atmosphereSystem.FractionToEqualizePressure(inlet.Air, outlet.Air); + dV = transferFrac * V1; - if (pressureDelta > 0 && P1 > 0 && denom > 0) - { - // Calculate the number of moles to transfer to equalize the final pressure of - // both sides of the valve. You can derive this equation yourself by solving - // the equations: - // - // P_inlet,final = P_outlet,final (pressure equilibrium) - // n_inlet,initial + n_outlet,initial = n_inlet,final + n_outlet,final (mass conservation) - // - // These simplifying assumptions allow an easy closed-form solution: - // - // T_inlet,initial = T_inlet,final - // T_outlet,initial = T_outlet,final - // - // If you don't want to push through the math, just know that this behaves like a - // pump that can equalize pressure instantly, i.e. much faster than pressure or - // volume pumps. - var transferMoles = n1 - (n1+n2)*T2*V1 / denom; - - // Get the volume transfered to update our flow meter. - // When you remove x from one side and add x to the other the total difference is 2x. - // Also account for atmos speedup so that measured flow rate matches the setting on the volume pump. - dV = 2*transferMoles*Atmospherics.R*T1/P1 / _atmosphereSystem.Speedup; - - // Actually transfer the gas. - _atmosphereSystem.Merge(outlet.Air, inlet.Air.Remove(transferMoles)); - } - - // Update transfer rate with an exponential moving average. - var tau = 1; // Time constant (averaging time) in seconds - var a = dt/tau; - gate.FlowRate = a*dV/tau + (1-a)*gate.FlowRate; // in L/sec + // Actually transfer the gas. + _atmosphereSystem.Merge(outlet.Air, inlet.Air.RemoveRatio(transferFrac)); } - private void OnExamined(Entity gate, ref ExaminedEvent args) - { - if (!Comp(gate).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status. - return; + gate.FlowRate = AtmosphereSystem.ExponentialMovingAverage(dV, gate.FlowRate, dt); + } - var str = Loc.GetString("gas-passive-gate-examined", ("flowRate", $"{gate.Comp.FlowRate:0.#}")); - args.PushMarkup(str); - } + private void OnExamined(Entity gate, ref ExaminedEvent args) + { + if (!Transform(gate).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status. + return; + + var str = Loc.GetString("gas-passive-gate-examined", ("flowRate", $"{gate.Comp.FlowRate:0.#}")); + args.PushMarkup(str); } } diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs index 63944cd5306..90e106a5b83 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs @@ -1,12 +1,11 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Piping.Components; using Content.Shared.Audio; using JetBrains.Annotations; diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs index 076d5adf8ec..a79eceaa194 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs @@ -1,8 +1,8 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping.Binary.Components; diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs index 9645b0a133e..be485ac7440 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs @@ -1,10 +1,9 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Piping.Binary.Components; -using Content.Server.Atmos.Piping.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Audio; diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs index c6c660614a5..b24e5074673 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs @@ -1,10 +1,10 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Atmos.Piping.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Atmos.Piping.Binary.Systems; using Content.Shared.Atmos.Piping.Components; @@ -53,13 +53,13 @@ private void OnVolumePumpUpdated(EntityUid uid, GasVolumePumpComponent pump, ref pump.Blocked = false; // Pump mechanism won't do anything if the pressure is too high/too low unless you overclock it. - if ((inputStartingPressure < pump.LowerThreshold) || (outputStartingPressure > pump.HigherThreshold) && !pump.Overclocked) + if ((inputStartingPressure <= pump.LowerThreshold) || (outputStartingPressure >= pump.HigherThreshold) && !pump.Overclocked) { pump.Blocked = true; } // Overclocked pumps can only force gas a certain amount. - if ((outputStartingPressure - inputStartingPressure > pump.OverclockThreshold) && pump.Overclocked) + if ((outputStartingPressure - inputStartingPressure >= pump.OverclockThreshold) && pump.Overclocked) { pump.Blocked = true; } @@ -70,7 +70,16 @@ private void OnVolumePumpUpdated(EntityUid uid, GasVolumePumpComponent pump, ref return; // We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters. - var removed = inlet.Air.RemoveVolume(pump.TransferRate * _atmosphereSystem.PumpSpeedup() * args.dt); + var transferVol = pump.TransferRate * _atmosphereSystem.PumpSpeedup() * args.dt; + var transferRatio = transferVol / inlet.Air.Volume; + + // Make sure we don't pump over the pressure limit. + var limitRatio = AtmosphereSystem.FractionToMaxPressure(inlet.Air, outlet.Air, pump.HigherThreshold); + + // This might end up negative under overclock conditions, but such cases are handled correctly by the + // `RemoveRatio` method + var removedRatio = Math.Min(transferRatio, limitRatio); + var removed = inlet.Air.RemoveRatio(removedRatio); // Some of the gas from the mixture leaks when overclocked. if (pump.Overclocked) diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs index 3feaf79eb73..b7487cd5389 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosDeviceSystem.cs @@ -1,6 +1,5 @@ -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Components; using JetBrains.Annotations; using Robust.Shared.Timing; diff --git a/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs index bb7678d28d1..6dfb1afa866 100644 --- a/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs +++ b/Content.Server/Atmos/Piping/EntitySystems/GasPipeManifoldSystem.cs @@ -51,6 +51,7 @@ private void OnAnalyzed(Entity ent, ref GasAnalyzerSca return; var pipeNames = ent.Comp.InletNames.Union(ent.Comp.OutletNames); + var pipeCount = pipeNames.Count(); foreach (var pipeName in pipeNames) { @@ -58,8 +59,8 @@ private void OnAnalyzed(Entity ent, ref GasAnalyzerSca continue; var pipeLocal = pipe.Air.Clone(); - pipeLocal.Multiply(pipe.Volume / pipe.Air.Volume); - pipeLocal.Volume = pipe.Volume; + pipeLocal.Multiply(pipe.Volume * pipeCount / pipe.Air.Volume); + pipeLocal.Volume = pipe.Volume * pipeCount; args.GasMixtures.Add((Name(ent), pipeLocal)); break; diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs index 8d0fb8ad2cf..7f05d2e4a56 100644 --- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs +++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs @@ -1,11 +1,10 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Trinary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Atmos.Piping.Trinary.Components; @@ -55,7 +54,7 @@ private void OnFilterUpdated(EntityUid uid, GasFilterComponent filter, ref Atmos { if (!filter.Enabled || !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.FilterName, filter.OutletName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode) - || outletNode.Air.Pressure >= Atmospherics.MaxOutputPressure) // No need to transfer if target is full. + || (outletNode.Air.Pressure >= Atmospherics.MaxOutputPressure && filterNode.Air.Pressure >= Atmospherics.MaxOutputPressure)) // No need to transfer if targets are full. { _ambientSoundSystem.SetAmbience(uid, false); return; @@ -74,17 +73,31 @@ private void OnFilterUpdated(EntityUid uid, GasFilterComponent filter, ref Atmos if (filter.FilteredGas.HasValue) { - var filteredOut = new GasMixture() { Temperature = removed.Temperature }; + // Make sure we don't pump over the pressure limit. + var limitMolesFilter = + AtmosphereSystem.MolesToMaxPressure(removed, filterNode.Air, Atmospherics.MaxOutputPressure); - filteredOut.SetMoles(filter.FilteredGas.Value, removed.GetMoles(filter.FilteredGas.Value)); - removed.SetMoles(filter.FilteredGas.Value, 0f); + var availableMoles = removed.GetMoles(filter.FilteredGas.Value); + var filteredMoles = Math.Max(Math.Min(limitMolesFilter, availableMoles), 0); + var filteredGasMixture = new GasMixture { Temperature = removed.Temperature }; - var target = filterNode.Air.Pressure < Atmospherics.MaxOutputPressure ? filterNode : inletNode; - _atmosphereSystem.Merge(target.Air, filteredOut); - _ambientSoundSystem.SetAmbience(uid, filteredOut.TotalMoles > 0f); + filteredGasMixture.SetMoles(filter.FilteredGas.Value, filteredMoles); + removed.AdjustMoles(filter.FilteredGas.Value, -filteredMoles); + + _atmosphereSystem.Merge(filterNode.Air, filteredGasMixture); + + _ambientSoundSystem.SetAmbience(uid, filteredMoles > 0f); } - _atmosphereSystem.Merge(outletNode.Air, removed); + // Fraction of `removed` that can be sent to outlet without exceeding max pressure. + var limitRatioOutlet = + AtmosphereSystem.FractionToMaxPressure(removed, outletNode.Air, Atmospherics.MaxOutputPressure); + + // This might end up negative, but such cases are handled correctly by the `RemoveRatio` method + var passthrough = removed.RemoveRatio(limitRatioOutlet); + + _atmosphereSystem.Merge(outletNode.Air, passthrough); + _atmosphereSystem.Merge(inletNode.Air, removed); } private void OnFilterLeaveAtmosphere(EntityUid uid, GasFilterComponent filter, ref AtmosDeviceDisabledEvent args) @@ -156,18 +169,18 @@ private void OnTransferRateChangeMessage(EntityUid uid, GasFilterComponent filte private void OnSelectGasMessage(EntityUid uid, GasFilterComponent filter, GasFilterSelectGasMessage args) { - if (args.ID.HasValue) + if (args.Gas.HasValue) { - if (Enum.TryParse(args.ID.ToString(), true, out var parsedGas)) + if (Enum.IsDefined(typeof(Gas), args.Gas)) { - filter.FilteredGas = parsedGas; + filter.FilteredGas = args.Gas; _adminLogger.Add(LogType.AtmosFilterChanged, LogImpact.Medium, - $"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {parsedGas.ToString()}"); + $"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {args.Gas.ToString()}"); DirtyUI(uid, filter); } else { - Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.ID}"); + Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.Gas}"); } } else diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs index 84abede0665..8f3f73db27a 100644 --- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs +++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs @@ -1,11 +1,10 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Trinary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Atmos.Piping.Trinary.Components; diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/PressureControlledValveSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/PressureControlledValveSystem.cs index d1570b11292..d9128f68ae9 100644 --- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/PressureControlledValveSystem.cs +++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/PressureControlledValveSystem.cs @@ -1,94 +1,97 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Trinary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Audio; using JetBrains.Annotations; -namespace Content.Server.Atmos.Piping.Trinary.EntitySystems +namespace Content.Server.Atmos.Piping.Trinary.EntitySystems; + +[UsedImplicitly] +public sealed class PressureControlledValveSystem : EntitySystem { - [UsedImplicitly] - public sealed class PressureControlledValveSystem : EntitySystem + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + + public override void Initialize() { - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUpdate); + SubscribeLocalEvent(OnFilterLeaveAtmosphere); + } - public override void Initialize() + private void OnInit(EntityUid uid, PressureControlledValveComponent comp, ComponentInit args) + { + UpdateAppearance(uid, comp); + } + + private void OnUpdate(EntityUid uid, PressureControlledValveComponent comp, ref AtmosDeviceUpdateEvent args) + { + if (!_nodeContainer.TryGetNodes(uid, comp.InletName, comp.ControlName, comp.OutletName, out PipeNode? inletNode, out PipeNode? controlNode, out PipeNode? outletNode)) { - base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnUpdate); - SubscribeLocalEvent(OnFilterLeaveAtmosphere); + _ambientSoundSystem.SetAmbience(uid, false); + comp.Enabled = false; + return; } - private void OnInit(EntityUid uid, PressureControlledValveComponent comp, ComponentInit args) + // If output is higher than input, flip input/output to enable bidirectional flow. + if (outletNode.Air.Pressure > inletNode.Air.Pressure) { - UpdateAppearance(uid, comp); + PipeNode temp = outletNode; + outletNode = inletNode; + inletNode = temp; } - private void OnUpdate(EntityUid uid, PressureControlledValveComponent comp, ref AtmosDeviceUpdateEvent args) + float control = (controlNode.Air.Pressure - outletNode.Air.Pressure) - comp.Threshold; + float transferRate; + if (control < 0) { - if (!_nodeContainer.TryGetNodes(uid, comp.InletName, comp.ControlName, comp.OutletName, out PipeNode? inletNode, out PipeNode? controlNode, out PipeNode? outletNode)) - { - _ambientSoundSystem.SetAmbience(uid, false); - comp.Enabled = false; - return; - } - - // If output is higher than input, flip input/output to enable bidirectional flow. - if (outletNode.Air.Pressure > inletNode.Air.Pressure) - { - PipeNode temp = outletNode; - outletNode = inletNode; - inletNode = temp; - } - - float control = (controlNode.Air.Pressure - outletNode.Air.Pressure) - comp.Threshold; - float transferRate; - if (control < 0) - { - comp.Enabled = false; - transferRate = 0; - } - else - { - comp.Enabled = true; - transferRate = Math.Min(control * comp.Gain, comp.MaxTransferRate * _atmosphereSystem.PumpSpeedup()); - } - UpdateAppearance(uid, comp); - - // We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters. - var transferVolume = transferRate * args.dt; - if (transferVolume <= 0) - { - _ambientSoundSystem.SetAmbience(uid, false); - return; - } - - _ambientSoundSystem.SetAmbience(uid, true); - var removed = inletNode.Air.RemoveVolume(transferVolume); - _atmosphereSystem.Merge(outletNode.Air, removed); + comp.Enabled = false; + transferRate = 0; + } + else + { + comp.Enabled = true; + transferRate = Math.Min(control * comp.Gain, comp.MaxTransferRate * _atmosphereSystem.PumpSpeedup()); } + UpdateAppearance(uid, comp); - private void OnFilterLeaveAtmosphere(EntityUid uid, PressureControlledValveComponent comp, ref AtmosDeviceDisabledEvent args) + // We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters. + var transferVolume = transferRate * args.dt; + if (transferVolume <= 0) { - comp.Enabled = false; - UpdateAppearance(uid, comp); _ambientSoundSystem.SetAmbience(uid, false); + return; } - private void UpdateAppearance(EntityUid uid, PressureControlledValveComponent? comp = null, AppearanceComponent? appearance = null) - { - if (!Resolve(uid, ref comp, ref appearance, false)) - return; + // clamp to equalization so we don't overshoot (happens with silly euler) + var maxFrac = _atmosphereSystem.FractionToEqualizePressure(inletNode.Air, outletNode.Air); + var maxVol = inletNode.Air.Volume * maxFrac; + var clampedVolume = Math.Min(transferVolume, maxVol); - _appearance.SetData(uid, FilterVisuals.Enabled, comp.Enabled, appearance); - } + _ambientSoundSystem.SetAmbience(uid, true); + var removed = inletNode.Air.RemoveVolume(clampedVolume); + _atmosphereSystem.Merge(outletNode.Air, removed); + } + + private void OnFilterLeaveAtmosphere(EntityUid uid, PressureControlledValveComponent comp, ref AtmosDeviceDisabledEvent args) + { + comp.Enabled = false; + UpdateAppearance(uid, comp); + _ambientSoundSystem.SetAmbience(uid, false); + } + + private void UpdateAppearance(EntityUid uid, PressureControlledValveComponent? comp = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref comp, ref appearance, false)) + return; + + _appearance.SetData(uid, FilterVisuals.Enabled, comp.Enabled, appearance); } } diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs index 14c85620b18..595b2de11fd 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs @@ -1,15 +1,16 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Binary.Components; +using Content.Shared.Atmos.Piping.Components; using Content.Shared.Atmos.Piping.Unary.Systems; using Content.Shared.Cargo; using Content.Shared.Database; using Content.Shared.NodeContainer; +using Content.Shared.Popups; using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent; namespace Content.Server.Atmos.Piping.Unary.EntitySystems; @@ -17,14 +18,16 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems; public sealed class GasCanisterSystem : SharedGasCanisterSystem { [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private const float ReleaseArea = 0.05f; // 500cm^2 Number chosen for balance reasons. It's quite large, but so are gas canisters (holding 1.5 cubic meters of gas!) public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCanisterUpdated); SubscribeLocalEvent(CalculateCanisterPrice); SubscribeLocalEvent(OnAnalyzed); } @@ -48,7 +51,7 @@ public void PurgeContents(EntityUid uid, GasCanisterComponent? canister = null, protected override void DirtyUI(EntityUid uid, GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null) { - if (!Resolve(uid, ref canister, ref nodeContainer)) + if (!Resolve(uid, ref canister, ref nodeContainer, logMissing: false)) return; var portStatus = false; @@ -64,64 +67,84 @@ protected override void DirtyUI(EntityUid uid, GasCanisterComponent? canister = tankPressure = tankComponent.Air.Pressure; } - UI.SetUiState(uid, GasCanisterUiKey.Key, + UI.SetUiState(uid, + GasCanisterUiKey.Key, new GasCanisterBoundUserInterfaceState(canister.Air.Pressure, portStatus, tankPressure)); } - private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args) + protected override void SafetyMeasures(Entity entity) + { + if (entity.Comp.SafetyValveOpen) + return; + + ToggleSafetyValve(entity, open: true); + if (entity.Comp.SafetyAlert != null) + _popup.PopupEntity(Loc.GetString(entity.Comp.SafetyAlert), entity, PopupType.LargeCaution); + } + + private void ToggleSafetyValve(Entity entity, bool open) + { + entity.Comp.SafetyValveOpen = open; + Audio.PlayPvs(entity.Comp.ValveSound, entity); + } + + protected override void DeviceUpdated(Entity entity, ref AtmosDeviceUpdateEvent args) { - _atmos.React(canister.Air, canister); + _atmos.React(entity.Comp.Air, entity.Comp); - if (!TryComp(uid, out var nodeContainer) - || !TryComp(uid, out var appearance)) + if (!TryComp(entity, out var nodeContainer) + || !TryComp(entity, out var appearance)) return; - if (!_nodeContainer.TryGetNode(nodeContainer, canister.PortName, out PortablePipeNode? portNode)) + if (!_nodeContainer.TryGetNode(nodeContainer, entity.Comp.PortName, out PortablePipeNode? portNode)) return; if (portNode.NodeGroup is PipeNet {NodeCount: > 1} net) { - MixContainerWithPipeNet(canister.Air, net.Air); + MixContainerWithPipeNet(entity.Comp.Air, net.Air); } - // Release valve is open, release gas. - if (canister.ReleaseValve) + // If safety valve is open, we release gas through there ignoring other outputs. + if (entity.Comp.SafetyValveOpen) { - if (canister.GasTankSlot.Item != null) - { - var gasTank = Comp(canister.GasTankSlot.Item.Value); - _atmos.ReleaseGasTo(canister.Air, gasTank.Air, canister.ReleasePressure); - } - else - { - var environment = _atmos.GetContainingMixture(uid, args.Grid, args.Map, false, true); - _atmos.ReleaseGasTo(canister.Air, environment, canister.ReleasePressure); - } + var environment = _atmos.GetContainingMixture(entity.Owner, args.Grid, args.Map, false, true); + _atmos.FlowGas(entity.Comp.Air, environment, args.dt, ReleaseArea); + if (entity.Comp.Air.Pressure < entity.Comp.SafetyPressure) + ToggleSafetyValve(entity, false); + } + else if (entity.Comp.ReleaseValveOpen) // Release valve is open, release gas. + { + var output = entity.Comp.GasTankSlot.Item == null + ? _atmos.GetContainingMixture(entity.Owner, args.Grid, args.Map, false, true) + : CompOrNull(entity.Comp.GasTankSlot.Item.Value)?.Air; + + // Only let gas flow one way! + _atmos.ReleaseGasTo(entity.Comp.Air, output, entity.Comp.ReleasePressure); } // If last pressure is very close to the current pressure, do nothing. - if (MathHelper.CloseToPercent(canister.Air.Pressure, canister.LastPressure)) + if (MathHelper.CloseToPercent(entity.Comp.Air.Pressure, entity.Comp.LastPressure)) return; - DirtyUI(uid, canister, nodeContainer); + DirtyUI(entity, entity.Comp, nodeContainer); - canister.LastPressure = canister.Air.Pressure; + entity.Comp.LastPressure = entity.Comp.Air.Pressure; - if (canister.Air.Pressure < 10) + if (entity.Comp.Air.Pressure < 10) { - _appearance.SetData(uid, GasCanisterVisuals.PressureState, 0, appearance); + _appearance.SetData(entity, GasCanisterVisuals.PressureState, 0, appearance); } - else if (canister.Air.Pressure < Atmospherics.OneAtmosphere) + else if (entity.Comp.Air.Pressure < Atmospherics.OneAtmosphere) { - _appearance.SetData(uid, GasCanisterVisuals.PressureState, 1, appearance); + _appearance.SetData(entity, GasCanisterVisuals.PressureState, 1, appearance); } - else if (canister.Air.Pressure < (15 * Atmospherics.OneAtmosphere)) + else if (entity.Comp.Air.Pressure < (15 * Atmospherics.OneAtmosphere)) { - _appearance.SetData(uid, GasCanisterVisuals.PressureState, 2, appearance); + _appearance.SetData(entity, GasCanisterVisuals.PressureState, 2, appearance); } else { - _appearance.SetData(uid, GasCanisterVisuals.PressureState, 3, appearance); + _appearance.SetData(entity, GasCanisterVisuals.PressureState, 3, appearance); } } diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCondenserSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCondenserSystem.cs index e903ceedafa..7bec2136bed 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCondenserSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCondenserSystem.cs @@ -1,12 +1,11 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; using JetBrains.Annotations; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs index 62039185170..86738518e35 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs @@ -1,9 +1,8 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping; using Content.Shared.Interaction; using JetBrains.Annotations; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs index 72812cb5237..84bd25d0688 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs @@ -1,10 +1,9 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using JetBrains.Annotations; namespace Content.Server.Atmos.Piping.Unary.EntitySystems diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs index 35ff89c7262..9f97f3560cd 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs @@ -1,7 +1,5 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; @@ -10,10 +8,10 @@ using Content.Shared.Atmos.Piping.Unary.Components; using JetBrains.Annotations; using Content.Server.Power.EntitySystems; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Unary.Systems; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Events; -using Content.Shared.Examine; using Content.Shared.DeviceNetwork.Components; namespace Content.Server.Atmos.Piping.Unary.EntitySystems diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs index 1066e4e88d3..0716d96f14e 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs @@ -1,6 +1,5 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.DeviceLinking.Systems; using Content.Server.DeviceNetwork.Systems; @@ -9,6 +8,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Atmos.Piping.Unary; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs index 22f22a682a0..f6f3829142f 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs @@ -1,6 +1,5 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.NodeContainer.EntitySystems; @@ -8,6 +7,7 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Piping.Components; using Content.Shared.Atmos.Piping.Unary.Components; diff --git a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs index 2b674009b1b..080244d5f63 100644 --- a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs +++ b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs @@ -33,7 +33,11 @@ public sealed partial class PortableScrubberComponent : Component Gas.Thermonium, /// Imperial Added Thermonium Gas.Phazonium, /// Imperial Added Phazonium and Ozonium Gas.Ozonium, /// Imperial Added Phazonium and Ozonium - Gas.Hydrogen /// Imperial Added Hydrogen + Gas.Hydrogen, /// Imperial Added Hydrogen + Gas.Deuterium, /// Imperial Atmos Update start + Gas.HyperNoblium, + Gas.AntiNoblium, + Gas.BZ /// Imperial Atmos Update end }; [ViewVariables(VVAccess.ReadWrite)] diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs index b1b6ed36789..b421c1771c7 100644 --- a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs +++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs @@ -3,10 +3,7 @@ using Content.Shared.Atmos.Visuals; using Content.Shared.Examine; using Content.Shared.Destructible; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Power.Components; -using Content.Server.NodeContainer; using Robust.Server.GameObjects; using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.NodeGroups; @@ -14,6 +11,7 @@ using Content.Server.Administration.Logs; using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Database; using Content.Shared.Power; diff --git a/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs b/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs index 1c05307c158..9385613d945 100644 --- a/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs +++ b/Content.Server/Atmos/Portable/SpaceHeaterSystem.cs @@ -1,9 +1,8 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Piping.Portable.Components; using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Atmos.Visuals; @@ -54,11 +53,13 @@ private void OnBeforeOpened(EntityUid uid, SpaceHeaterComponent spaceHeater, Bef private void OnUIActivationAttempt(EntityUid uid, SpaceHeaterComponent spaceHeater, ActivatableUIOpenAttemptEvent args) { - if (!Comp(uid).Anchored) - { + if (Comp(uid).Anchored) + return; + + if (!args.Silent) _popup.PopupEntity(Loc.GetString("comp-space-heater-unanchored", ("device", Loc.GetString("comp-space-heater-device-name"))), uid, args.User); - args.Cancel(); - } + + args.Cancel(); } private void OnDeviceUpdated(EntityUid uid, SpaceHeaterComponent spaceHeater, ref AtmosDeviceUpdateEvent args) @@ -112,7 +113,9 @@ private void OnTemperatureChanged(EntityUid uid, SpaceHeaterComponent spaceHeate if (!TryComp(uid, out var thermoMachine)) return; - thermoMachine.TargetTemperature = float.Clamp(thermoMachine.TargetTemperature + args.Temperature, thermoMachine.MinTemperature, thermoMachine.MaxTemperature); + thermoMachine.TargetTemperature = float.Clamp(thermoMachine.TargetTemperature + args.Temperature, + spaceHeater.MinTemperature, + spaceHeater.MaxTemperature); UpdateAppearance(uid); DirtyUI(uid, spaceHeater); diff --git a/Content.Server/Atmos/Reactions/GasReactionPrototype.cs b/Content.Server/Atmos/Reactions/GasReactionPrototype.cs index dc7ba85dcc3..9c6224fb0d8 100644 --- a/Content.Server/Atmos/Reactions/GasReactionPrototype.cs +++ b/Content.Server/Atmos/Reactions/GasReactionPrototype.cs @@ -42,6 +42,8 @@ public sealed partial class GasReactionPrototype : IPrototype /// [DataField("priority")] public int Priority { get; private set; } = int.MinValue; + [DataField] // Imperial Atmos Update + public bool StoppedAntiNoblium { get; private set; } = true;// Imperial Atmos Update /// /// A list of effects this will produce. diff --git a/Content.Server/Atmos/Reactions/TritiumFireReaction.cs b/Content.Server/Atmos/Reactions/TritiumFireReaction.cs index 8b7d4e4872e..d6e4b9624e6 100644 --- a/Content.Server/Atmos/Reactions/TritiumFireReaction.cs +++ b/Content.Server/Atmos/Reactions/TritiumFireReaction.cs @@ -27,12 +27,14 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, Atmos burnedFuel = initialTrit; mixture.AdjustMoles(Gas.Tritium, -burnedFuel); + mixture.AdjustMoles(Gas.Oxygen, -burnedFuel / Atmospherics.TritiumBurnFuelRatio); } else { - burnedFuel = initialTrit; - mixture.SetMoles(Gas.Tritium, mixture.GetMoles(Gas.Tritium ) * (1 - 1 / Atmospherics.TritiumBurnTritFactor)); - mixture.AdjustMoles(Gas.Oxygen, -mixture.GetMoles(Gas.Tritium)); + // Limit the amount of fuel burned by the limiting reactant, either our initial tritium or the amount of oxygen available given the burn ratio. + burnedFuel = Math.Min(initialTrit, mixture.GetMoles(Gas.Oxygen) / Atmospherics.TritiumBurnFuelRatio) / Atmospherics.TritiumBurnTritFactor; + mixture.AdjustMoles(Gas.Tritium, -burnedFuel); + mixture.AdjustMoles(Gas.Oxygen, -burnedFuel / Atmospherics.TritiumBurnFuelRatio); energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel * (Atmospherics.TritiumBurnTritFactor - 1)); } diff --git a/Content.Server/Atmos/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs index 6f14debc3d0..07526e4b799 100644 --- a/Content.Server/Atmos/Rotting/RottingSystem.cs +++ b/Content.Server/Atmos/Rotting/RottingSystem.cs @@ -1,9 +1,10 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Temperature.Components; using Content.Shared.Atmos; using Content.Shared.Atmos.Rotting; using Content.Shared.Body.Events; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Gibbing; +using Content.Shared.Temperature.Components; using Robust.Server.Containers; using Robust.Shared.Physics.Components; using Robust.Shared.Timing; @@ -21,12 +22,12 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnGibbed); + SubscribeLocalEvent(OnGibbed); SubscribeLocalEvent(OnTempIsRotting); } - private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEvent args) + private void OnGibbed(EntityUid uid, RottingComponent component, GibbedBeforeDeletionEvent args) { if (!TryComp(uid, out var physics)) return; @@ -72,22 +73,26 @@ public override void Update(float frameTime) { if (_timing.CurTime < perishable.RotNextUpdate) continue; + perishable.RotNextUpdate += perishable.PerishUpdateRate; var stage = PerishStage((uid, perishable), MaxStages); if (stage != perishable.Stage) { perishable.Stage = stage; - Dirty(uid, perishable); + DirtyField(uid, perishable, nameof(PerishableComponent.Stage)); } if (IsRotten(uid) || !IsRotProgressing(uid, perishable)) continue; perishable.RotAccumulator += perishable.PerishUpdateRate * GetRotRate(uid); + DirtyField(uid, perishable, nameof(PerishableComponent.RotAccumulator)); if (perishable.RotAccumulator >= perishable.RotAfter) { var rot = AddComp(uid); + var ev = new BeginRottingEvent(); + RaiseLocalEvent(uid, ref ev); rot.NextRotUpdate = _timing.CurTime + rot.RotUpdateRate; } } @@ -122,11 +127,11 @@ public override void Update(float frameTime) if (!TryComp(uid, out var physics)) continue; + // We need a way to get the mass of the mob alone without armor etc in the future // or just remove the mass mechanics altogether because they aren't good. var molRate = perishable.MolsPerSecondPerUnitMass * (float)rotting.RotUpdateRate.TotalSeconds; - var tileMix = _atmosphere.GetTileMixture(uid, excite: true); - tileMix?.AdjustMoles(Gas.Ammonia, molRate * physics.FixturesMass); + _atmosphere.AdjustTileMixture(uid, Gas.Ammonia, molRate * physics.FixturesMass, excite: true); } } } diff --git a/Content.Server/Atmos/TileAtmosphere.cs b/Content.Server/Atmos/TileAtmosphere.cs deleted file mode 100644 index 46a85990fa9..00000000000 --- a/Content.Server/Atmos/TileAtmosphere.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Shared.Atmos; -using Content.Shared.Maps; -using Robust.Shared.Map; - -namespace Content.Server.Atmos -{ - /// - /// Internal Atmos class that stores data about the atmosphere in a grid. - /// You shouldn't use this directly, use instead. - /// - [Access(typeof(AtmosphereSystem), typeof(GasTileOverlaySystem), typeof(AtmosDebugOverlaySystem))] - public sealed class TileAtmosphere : IGasMixtureHolder - { - [ViewVariables] - public int ArchivedCycle; - - [ViewVariables] - public int CurrentCycle; - - [ViewVariables] - public float Temperature { get; set; } = Atmospherics.T20C; - - [ViewVariables] - public TileAtmosphere? PressureSpecificTarget { get; set; } - - /// - /// This is either the pressure difference, or the quantity of moles transferred if monstermos is enabled. - /// - [ViewVariables] - public float PressureDifference { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public float HeatCapacity { get; set; } = Atmospherics.MinimumHeatCapacity; - - [ViewVariables] - public float ThermalConductivity { get; set; } = 0.05f; - - [ViewVariables] - public bool Excited { get; set; } - - /// - /// Whether this tile should be considered space. - /// - [ViewVariables] - public bool Space { get; set; } - - /// - /// Adjacent tiles in the same order as . (NSEW) - /// - [ViewVariables] - public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions]; - - /// - /// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the - /// unblocked directions on adjacent tiles. - /// - [ViewVariables] - public AtmosDirection AdjacentBits = AtmosDirection.Invalid; - - [ViewVariables, Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] - public MonstermosInfo MonstermosInfo; - - [ViewVariables] - public Hotspot Hotspot; - - [ViewVariables] - public AtmosDirection PressureDirection; - - // For debug purposes. - [ViewVariables] - public AtmosDirection LastPressureDirection; - - [ViewVariables] - [Access(typeof(AtmosphereSystem))] - public EntityUid GridIndex { get; set; } - - [ViewVariables] - public Vector2i GridIndices; - - [ViewVariables] - public ExcitedGroup? ExcitedGroup { get; set; } - - /// - /// The air in this tile. If null, this tile is completely air-blocked. - /// This can be immutable if the tile is spaced. - /// - [ViewVariables] - [Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public GasMixture? Air { get; set; } - - /// - /// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air - /// in-place without affecting the results based on update order. - /// - [ViewVariables] - public GasMixture? AirArchived; - - [DataField("lastShare")] - public float LastShare; - - GasMixture IGasMixtureHolder.Air - { - get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature }; - set => Air = value; - } - - [ViewVariables] - public float MaxFireTemperatureSustained { get; set; } - - /// - /// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at - /// this position, or because the tile type is not airtight. - /// - [ViewVariables] - public bool MapAtmosphere; - - /// - /// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for - /// adjacent grid tiles. - /// - [ViewVariables] - public bool NoGridTile; - - /// - /// If true, this tile is queued for processing in - /// - [ViewVariables] - public bool TrimQueued; - - /// - /// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated - /// (i.e., gets added to ). - /// - public AtmosphereSystem.AirtightData AirtightData; - - public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false) - { - GridIndex = gridIndex; - GridIndices = gridIndices; - Air = mixture; - AirArchived = Air != null ? Air.Clone() : null; - Space = space; - - if(immutable) - Air?.MarkImmutable(); - } - - public TileAtmosphere(TileAtmosphere other) - { - GridIndex = other.GridIndex; - GridIndices = other.GridIndices; - Space = other.Space; - NoGridTile = other.NoGridTile; - MapAtmosphere = other.MapAtmosphere; - Air = other.Air?.Clone(); - AirArchived = Air != null ? Air.Clone() : null; - } - - public TileAtmosphere() - { - } - } -} diff --git a/Content.Server/Atmos/TileFireEvent.cs b/Content.Server/Atmos/TileFireEvent.cs deleted file mode 100644 index 5dad4e8fc08..00000000000 --- a/Content.Server/Atmos/TileFireEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Content.Server.Atmos -{ - /// - /// Event raised directed to an entity when it is standing on a tile that's on fire. - /// - [ByRefEvent] - public readonly struct TileFireEvent - { - public readonly float Temperature; - public readonly float Volume; - - public TileFireEvent(float temperature, float volume) - { - Temperature = temperature; - Volume = volume; - } - } -} diff --git a/Content.Server/Audio/Jukebox/JukeboxSystem.cs b/Content.Server/Audio/Jukebox/JukeboxSystem.cs index 81d71894b86..c8600a35536 100644 --- a/Content.Server/Audio/Jukebox/JukeboxSystem.cs +++ b/Content.Server/Audio/Jukebox/JukeboxSystem.cs @@ -5,14 +5,11 @@ using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Components; -using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using JukeboxComponent = Content.Shared.Audio.Jukebox.JukeboxComponent; namespace Content.Server.Audio.Jukebox; - public sealed class JukeboxSystem : SharedJukeboxSystem { [Dependency] private readonly IPrototypeManager _protoManager = default!; @@ -32,81 +29,51 @@ public override void Initialize() SubscribeLocalEvent(OnPowerChanged); } - private void OnComponentInit(EntityUid uid, JukeboxComponent component, ComponentInit args) + private void OnComponentInit(Entity ent, ref ComponentInit args) { - if (HasComp(uid)) + if (HasComp(ent)) { - TryUpdateVisualState(uid, component); + TryUpdateVisualState(ent.AsNullable()); } } - private void OnJukeboxPlay(EntityUid uid, JukeboxComponent component, ref JukeboxPlayingMessage args) + private void OnJukeboxPlay(Entity ent, ref JukeboxPlayingMessage args) { - if (Exists(component.AudioStream)) - { - Audio.SetState(component.AudioStream, AudioState.Playing); - } - else - { - component.AudioStream = Audio.Stop(component.AudioStream); - - if (string.IsNullOrEmpty(component.SelectedSongId) || - !_protoManager.Resolve(component.SelectedSongId, out var jukeboxProto)) - { - return; - } - - component.AudioStream = Audio.PlayPvs(jukeboxProto.Path, uid, AudioParams.Default.WithMaxDistance(10f))?.Entity; - Dirty(uid, component); - } + TryPlay(ent.AsNullable()); } private void OnJukeboxPause(Entity ent, ref JukeboxPauseMessage args) { - Audio.SetState(ent.Comp.AudioStream, AudioState.Paused); + Pause(ent.AsNullable()); } - private void OnJukeboxSetTime(EntityUid uid, JukeboxComponent component, JukeboxSetTimeMessage args) + private void OnJukeboxSetTime(Entity ent, ref JukeboxSetTimeMessage args) { if (TryComp(args.Actor, out ActorComponent? actorComp)) { var offset = actorComp.PlayerSession.Channel.Ping * 1.5f / 1000f; - Audio.SetPlaybackPosition(component.AudioStream, args.SongTime + offset); + SetTime(ent.AsNullable(), args.SongTime + offset); } } private void OnPowerChanged(Entity entity, ref PowerChangedEvent args) { - TryUpdateVisualState(entity); + TryUpdateVisualState(entity.AsNullable()); if (!this.IsPowered(entity.Owner, EntityManager)) { - Stop(entity); + Stop(entity.AsNullable()); } } private void OnJukeboxStop(Entity entity, ref JukeboxStopMessage args) { - Stop(entity); - } - - private void Stop(Entity entity) - { - Audio.SetState(entity.Comp.AudioStream, AudioState.Stopped); - Dirty(entity); + Stop(entity.AsNullable()); } private void OnJukeboxSelected(EntityUid uid, JukeboxComponent component, JukeboxSelectedMessage args) { - if (!Audio.IsPlaying(component.AudioStream)) - { - component.SelectedSongId = args.SongId; - DirectSetVisualState(uid, JukeboxVisualState.Select); - component.Selecting = true; - component.AudioStream = Audio.Stop(component.AudioStream); - } - - Dirty(uid, component); + SetSelectedTrack((uid, component), args.SongId); } public override void Update(float frameTime) @@ -124,15 +91,15 @@ public override void Update(float frameTime) comp.SelectAccumulator = 0f; comp.Selecting = false; - TryUpdateVisualState(uid, comp); + TryUpdateVisualState((uid, comp)); } } } } - private void OnComponentShutdown(EntityUid uid, JukeboxComponent component, ComponentShutdown args) + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) { - component.AudioStream = Audio.Stop(component.AudioStream); + ent.Comp.AudioStream = Audio.Stop(ent.Comp.AudioStream); } private void DirectSetVisualState(EntityUid uid, JukeboxVisualState state) @@ -140,18 +107,99 @@ private void DirectSetVisualState(EntityUid uid, JukeboxVisualState state) _appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, state); } - private void TryUpdateVisualState(EntityUid uid, JukeboxComponent? jukeboxComponent = null) + private void TryUpdateVisualState(Entity ent) { - if (!Resolve(uid, ref jukeboxComponent)) + if (!Resolve(ent, ref ent.Comp)) return; var finalState = JukeboxVisualState.On; - if (!this.IsPowered(uid, EntityManager)) + if (!this.IsPowered(ent, EntityManager)) { finalState = JukeboxVisualState.Off; } - _appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, finalState); + _appearanceSystem.SetData(ent, JukeboxVisuals.VisualState, finalState); + } + + /// + /// Set the selected track of the jukebox to the specified prototype. + /// + public void SetSelectedTrack(Entity ent, ProtoId track) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!Audio.IsPlaying(ent.Comp.AudioStream)) + { + ent.Comp.SelectedSongId = track; + DirectSetVisualState(ent, JukeboxVisualState.Select); + ent.Comp.Selecting = true; + ent.Comp.AudioStream = Audio.Stop(ent.Comp.AudioStream); + Dirty(ent); + } + } + + /// + /// Attempts to play the jukebox's current selected track. + /// + /// false if no track is selected or the track prototype cannot be found, otherwise true. + public bool TryPlay(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (Exists(ent.Comp.AudioStream)) + { + Audio.SetState(ent.Comp.AudioStream, AudioState.Playing); + } + else + { + if (string.IsNullOrEmpty(ent.Comp.SelectedSongId) || + !_protoManager.Resolve(ent.Comp.SelectedSongId, out var jukeboxProto)) + { + return false; + } + + ent.Comp.AudioStream = Audio.PlayPvs(jukeboxProto.Path, ent, AudioParams.Default.WithMaxDistance(10f))?.Entity; + Dirty(ent); + } + return true; + } + + /// + /// Stops any track that may currently be playing. + /// + public void Stop(Entity entity) + { + if (!Resolve(entity, ref entity.Comp, logMissing: false)) + return; + + Audio.SetState(entity.Comp.AudioStream, AudioState.Stopped); + } + + /// + /// Pauses any track that may currently be playing. + /// + public void Pause(Entity entity) + { + if (!Resolve(entity, ref entity.Comp, logMissing: false)) + return; + + Audio.SetState(entity.Comp.AudioStream, AudioState.Paused); + } + + /// + /// Sets the playback position within the current audio track. + /// + /// + /// If setting based on user input, you may need to compensate for the player's ping. + /// + public void SetTime(Entity entity, float songTime) + { + if (!Resolve(entity, ref entity.Comp)) + return; + + Audio.SetPlaybackPosition(entity.Comp.AudioStream, songTime); } } diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs deleted file mode 100644 index 0d96a584bde..00000000000 --- a/Content.Server/Bed/BedSystem.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Bed; -using Content.Shared.Bed.Components; -using Content.Shared.Bed.Sleep; -using Content.Shared.Buckle.Components; -using Content.Shared.Damage; -using Content.Shared.Imperial.Medieval.Skills; -using Content.Shared.Mobs.Systems; -using Content.Shared.Inventory; -using Content.Shared.Popups; -using Robust.Shared.Utility; -using Content.Server.Armor.Components; - -namespace Content.Server.Bed -{ - public sealed class BedSystem : SharedBedSystem - { - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - - private EntityQuery _sleepingQuery; - - public override void Initialize() - { - SubscribeLocalEvent(OnStrapped); - base.Initialize(); - - _sleepingQuery = GetEntityQuery(); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var bedComponent, out var strapComponent)) - { - if (Timing.CurTime < bedComponent.NextHealTime) - continue; - - bedComponent.NextHealTime += TimeSpan.FromSeconds(bedComponent.HealTime); - - if (strapComponent.BuckledEntities.Count == 0) - continue; - - foreach (var healedEntity in strapComponent.BuckledEntities) - { - if (_mobStateSystem.IsDead(healedEntity)) - continue; - - var damage = bedComponent.Damage; - - if (_sleepingQuery.HasComp(healedEntity)) - damage *= bedComponent.SleepMultiplier; - - _damageableSystem.TryChangeDamage(healedEntity, damage, true, origin: uid); - } - } - } - // imperial medieval sleep start - private void OnStrapped(Entity bed, ref StrappedEvent args) - { - if (!Timing.IsFirstTimePredicted) - return; - if (HasComp(args.Buckle)) - return; - EnsureComp(bed); - if (_inventorySystem.TryGetSlotEntity(args.Buckle.Owner, "outerClothing", out var existingOutfit) && !HasComp(existingOutfit.Value)) - { - var meta = EntityManager.GetComponent(existingOutfit.Value); - _popup.PopupEntity(Loc.GetString("Как неудобно спать в " + meta.EntityName), args.Buckle.Owner); - return; - } - - if (_inventorySystem.TryGetSlotEntity(args.Buckle.Owner, "head", out var existingHead) && !HasComp(existingHead.Value)) - { - var meta = EntityManager.GetComponent(existingHead.Value); - _popup.PopupEntity(Loc.GetString("Как неудобно спать в " + meta.EntityName), args.Buckle.Owner); - return; - } - bed.Comp.NextHealTime = Timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime); - _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed); - if (TryComp(args.Buckle, out var skills)) - _actionsSystem.SetCooldown(bed.Comp.SleepAction, TimeSpan.FromSeconds(10 - (skills.Levels[SharedSkillsSystem.VitalityId]-10) * 0.5)); - Dirty(bed); - - // Single action entity, cannot strap multiple entities to the same bed. - //DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1) - } - // imperial medieval sleep end - } -} diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs index 2aabb5eb270..5ce37276b78 100644 --- a/Content.Server/Bible/BibleSystem.cs +++ b/Content.Server/Bible/BibleSystem.cs @@ -4,7 +4,7 @@ using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Bible; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Ghost.Roles.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -14,8 +14,8 @@ using Content.Shared.Popups; using Content.Shared.Timing; using Content.Shared.Verbs; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Random; @@ -115,15 +115,18 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter return; } + var userEnt = Identity.Entity(args.User, EntityManager); + var targetEnt = Identity.Entity(args.Target.Value, EntityManager); + // This only has a chance to fail if the target is not wearing anything on their head and is not a familiar. - if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp(args.Target.Value)) + if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out _) && !HasComp(args.Target.Value)) { if (_random.Prob(component.FailChance)) { - var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); + var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); _popupSystem.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); - var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); + var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", targetEnt), ("bible", uid)); _popupSystem.PopupEntity(selfFailMessage, args.User, args.User, PopupType.MediumCaution); _audio.PlayPvs(component.BibleHitSound, args.User); @@ -133,26 +136,28 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter } } - var damage = _damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid); + string othersMessage; + string selfMessage; - if (damage == null || damage.Empty) + if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid)) { - var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); - _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); + othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); + selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", targetEnt), ("bible", uid)); + + _audio.PlayPvs(component.HealSoundPath, args.User); + _delay.TryResetDelay((uid, useDelay)); - var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); - _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); + if (component.HealingLightEffect.HasValue) + Spawn(component.HealingLightEffect.Value, new EntityCoordinates(args.Target.Value, default)); } else { - var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); - _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); - - var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); - _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); - _audio.PlayPvs(component.HealSoundPath, args.User); - _delay.TryResetDelay((uid, useDelay)); + othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); + selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", targetEnt), ("bible", uid)); } + + _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); + _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); } private void AddSummonVerb(EntityUid uid, SummonableComponent component, GetVerbsEvent args) diff --git a/Content.Server/Bible/Components/BibleComponent.cs b/Content.Server/Bible/Components/BibleComponent.cs index 7f4af569e5f..af600158e93 100644 --- a/Content.Server/Bible/Components/BibleComponent.cs +++ b/Content.Server/Bible/Components/BibleComponent.cs @@ -53,5 +53,11 @@ public sealed partial class BibleComponent : Component [DataField("locPrefix")] public string LocPrefix = "bible"; + + /// + /// A short light effect to display when successfully healing someone + /// + [DataField] + public EntProtoId? HealingLightEffect = "HolyLightEffect"; } } diff --git a/Content.Server/Body/Commands/AddHandCommand.cs b/Content.Server/Body/Commands/AddHandCommand.cs index 4d2805333a7..ae8d6f69ebe 100644 --- a/Content.Server/Body/Commands/AddHandCommand.cs +++ b/Content.Server/Body/Commands/AddHandCommand.cs @@ -1,13 +1,8 @@ -using System.Linq; using Content.Server.Administration; -using Content.Server.Body.Systems; using Content.Server.Hands.Systems; using Content.Shared.Administration; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; using Content.Shared.Hands.Components; using Robust.Shared.Console; -using Robust.Shared.Prototypes; namespace Content.Server.Body.Commands { @@ -15,21 +10,18 @@ namespace Content.Server.Body.Commands sealed class AddHandCommand : IConsoleCommand { [Dependency] private readonly IEntityManager _entManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - private static readonly EntProtoId DefaultHandPrototype = "LeftHandHuman"; private static int _handIdAccumulator; public string Command => "addhand"; public string Description => "Adds a hand to your entity."; - public string Help => $"Usage: {Command} / {Command} / {Command} / {Command}"; + public string Help => $"Usage: {Command} "; public void Execute(IConsoleShell shell, string argStr, string[] args) { var player = shell.Player; EntityUid entity; - EntityUid hand; switch (args.Length) { @@ -47,7 +39,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } entity = player.AttachedEntity.Value; - hand = _entManager.SpawnEntity(DefaultHandPrototype, _entManager.GetComponent(entity).Coordinates); break; case 1: { @@ -60,7 +51,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } entity = uid.Value; - hand = _entManager.SpawnEntity(DefaultHandPrototype, _entManager.GetComponent(entity).Coordinates); } else { @@ -77,35 +67,8 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } entity = player.AttachedEntity.Value; - hand = _entManager.SpawnEntity(args[0], _entManager.GetComponent(entity).Coordinates); } - break; - } - case 2: - { - if (!NetEntity.TryParse(args[0], out var netEnt) || !_entManager.TryGetEntity(netEnt, out var uid)) - { - shell.WriteLine($"{args[0]} is not a valid entity uid."); - return; - } - - if (!_entManager.EntityExists(uid)) - { - shell.WriteLine($"No entity exists with uid {uid}."); - return; - } - - entity = uid.Value; - - if (!_protoManager.HasIndex(args[1])) - { - shell.WriteLine($"No hand entity exists with id {args[1]}."); - return; - } - - hand = _entManager.SpawnEntity(args[1], _entManager.GetComponent(entity).Coordinates); - break; } default: @@ -113,41 +76,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; } - if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null) - { - var location = _entManager.GetComponentOrNull(hand)?.Symmetry switch - { - BodyPartSymmetry.None => HandLocation.Middle, - BodyPartSymmetry.Left => HandLocation.Left, - BodyPartSymmetry.Right => HandLocation.Right, - _ => HandLocation.Right - }; - _entManager.DeleteEntity(hand); - - // You have no body and you must scream. - _entManager.System().AddHand(entity, $"{hand}-cmd-{_handIdAccumulator++}", location); - return; - } - - if (!_entManager.TryGetComponent(hand, out BodyPartComponent? part)) - { - shell.WriteLine($"Hand entity {hand} does not have a {nameof(BodyPartComponent)} component."); - return; - } - - var bodySystem = _entManager.System(); - - var attachAt = bodySystem.GetBodyChildrenOfType(entity, BodyPartType.Arm, body).FirstOrDefault(); - if (attachAt == default) - attachAt = bodySystem.GetBodyChildren(entity, body).First(); - - var slotId = part.GetHashCode().ToString(); - - if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand, attachAt.Component, part)) - { - shell.WriteError($"Couldn't create a slot with id {slotId} on entity {_entManager.ToPrettyString(entity)}"); - return; - } + _entManager.System().AddHand(entity, $"cmd-{_handIdAccumulator++}", HandLocation.Middle); shell.WriteLine($"Added hand to entity {_entManager.GetComponent(entity).EntityName}"); } diff --git a/Content.Server/Body/Commands/AttachBodyPartCommand.cs b/Content.Server/Body/Commands/AttachBodyPartCommand.cs deleted file mode 100644 index 9172e9e87ec..00000000000 --- a/Content.Server/Body/Commands/AttachBodyPartCommand.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Content.Server.Administration; -using Content.Server.Body.Systems; -using Content.Shared.Administration; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Robust.Shared.Console; - -namespace Content.Server.Body.Commands -{ - [AdminCommand(AdminFlags.Fun)] - public sealed class AttachBodyPartCommand : IConsoleCommand - { - [Dependency] private readonly IEntityManager _entManager = default!; - - public string Command => "attachbodypart"; - public string Description => "Attaches a body part to you or someone else."; - public string Help => $"{Command} / {Command} "; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - - EntityUid bodyId; - EntityUid? partUid; - - switch (args.Length) - { - case 1: - if (player == null) - { - shell.WriteLine($"You need to specify an entity to attach the part to if you aren't a player.\n{Help}"); - return; - } - - if (player.AttachedEntity == null) - { - shell.WriteLine($"You need to specify an entity to attach the part to if you aren't attached to an entity.\n{Help}"); - return; - } - - if (!NetEntity.TryParse(args[0], out var partNet) || !_entManager.TryGetEntity(partNet, out partUid)) - { - shell.WriteLine($"{args[0]} is not a valid entity uid."); - return; - } - - bodyId = player.AttachedEntity.Value; - - break; - case 2: - if (!NetEntity.TryParse(args[0], out var entityNet) || !_entManager.TryGetEntity(entityNet, out var entityUid)) - { - shell.WriteLine($"{args[0]} is not a valid entity uid."); - return; - } - - if (!NetEntity.TryParse(args[1], out partNet) || !_entManager.TryGetEntity(partNet, out partUid)) - { - shell.WriteLine($"{args[1]} is not a valid entity uid."); - return; - } - - if (!_entManager.EntityExists(entityUid)) - { - shell.WriteLine($"{entityUid} is not a valid entity."); - return; - } - - bodyId = entityUid.Value; - break; - default: - shell.WriteLine(Help); - return; - } - - if (!_entManager.TryGetComponent(bodyId, out BodyComponent? body)) - { - shell.WriteLine($"Entity {_entManager.GetComponent(bodyId).EntityName} with uid {bodyId} does not have a {nameof(BodyComponent)}."); - return; - } - - if (!_entManager.EntityExists(partUid)) - { - shell.WriteLine($"{partUid} is not a valid entity."); - return; - } - - if (!_entManager.TryGetComponent(partUid, out BodyPartComponent? part)) - { - shell.WriteLine($"Entity {_entManager.GetComponent(partUid.Value).EntityName} with uid {args[0]} does not have a {nameof(BodyPartComponent)}."); - return; - } - - var bodySystem = _entManager.System(); - if (bodySystem.BodyHasChild(bodyId, partUid.Value, body, part)) - { - shell.WriteLine($"Body part {_entManager.GetComponent(partUid.Value).EntityName} with uid {partUid} is already attached to entity {_entManager.GetComponent(bodyId).EntityName} with uid {bodyId}"); - return; - } - - var slotId = $"AttachBodyPartVerb-{partUid}"; - - if (body.RootContainer.ContainedEntity is null && !bodySystem.AttachPartToRoot(bodyId, partUid.Value, body, part)) - { - shell.WriteError("Body container does not have a root entity to attach to the body part!"); - return; - } - - var (rootPartId, rootPart) = bodySystem.GetRootPartOrNull(bodyId, body)!.Value; - if (!bodySystem.TryCreatePartSlotAndAttach(rootPartId, - slotId, - partUid.Value, - part.PartType, - rootPart, - part)) - { - shell.WriteError($"Could not create slot {slotId} on entity {_entManager.ToPrettyString(bodyId)}"); - return; - } - shell.WriteLine($"Attached part {_entManager.ToPrettyString(partUid.Value)} to {_entManager.ToPrettyString(bodyId)}"); - } - } -} diff --git a/Content.Server/Body/Commands/DestroyMechanismCommand.cs b/Content.Server/Body/Commands/DestroyMechanismCommand.cs deleted file mode 100644 index e3278fd4e72..00000000000 --- a/Content.Server/Body/Commands/DestroyMechanismCommand.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Content.Server.Administration; -using Content.Server.Body.Systems; -using Content.Shared.Administration; -using Content.Shared.Body.Components; -using Robust.Shared.Console; - -namespace Content.Server.Body.Commands -{ - [AdminCommand(AdminFlags.Fun)] - internal sealed class DestroyMechanismCommand : LocalizedEntityCommands - { - [Dependency] private readonly IComponentFactory _compFactory = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; - - public override string Command => "destroymechanism"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - if (player == null) - { - shell.WriteLine(Loc.GetString($"shell-only-players-can-run-this-command")); - return; - } - - if (args.Length == 0) - { - shell.WriteLine(Help); - return; - } - - if (player.AttachedEntity is not {} attached) - { - shell.WriteLine(Loc.GetString($"shell-must-be-attached-to-entity")); - return; - } - - if (!EntityManager.TryGetComponent(attached, out BodyComponent? body)) - { - shell.WriteLine(Loc.GetString($"shell-must-have-body")); - return; - } - - var mechanismName = string.Join(" ", args).ToLowerInvariant(); - - foreach (var organ in _bodySystem.GetBodyOrgans(attached, body)) - { - if (_compFactory.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName) - { - EntityManager.QueueDeleteEntity(organ.Id); - shell.WriteLine(Loc.GetString($"cmd-destroymechanism-success", ("name", mechanismName))); - return; - } - } - - shell.WriteLine(Loc.GetString($"cmd-destroymechanism-no-mechanism-found", ("name", mechanismName))); - } - } -} diff --git a/Content.Server/Body/Commands/RemoveHandCommand.cs b/Content.Server/Body/Commands/RemoveHandCommand.cs deleted file mode 100644 index 4a2956ae7ab..00000000000 --- a/Content.Server/Body/Commands/RemoveHandCommand.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using Content.Server.Administration; -using Content.Server.Body.Systems; -using Content.Shared.Administration; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Robust.Shared.Console; -using Robust.Shared.Random; - -namespace Content.Server.Body.Commands -{ - [AdminCommand(AdminFlags.Fun)] - public sealed class RemoveHandCommand : IConsoleCommand - { - [Dependency] private readonly IEntityManager _entManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - public string Command => "removehand"; - public string Description => "Removes a hand from your entity."; - public string Help => $"Usage: {Command}"; - - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - if (player == null) - { - shell.WriteLine("Only a player can run this command."); - return; - } - - if (player.AttachedEntity == null) - { - shell.WriteLine("You have no entity."); - return; - } - - if (!_entManager.TryGetComponent(player.AttachedEntity, out BodyComponent? body)) - { - var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}"; - - shell.WriteLine(text); - return; - } - - var bodySystem = _entManager.System(); - var hand = bodySystem.GetBodyChildrenOfType(player.AttachedEntity.Value, BodyPartType.Hand, body).FirstOrDefault(); - - if (hand == default) - { - shell.WriteLine("You have no hands."); - } - else - { - _entManager.System().AttachToGridOrMap(hand.Id); - } - } - } -} diff --git a/Content.Server/Body/Components/BrainComponent.cs b/Content.Server/Body/Components/BrainComponent.cs deleted file mode 100644 index 004ff24eaff..00000000000 --- a/Content.Server/Body/Components/BrainComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.Body.Systems; - -namespace Content.Server.Body.Components -{ - [RegisterComponent, Access(typeof(BrainSystem))] - public sealed partial class BrainComponent : Component - { - } -} diff --git a/Content.Server/Body/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs deleted file mode 100644 index 5821b0d9447..00000000000 --- a/Content.Server/Body/Components/MetabolizerComponent.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Content.Shared.Body.Components; -using Content.Server.Body.Systems; -using Content.Shared.Body.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Body.Components -{ - /// - /// Handles metabolizing various reagents with given effects. - /// - [RegisterComponent, Access(typeof(MetabolizerSystem))] - public sealed partial class MetabolizerComponent : Component - { - /// - /// The next time that reagents will be metabolized. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// How often to metabolize reagents. - /// - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); - - /// - /// Multiplier applied to for adjusting based on metabolic rate multiplier. - /// - [DataField] - public float UpdateIntervalMultiplier = 1f; - - /// - /// Adjusted update interval based off of the multiplier value. - /// - [ViewVariables] - public TimeSpan AdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier; - - /// - /// From which solution will this metabolizer attempt to metabolize chemicals - /// - [DataField("solution")] - public string SolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; - - /// - /// Does this component use a solution on it's parent entity (the body) or itself - /// - /// - /// Most things will use the parent entity (bloodstream). - /// - [DataField] - public bool SolutionOnBody = true; - - /// - /// List of metabolizer types that this organ is. ex. Human, Slime, Felinid, w/e. - /// - [DataField] - [Access(typeof(MetabolizerSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public HashSet>? MetabolizerTypes; - - /// - /// Should this metabolizer remove chemicals that have no metabolisms defined? - /// As a stop-gap, basically. - /// - [DataField] - public bool RemoveEmpty; - - /// - /// How many reagents can this metabolizer process at once? - /// Used to nerf 'stacked poisons' where having 5+ different poisons in a syringe, even at low - /// quantity, would be muuuuch better than just one poison acting. - /// - [DataField("maxReagents")] - public int MaxReagentsProcessable = 3; - - /// - /// A list of metabolism groups that this metabolizer will act on, in order of precedence. - /// - [DataField("groups")] - public List? MetabolismGroups; - } - - /// - /// Contains data about how a metabolizer will metabolize a single group. - /// This allows metabolizers to remove certain groups much faster, or not at all. - /// - [DataDefinition] - public sealed partial class MetabolismGroupEntry - { - [DataField(required: true)] - public ProtoId Id; - - [DataField("rateModifier")] - public FixedPoint2 MetabolismRateModifier = 1.0; - } -} diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index 90122c6a147..c56084b2890 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -30,23 +30,26 @@ public override void Initialize() private void OnComponentInit(Entity entity, ref ComponentInit args) { if (!SolutionContainer.EnsureSolution(entity.Owner, - entity.Comp.ChemicalSolutionName, - out var chemicalSolution) || - !SolutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName, out var bloodSolution) || !SolutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName, - out var tempSolution)) + out var tempSolution) || + !SolutionContainer.EnsureSolution(entity.Owner, + entity.Comp.MetabolitesSolutionName, + out var metabolitesSolution)) return; - chemicalSolution.MaxVolume = entity.Comp.ChemicalMaxVolume; - bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume; + bloodSolution.MaxVolume = entity.Comp.BloodReferenceSolution.Volume * entity.Comp.MaxVolumeModifier; + metabolitesSolution.MaxVolume = bloodSolution.MaxVolume; tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well + entity.Comp.BloodReferenceSolution.SetReagentData(GetEntityBloodData((entity, entity.Comp))); // Fill blood solution with BLOOD // The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription - bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume); + var solution = entity.Comp.BloodReferenceSolution.Clone(); + solution.ScaleTo(entity.Comp.BloodReferenceSolution.Volume - bloodSolution.Volume); + bloodSolution.AddSolution(solution, PrototypeManager); } // forensics is not predicted yet @@ -54,11 +57,14 @@ private void OnDnaGenerated(Entity entity, ref GenerateDna { if (SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)) { + var data = NewEntityBloodData(entity); + entity.Comp.BloodReferenceSolution.SetReagentData(data); + foreach (var reagent in bloodSolution.Contents) { List reagentData = reagent.Reagent.EnsureReagentData(); reagentData.RemoveAll(x => x is DnaData); - reagentData.AddRange(GetEntityBloodData(entity.Owner)); + reagentData.AddRange(data); } } else diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs deleted file mode 100644 index 957dea9d418..00000000000 --- a/Content.Server/Body/Systems/BodySystem.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Numerics; -using Content.Server.Ghost; -using Content.Server.Humanoid; -using Content.Shared.Body.Components; -using Content.Shared.Body.Events; -using Content.Shared.Body.Part; -using Content.Shared.Body.Systems; -using Content.Shared.Damage.Components; -using Content.Shared.Humanoid; -using Content.Shared.Mind; -using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Events; -using Content.Shared.Movement.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Timing; - -namespace Content.Server.Body.Systems; - -public sealed class BodySystem : SharedBodySystem -{ - [Dependency] private readonly GhostSystem _ghostSystem = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRelayMoveInput); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnRelayMoveInput(Entity ent, ref MoveInputEvent args) - { - // If they haven't actually moved then ignore it. - if ((args.Entity.Comp.HeldMoveButtons & - (MoveButtons.Down | MoveButtons.Left | MoveButtons.Up | MoveButtons.Right)) == 0x0) - { - return; - } - - if (_mobState.IsDead(ent) && _mindSystem.TryGetMind(ent, out var mindId, out var mind)) - { - mind.TimeOfDeath ??= _gameTiming.RealTime; - _ghostSystem.OnGhostAttempt(mindId, canReturnGlobal: true, mind: mind); - } - } - - private void OnApplyMetabolicMultiplier( - Entity ent, - ref ApplyMetabolicMultiplierEvent args) - { - foreach (var organ in GetBodyOrgans(ent, ent)) - { - RaiseLocalEvent(organ.Id, ref args); - } - } - - protected override void AddPart( - Entity bodyEnt, - Entity partEnt, - string slotId) - { - // TODO: Predict this probably. - base.AddPart(bodyEnt, partEnt, slotId); - - var layer = partEnt.Comp.ToHumanoidLayers(); - if (layer != null) - { - var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value); - _humanoidSystem.SetLayersVisibility(bodyEnt.Owner, layers, visible: true); - } - } - - protected override void RemovePart( - Entity bodyEnt, - Entity partEnt, - string slotId) - { - base.RemovePart(bodyEnt, partEnt, slotId); - - if (!TryComp(bodyEnt, out var humanoid)) - return; - - var layer = partEnt.Comp.ToHumanoidLayers(); - - if (layer is null) - return; - - var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value); - _humanoidSystem.SetLayersVisibility((bodyEnt, humanoid), layers, visible: false); - } - - public override HashSet GibBody( - EntityUid bodyId, - bool gibOrgans = false, - BodyComponent? body = null, - bool launchGibs = true, - Vector2? splatDirection = null, - float splatModifier = 1, - Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null - ) - { - if (!Resolve(bodyId, ref body, logMissing: false) - || TerminatingOrDeleted(bodyId) - || EntityManager.IsQueuedForDeletion(bodyId)) - { - return new HashSet(); - } - - if (HasComp(bodyId)) - return new HashSet(); - - var xform = Transform(bodyId); - if (xform.MapUid is null) - return new HashSet(); - - var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs, - splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone); - - var ev = new BeingGibbedEvent(gibs); - RaiseLocalEvent(bodyId, ref ev); - - QueueDel(bodyId); - - return gibs; - } -} diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index 9110e18a9d4..2a0d94923d6 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -1,14 +1,9 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Popups; using Content.Shared.Alert; -using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; -using Content.Shared.DoAfter; using Content.Shared.Internals; -using Content.Shared.Inventory; using Content.Shared.Roles; namespace Content.Server.Body.Systems; @@ -59,7 +54,7 @@ private void OnInhaleLocation(Entity ent, ref InhaleLocation if (AreInternalsWorking(ent)) { var gasTank = Comp(ent.Comp.GasTankEntity!.Value); - args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), args.Respirator.BreathVolume); + args.Gas = _gasTank.RemoveAirOutput((ent.Comp.GasTankEntity.Value, gasTank), args.Respirator.BreathVolume); // Imperial Medieval: RemoveAirVolume -> RemoveAirOutput // TODO: Should listen to gas tank updates instead I guess? //_alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent)); # Imperial Medieval } diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs deleted file mode 100644 index 13af0748b3c..00000000000 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Content.Server.Body.Components; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Events; -using Content.Shared.Body.Organ; -using Content.Shared.Body.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Robust.Shared.Collections; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; -using Content.Server.Imperial.Medieval.Plague; - -namespace Content.Server.Body.Systems -{ - /// - public sealed class MetabolizerSystem : SharedMetabolizerSystem - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - - private EntityQuery _organQuery; - private EntityQuery _solutionQuery; - - public override void Initialize() - { - base.Initialize(); - - _organQuery = GetEntityQuery(); - _solutionQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnMetabolizerInit); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - private void OnMetabolizerInit(Entity entity, ref ComponentInit args) - { - if (!entity.Comp.SolutionOnBody) - { - _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _); - } - else if (_organQuery.CompOrNull(entity)?.Body is { } body) - { - _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _); - } - } - - private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args) - { - ent.Comp.UpdateIntervalMultiplier = args.Multiplier; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count()); - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var comp)) - { - metabolizers.Add((uid, comp)); - } - - foreach (var (uid, metab) in metabolizers) - { - // Only update as frequently as it should - if (_gameTiming.CurTime < metab.NextUpdate) - continue; - - metab.NextUpdate += metab.AdjustedUpdateInterval; - TryMetabolize((uid, metab)); - } - } - - private void TryMetabolize(Entity ent) - { - _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false); - - // First step is get the solution we actually care about - var solutionName = ent.Comp1.SolutionName; - Solution? solution = null; - Entity? soln = default!; - EntityUid? solutionEntityUid = null; - - if (ent.Comp1.SolutionOnBody) - { - if (ent.Comp2?.Body is { } body) - { - if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution); - solutionEntityUid = body; - } - } - else - { - if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false)) - return; - - _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution); - solutionEntityUid = ent; - } - - if (solutionEntityUid is null - || soln is null - || solution is null - || solution.Contents.Count == 0) - { - return; - } - - // randomize the reagent list so we don't have any weird quirks - // like alphabetical order or insertion order mattering for processing - var list = solution.Contents.ToArray(); - _random.Shuffle(list); - - int reagents = 0; - foreach (var (reagent, quantity) in list) - { - if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto)) - continue; - - var mostToRemove = FixedPoint2.Zero; - if (proto.Metabolisms is null) - { - if (ent.Comp1.RemoveEmpty) - { - solution.RemoveReagent(reagent, FixedPoint2.New(1)); - } - - continue; - } - - // we're done here entirely if this is true - if (reagents >= ent.Comp1.MaxReagentsProcessable) - return; - - - // loop over all our groups and see which ones apply - if (ent.Comp1.MetabolismGroups is null) - continue; - - // Imperial Medieval start - var ev = new ReagentMetabolizingEvent(reagent.Prototype); - RaiseLocalEvent(ent.Owner, ref ev); - // Imperial Medievel end - - foreach (var group in ent.Comp1.MetabolismGroups) - { - if (!proto.Metabolisms.TryGetValue(group.Id, out var entry)) - continue; - - var rate = entry.MetabolismRate * group.MetabolismRateModifier; - - // Remove $rate, as long as there's enough reagent there to actually remove that much - mostToRemove = FixedPoint2.Clamp(rate, 0, quantity); - - float scale = (float) mostToRemove / (float) rate; - - // if it's possible for them to be dead, and they are, - // then we shouldn't process any effects, but should probably - // still remove reagents - if (TryComp(solutionEntityUid.Value, out var state)) - { - if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state)) - continue; - } - - var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value; - var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale); - - // do all effects, if conditions apply - foreach (var effect in entry.Effects) - { - if (!effect.ShouldApply(args, _random)) - continue; - - if (effect.ShouldLog) - { - _adminLogger.Add( - LogType.ReagentEffect, - effect.LogImpact, - $"Metabolism effect {effect.GetType().Name:effect}" - + $" of reagent {proto.LocalizedName:reagent}" - + $" applied on entity {actualEntity:entity}" - + $" at {Transform(actualEntity).Coordinates:coordinates}" - ); - } - - effect.Effect(args); - } - } - - // remove a certain amount of reagent - if (mostToRemove > FixedPoint2.Zero) - { - solution.RemoveReagent(reagent, mostToRemove); - - // We have processed a reagant, so count it towards the cap - reagents += 1; - } - } - - _solutionContainerSystem.UpdateChemicals(soln.Value); - } - } -} diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 0e9f1ef741f..63e5b84f104 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -2,20 +2,24 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Components; using Content.Server.Chat.Systems; -using Content.Server.EntityEffects; +using Content.Shared.Body.Systems; using Content.Shared.Alert; using Content.Shared.Atmos; +using Content.Shared.Body; using Content.Shared.Body.Components; using Content.Shared.Body.Events; -using Content.Shared.Body.Prototypes; +using Content.Shared.Chat; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; +using Content.Shared.EntityConditions; +using Content.Shared.EntityConditions.Conditions.Body; using Content.Shared.EntityEffects; -using Content.Shared.EntityEffects.EffectConditions; -using Content.Shared.EntityEffects.Effects; +using Content.Shared.EntityEffects.Effects.Body; +using Content.Shared.EntityEffects.Effects.Damage; +using Content.Shared.Metabolism; using Content.Shared.Mobs.Systems; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -31,18 +35,18 @@ public sealed class RespiratorSystem : EntitySystem { [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosSys = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly DamageableSystem _damageableSys = default!; [Dependency] private readonly LungSystem _lungSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly EntityEffectSystem _entityEffect = default!; - private static readonly ProtoId GasId = new("Gas"); + private static readonly ProtoId RespirationStage = new("Respiration"); public override void Initialize() { @@ -54,11 +58,17 @@ public override void Initialize() SubscribeLocalEvent(OnApplyMetabolicMultiplier); // BodyComp stuff - SubscribeLocalEvent(OnGasInhaled); - SubscribeLocalEvent(OnGasExhaled); - SubscribeLocalEvent(CanBodyMetabolizeGas); - SubscribeLocalEvent(OnSuffocation); - SubscribeLocalEvent(OnStopSuffocating); + SubscribeLocalEvent(_body.RelayEvent); + SubscribeLocalEvent(_body.RelayEvent); + SubscribeLocalEvent(_body.RelayEvent); + SubscribeLocalEvent(_body.RelayEvent); + SubscribeLocalEvent(_body.RelayEvent); + + SubscribeLocalEvent>(OnGasInhaled); + SubscribeLocalEvent>(OnGasExhaled); + SubscribeLocalEvent>(CanBodyMetabolizeGas); + SubscribeLocalEvent>(OnSuffocation); + SubscribeLocalEvent>(OnStopSuffocating); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -197,7 +207,7 @@ public bool IsBreathing(Entity ent) /// Returns true only if the air is not toxic, and it wouldn't suffocate. public bool CanMetabolizeInhaledAir(Entity ent) { - if (!Resolve(ent, ref ent.Comp)) + if (!Resolve(ent, ref ent.Comp, false)) return false; // Get the gas at our location but don't actually remove it from the gas mixture. @@ -236,72 +246,22 @@ public bool CanMetabolizeInhaledAir(Entity ent, GasMixture /// /// Tries to safely metabolize the current solutions in a body's lungs. /// - private void CanBodyMetabolizeGas(Entity ent, ref CanMetabolizeGasEvent args) + private void CanBodyMetabolizeGas(Entity ent, ref BodyRelayedEvent args) { - if (args.Handled) + if (args.Args.Handled) return; - var organs = _bodySystem.GetBodyOrganEntityComps((ent, null)); - if (organs.Count == 0) - return; - - var solution = _lungSystem.GasToReagent(args.Gas); + var solution = _lungSystem.GasToReagent(args.Args.Gas); var saturation = 0f; - foreach (var organ in organs) - { - saturation += GetSaturation(solution, organ.Owner, out var toxic); - if (!toxic) - continue; - - args.Handled = true; - args.Toxic = true; - return; - } - - args.Handled = true; - args.Saturation = saturation; - } - - public bool TryInhaleGasToBody(Entity entity, GasMixture gas) - { - if (!Resolve(entity, ref entity.Comp)) - return false; - - var organs = _bodySystem.GetBodyOrganEntityComps((entity, entity.Comp)); - if (organs.Count == 0) - return false; - - var lungRatio = 1.0f / organs.Count; - var splitGas = organs.Count == 1 ? gas : gas.RemoveRatio(lungRatio); - foreach (var (organUid, lung, _) in organs) - { - // Merge doesn't remove gas from the giver. - _atmosSys.Merge(lung.Air, splitGas); - _lungSystem.GasToReagent(organUid, lung); - } - - return true; - } - - public void RemoveGasFromBody(Entity ent, GasMixture gas) - { - var outGas = new GasMixture(gas.Volume); + saturation += GetSaturation(solution, ent.Owner, out var toxic); - var organs = _bodySystem.GetBodyOrganEntityComps((ent, ent.Comp)); - if (organs.Count == 0) - return; - - foreach (var (organUid, lung, _) in organs) + args.Args = args.Args with { - _atmosSys.Merge(outGas, lung.Air); - lung.Air.Clear(); - - if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution)) - _solutionContainerSystem.RemoveAllSolution(lung.Solution.Value); - } - - _atmosSys.Merge(gas, outGas); + Saturation = saturation, + Toxic = toxic, + Handled = true + }; } /// @@ -320,7 +280,7 @@ private float GetSaturation(Solution solution, Entity lun if (!Resolve(lung, ref lung.Comp)) return 0; - if (lung.Comp.MetabolismGroups == null) + if (lung.Comp.Stages == null) return 0; float saturation = 0; @@ -330,7 +290,7 @@ private float GetSaturation(Solution solution, Entity lun if (reagent.Metabolisms == null) continue; - if (!reagent.Metabolisms.TryGetValue(GasId, out var entry)) + if (!reagent.Metabolisms.Metabolisms.TryGetValue(RespirationStage, out var entry)) continue; foreach (var effect in entry.Effects) @@ -342,7 +302,6 @@ private float GetSaturation(Solution solution, Entity lun } } - // TODO generalize condition checks // this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture // Applying actual reaction effects require a full ReagentEffectArgs struct. bool CanMetabolize(EntityEffect effect) @@ -350,9 +309,10 @@ bool CanMetabolize(EntityEffect effect) if (effect.Conditions == null) return true; + // TODO: Use Metabolism Public API to do this instead, once that API has been built. foreach (var cond in effect.Conditions) { - if (cond is OrganType organ && !_entityEffect.OrganCondition(organ, lung)) + if (cond is MetabolizerTypeCondition organ && !_entityConditions.TryCondition(lung, organ)) return false; } @@ -372,7 +332,7 @@ private void TakeSuffocationDamage(Entity ent) RaiseLocalEvent(ent, ref evmdv); // Imperial Medieval Skills end - _damageableSys.TryChangeDamage(ent, ent.Comp.Damage * evmdv.Modifier, interruptsDoAfters: false); // Imperial Medieval - modifier added + _damageableSys.TryChangeDamage(ent.Owner, ent.Comp.Damage * evmdv.Modifier, interruptsDoAfters: false); // Imperial Medieval - Entity -> Owner if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold) return; @@ -386,30 +346,20 @@ private void StopSuffocation(Entity ent) if (ent.Comp.SuffocationCycles >= 2) _adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating"); - _damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery); + _damageableSys.ChangeDamage(ent.Owner, ent.Comp.DamageRecovery); var ev = new StopSuffocatingEvent(); RaiseLocalEvent(ent, ref ev); } - private void OnSuffocation(Entity ent, ref SuffocationEvent args) + private void OnSuffocation(Entity ent, ref BodyRelayedEvent args) { - // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility - var organs = _bodySystem.GetBodyOrganEntityComps((ent, null)); - foreach (var entity in organs) - { - _alertsSystem.ShowAlert(ent.Owner, entity.Comp1.Alert); - } + _alertsSystem.ShowAlert(args.Body.Owner, ent.Comp.Alert); } - private void OnStopSuffocating(Entity ent, ref StopSuffocatingEvent args) + private void OnStopSuffocating(Entity ent, ref BodyRelayedEvent args) { - // TODO: This is not going work with multiple different lungs, if that ever becomes a possibility - var organs = _bodySystem.GetBodyOrganEntityComps((ent, null)); - foreach (var entity in organs) - { - _alertsSystem.ClearAlert(ent.Owner, entity.Comp1.Alert); - } + _alertsSystem.ClearAlert(args.Body.Owner, ent.Comp.Alert); } public void UpdateSaturation(EntityUid uid, float amount, RespiratorComponent? respirator = null) @@ -427,24 +377,33 @@ private void OnApplyMetabolicMultiplier(Entity ent, ref App ent.Comp.UpdateIntervalMultiplier = args.Multiplier; } - private void OnGasInhaled(Entity entity, ref InhaledGasEvent args) + private void OnGasInhaled(Entity ent, ref BodyRelayedEvent args) { - if (args.Handled) + if (args.Args.Handled) return; - args.Handled = true; + _atmosSys.Merge(ent.Comp.Air, args.Args.Gas); + _lungSystem.GasToReagent(ent, ent); - args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas); + args.Args = args.Args with + { + Handled = true, + Succeeded = true + }; } - private void OnGasExhaled(Entity entity, ref ExhaledGasEvent args) + private void OnGasExhaled(Entity ent, ref BodyRelayedEvent args) { - if (args.Handled) - return; + _atmosSys.Merge(args.Args.Gas, ent.Comp.Air); + ent.Comp.Air.Clear(); - args.Handled = true; + if (_solutionContainerSystem.ResolveSolution(ent.Owner, ent.Comp.SolutionName, ref ent.Comp.Solution)) + _solutionContainerSystem.RemoveAllSolution(ent.Comp.Solution.Value); - RemoveGasFromBody(entity, args.Gas); + args.Args = args.Args with + { + Handled = true + }; } // Imperial Medieval start diff --git a/Content.Server/Body/Systems/ThermalRegulatorSystem.cs b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs index 3ba9e6af318..af7b17643e9 100644 --- a/Content.Server/Body/Systems/ThermalRegulatorSystem.cs +++ b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Body.Components; -using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; using Content.Shared.ActionBlocker; +using Content.Shared.Temperature.Components; using Robust.Shared.Timing; namespace Content.Server.Body.Systems; diff --git a/Content.Server/Body/VisualBodySystem.cs b/Content.Server/Body/VisualBodySystem.cs new file mode 100644 index 00000000000..4cb1f91a0be --- /dev/null +++ b/Content.Server/Body/VisualBodySystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Body; + +namespace Content.Server.Body; + +public sealed partial class VisualBodySystem : SharedVisualBodySystem; diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs index ee7ca4f5846..a01d96a8781 100644 --- a/Content.Server/Botany/SeedPrototype.cs +++ b/Content.Server/Botany/SeedPrototype.cs @@ -1,8 +1,6 @@ -using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; -using Content.Server.EntityEffects; using Content.Shared.Atmos; using Content.Shared.Database; +using Content.Shared.FixedPoint; using Content.Shared.Random; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -60,18 +58,18 @@ public partial struct SeedChemQuantity /// /// Minimum amount of chemical that is added to produce, regardless of the potency /// - [DataField("Min")] public int Min; + [DataField("Min")] public FixedPoint2 Min = FixedPoint2.Epsilon; /// /// Maximum amount of chemical that can be produced after taking plant potency into account. /// - [DataField("Max")] public int Max; + [DataField("Max")] public FixedPoint2 Max; /// /// When chemicals are added to produce, the potency of the seed is divided with this value. Final chemical amount is the result plus the `Min` value. /// Example: PotencyDivisor of 20 with seed potency of 55 results in 2.75, 55/20 = 2.75. If minimum is 1 then final result will be 3.75 of that chemical, 55/20+1 = 3.75. /// - [DataField("PotencyDivisor")] public int PotencyDivisor; + [DataField("PotencyDivisor")] public float PotencyDivisor; /// /// Inherent chemical is one that is NOT result of mutation or crossbreeding. These chemicals are removed if species mutation is executed. @@ -79,9 +77,13 @@ public partial struct SeedChemQuantity [DataField("Inherent")] public bool Inherent = true; } -// TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component. +// TODO Make Botany ECS and give it a proper API. I removed the limited access of this class because it's egregious how many systems needed access to it due to a lack of an actual API. +/// +/// SeedData is no longer restricted because the number of friends is absolutely unreasonable. +/// This entire data definition is unreasonable. I felt genuine fear looking at this, this is horrific. Send help. +/// +// TODO: Hit Botany with hammers [Virtual, DataDefinition] -[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffectSystem), typeof(MutationSystem))] public partial class SeedData { #region Tracking diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs index e0be7d08546..bba3c48b867 100644 --- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs +++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs @@ -7,6 +7,8 @@ namespace Content.Server.Botany.Systems; public sealed partial class BotanySystem { + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; + public void ProduceGrown(EntityUid uid, ProduceComponent produce) { if (!TryGetSeed(produce, out var seed)) @@ -15,18 +17,7 @@ public void ProduceGrown(EntityUid uid, ProduceComponent produce) foreach (var mutation in seed.Mutations) { if (mutation.AppliesToProduce) - { - var args = new EntityEffectBaseArgs(uid, EntityManager); - // Imperial inf harvest fix - try - { - mutation.Effect.Effect(args); - } - catch (KeyNotFoundException) - { - Logger.Warning($"Failed to add mutation: {mutation.Name} to {args.TargetEntity} uid"); - } - } + _entityEffects.TryApplyEffect(uid, mutation.Effect); } if (!_solutionContainerSystem.EnsureSolution(uid, @@ -38,10 +29,10 @@ public void ProduceGrown(EntityUid uid, ProduceComponent produce) solutionContainer.RemoveAllSolution(); foreach (var (chem, quantity) in seed.Chemicals) { - var amount = FixedPoint2.New(quantity.Min); + var amount = quantity.Min; if (quantity.PotencyDivisor > 0 && seed.Potency > 0) - amount += FixedPoint2.New(seed.Potency / quantity.PotencyDivisor); - amount = FixedPoint2.New(MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max)); + amount += seed.Potency / quantity.PotencyDivisor; + amount = FixedPoint2.Clamp(amount, quantity.Min, quantity.Max); solutionContainer.MaxVolume += amount; solutionContainer.AddReagent(chem, amount); } diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs index ee35db48e3b..834fd9e8efb 100644 --- a/Content.Server/Botany/Systems/MutationSystem.cs +++ b/Content.Server/Botany/Systems/MutationSystem.cs @@ -13,6 +13,7 @@ public sealed class MutationSystem : EntitySystem [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; private RandomPlantMutationListPrototype _randomMutations = default!; public override void Initialize() @@ -32,10 +33,8 @@ public void CheckRandomMutations(EntityUid plantHolder, ref SeedData seed, float if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f))) { if (mutation.AppliesToPlant) - { - var args = new EntityEffectBaseArgs(plantHolder, EntityManager); - mutation.Effect.Effect(args); - } + _entityEffects.TryApplyEffect(plantHolder, mutation.Effect); + // Stat adjustments do not persist by being an attached effect, they just change the stat. if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name)) seed.Mutations.Add(mutation); diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs index caa796efe2c..2a1fb1a3ea8 100644 --- a/Content.Server/Botany/Systems/PlantHolderSystem.cs +++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs @@ -23,9 +23,10 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; -using Content.Shared.Administration.Logs; +using Content.Shared.Chemistry.Reaction; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.Kitchen.Components; using Content.Shared.Labels.Components; @@ -48,9 +49,11 @@ public sealed class PlantHolderSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; public const float HydroponicsSpeedMultiplier = 1f; public const float HydroponicsConsumptionMultiplier = 2f; + public readonly FixedPoint2 PlantMetabolismRate = FixedPoint2.New(1); private static readonly ProtoId HoeTag = "Hoe"; private static readonly ProtoId PlantSampleTakerTag = "PlantSampleTaker"; @@ -300,6 +303,7 @@ private void OnInteractUsing(Entity entity, ref InteractUs { healthOverride = component.Health; } + component.Seed.Unique = false; var packetSeed = component.Seed; var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride); _randomHelper.RandomOffset(seed, 0.25f); @@ -425,6 +429,7 @@ public void Update(EntityUid uid, PlantHolderComponent? component = null) && component.WeedLevel >= component.Seed.WeedHighLevelThreshold) { Spawn(component.Seed.KudzuPrototype, Transform(uid).Coordinates.SnapToGrid(EntityManager)); + EnsureUniqueSeed(uid, component); component.Seed.TurnIntoKudzu = false; component.Health = 0; } @@ -883,12 +888,18 @@ public void UpdateReagents(EntityUid uid, PlantHolderComponent? component = null if (solution.Volume > 0 && component.MutationLevel < 25) { - var amt = FixedPoint2.New(1); - foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt)) + // Don't apply any effects to a non-unique seed ever! Remove this when botany code is sane... + EnsureUniqueSeed(uid, component); + foreach (var entry in solution.Contents) { + if (entry.Quantity < PlantMetabolismRate) + continue; + var reagentProto = _prototype.Index(entry.Reagent.Prototype); - reagentProto.ReactionPlant(uid, entry, solution, EntityManager, _random, _adminLogger); + _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray(), entry.Quantity); } + + _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, PlantMetabolismRate); } CheckLevelSanity(uid, component); diff --git a/Content.Server/Buckle/Systems/IgniteOnBuckleSystem.cs b/Content.Server/Buckle/Systems/IgniteOnBuckleSystem.cs new file mode 100644 index 00000000000..e7d088df7ff --- /dev/null +++ b/Content.Server/Buckle/Systems/IgniteOnBuckleSystem.cs @@ -0,0 +1,70 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.Buckle.Components; +using Robust.Shared.Timing; + +namespace Content.Server.Buckle.Systems; + +public sealed class IgniteOnBuckleSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly FlammableSystem _flammable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStrapped); + SubscribeLocalEvent(OnUnstrapped); + + SubscribeLocalEvent(ActiveOnInit); + } + + private void OnStrapped(Entity ent, ref StrappedEvent args) + { + // We cache the values here to the other component. + // This is done so we have to do less lookups + var comp = EnsureComp(args.Buckle); + comp.FireStacks = ent.Comp.FireStacks; + comp.MaxFireStacks = ent.Comp.MaxFireStacks; + comp.IgniteTime = ent.Comp.IgniteTime; + } + + private void ActiveOnInit(Entity ent, ref MapInitEvent args) + { + // Handle this via a separate MapInit so the component can be added by itself if need be. + ent.Comp.NextIgniteTime = _timing.CurTime + ent.Comp.NextIgniteTime; + Dirty(ent); + } + + private void OnUnstrapped(Entity ent, ref UnstrappedEvent args) + { + RemCompDeferred(args.Buckle); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var igniteComponent, out var flammableComponent)) + { + if (curTime < igniteComponent.NextIgniteTime) + continue; + + igniteComponent.NextIgniteTime += TimeSpan.FromSeconds(igniteComponent.IgniteTime); + Dirty(uid, igniteComponent); + + if (flammableComponent.FireStacks > igniteComponent.MaxFireStacks) + continue; + + var stacks = flammableComponent.FireStacks + igniteComponent.FireStacks; + if (igniteComponent.MaxFireStacks.HasValue) + stacks = Math.Min(stacks, igniteComponent.MaxFireStacks.Value); + + _flammable.SetFireStacks(uid, stacks, flammableComponent, true); + } + } +} diff --git a/Content.Server/CardboardBox/CardboardBoxSystem.cs b/Content.Server/CardboardBox/CardboardBoxSystem.cs index 836dc485d92..9fdd23e7804 100644 --- a/Content.Server/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Server/CardboardBox/CardboardBoxSystem.cs @@ -1,20 +1,16 @@ -using Content.Server.Storage.Components; using Content.Server.Storage.EntitySystems; using Content.Shared.Access.Components; using Content.Shared.CardboardBox; using Content.Shared.CardboardBox.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Interaction; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Stealth; using Content.Shared.Stealth.Components; using Content.Shared.Storage.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.CardboardBox; @@ -109,10 +105,10 @@ private void AfterStorageClosed(EntityUid uid, CardboardBoxComponent component, //Relay damage to the mover private void OnDamage(EntityUid uid, CardboardBoxComponent component, DamageChangedEvent args) { - if (args.DamageDelta != null && args.DamageIncreased) - { - _damageable.TryChangeDamage(component.Mover, args.DamageDelta, origin: args.Origin); - } + if (args.DamageDelta == null || !args.DamageIncreased || component.Mover is not { } mover) + return; + + _damageable.ChangeDamage(mover, args.DamageDelta, origin: args.Origin); } private void OnEntInserted(EntityUid uid, CardboardBoxComponent component, EntInsertedIntoContainerMessage args) diff --git a/Content.Server/Cargo/Components/MobPriceComponent.cs b/Content.Server/Cargo/Components/MobPriceComponent.cs index ee9f53d0871..ec4bb514874 100644 --- a/Content.Server/Cargo/Components/MobPriceComponent.cs +++ b/Content.Server/Cargo/Components/MobPriceComponent.cs @@ -6,12 +6,6 @@ [RegisterComponent] public sealed partial class MobPriceComponent : Component { - /// - /// How much of a penalty per part there should be. This is a multiplier for a multiplier, the penalty for each body part is calculated from the total number of slots, and then multiplied by this. - /// - [DataField("missingBodyPartPenalty")] - public double MissingBodyPartPenalty = 1.0f; - /// /// The base price this mob should fetch. /// diff --git a/Content.Server/Cargo/Components/RandomPriceComponent.cs b/Content.Server/Cargo/Components/RandomPriceComponent.cs new file mode 100644 index 00000000000..3ece8334e6c --- /dev/null +++ b/Content.Server/Cargo/Components/RandomPriceComponent.cs @@ -0,0 +1,38 @@ +using Content.Server.Cargo.Systems; + +namespace Content.Server.Cargo.Components; + +/// +/// Adds a random value between 0 and X to an entity's sell value. +/// +[RegisterComponent, Access(typeof(PricingSystem))] +public sealed partial class RandomPriceComponent : Component +{ + /// + /// The max random price the entity may be priced at. Non-inclusive. + /// + [DataField(required: true)] + public double MaxRandomPrice; + + /// + /// How the random pricing modifier (0.0 - 1.0) should be distributed. + /// + [DataField] + public RandomPricingCurve PricingCurve = RandomPricingCurve.Cubed; + + /// + /// The generated price for the specific entity. + /// + [DataField] + public double? RandomPrice = null; +} + +/// +/// The random distribution used when generating a random price. +/// +public enum RandomPricingCurve +{ + Linear, + Squared, + Cubed, +} diff --git a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs index 56401602b31..73d69a61b7c 100644 --- a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs +++ b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs @@ -39,6 +39,7 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component public List> Markets = new() { "market", + "weaponry", /// Imperial Weaponry Access Rework }; // TODO: Can probably dump this diff --git a/Content.Server/Cargo/Components/TradeStationComponent.cs b/Content.Server/Cargo/Components/TradeStationComponent.cs deleted file mode 100644 index 0422470cc90..00000000000 --- a/Content.Server/Cargo/Components/TradeStationComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Server.Cargo.Components; - -/// -/// Target for approved orders to spawn at. -/// -[RegisterComponent] -public sealed partial class TradeStationComponent : Component -{ - -} diff --git a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs index c2c2a8365cf..72941a16650 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs @@ -119,9 +119,9 @@ public void SetupBountyLabel(EntityUid uid, EntityUid stationId, CargoBountyData label.Id = bounty.Id; label.AssociatedStationId = stationId; var msg = new FormattedMessage(); - msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id))); + msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-header", ("id", bounty.Id))); msg.PushNewline(); - msg.AddText(Loc.GetString("bounty-manifest-list-start")); + msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-list-start")); msg.PushNewline(); foreach (var entry in prototype.Entries) { diff --git a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs index 4a3fa5330e5..bc6571c0cc5 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs @@ -56,7 +56,7 @@ private void OnWithdrawFunds(Entity ent, ref CargoCo if (args.Account == null) { var stackPrototype = _protoMan.Index(ent.Comp.CashType); - _stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates); + _stack.SpawnAtPosition(args.Amount, stackPrototype, Transform(ent).Coordinates); if (!_emag.CheckFlag(ent, EmagType.Interaction)) { diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 0b5f0155937..dc89dfae169 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -18,6 +18,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Robust.Shared.Random; namespace Content.Server.Cargo.Systems { @@ -76,7 +77,7 @@ private void OnInteractUsingSlip(Entity ent, ref Int return; var orderId = GenerateOrderId(orderDatabase); - var data = new CargoOrderData(orderId, product.Product, product.Name, product.Cost, slip.OrderQuantity, slip.Requester, slip.Reason, slip.Account); + var data = new CargoOrderData(orderId, product, slip.OrderQuantity, slip.Requester, slip.Reason, slip.Account); if (!TryAddOrder(stationUid.Value, ent.Comp.Account, data, orderDatabase)) { @@ -88,7 +89,7 @@ private void OnInteractUsingSlip(Entity ent, ref Int _audio.PlayPvs(ent.Comp.ScanSound, ent); _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(args.User):user} inserted order slip [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]"); + $"{ToPrettyString(args.User):user} inserted order slip [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.Product}, requester:{data.Requester}, reason:{data.Reason}]"); QueueDel(args.Used); args.Handled = true; } @@ -173,7 +174,7 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com } // Invalid order - if (!_protoMan.HasIndex(order.ProductId)) + if (!_protoMan.Resolve(order.Product, out var product)) { ConsolePopup(args.Actor, Loc.GetString("cargo-console-invalid-product")); PlayDenySound(uid, component); @@ -201,7 +202,7 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com PlayDenySound(uid, component); } - var cost = order.Price * order.OrderQuantity; + var cost = product.Cost * order.OrderQuantity; var accountBalance = GetBalanceFromAccount((station.Value, bank), order.Account); // Not enough balance @@ -238,7 +239,7 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com order.SetApproverData(tryGetIdentityShortInfoEvent.Title); var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", - ("productName", Loc.GetString(order.ProductName)), + ("productName", Loc.GetString(product.Name)), ("orderAmount", order.OrderQuantity), ("approver", order.Approver ?? string.Empty), ("cost", cost)); @@ -252,7 +253,7 @@ private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent com // Log order approval _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {order.Account} with balance at {accountBalance}"); + $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.Product}, requester:{order.Requester}, reason:{order.Reason}] on account {order.Account} with balance at {accountBalance}"); orderDatabase.Orders[component.Account].Remove(order); UpdateBankAccount((station.Value, bank), -cost, order.Account); @@ -397,7 +398,7 @@ private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent compone // Log order addition _adminLogger.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(player):user} added order [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]"); + $"{ToPrettyString(player):user} added order [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.Product}, requester:{data.Requester}, reason:{data.Reason}]"); } @@ -466,7 +467,7 @@ private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component) private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id, ProtoId account) { - return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason, account); + return new CargoOrderData(id, cargoProduct, args.Amount, args.Requester, args.Reason, account); } public int GetOutstandingOrderCount(Entity station, ProtoId account) @@ -519,9 +520,7 @@ private void UpdateOrders(EntityUid dbUid) public bool AddAndApproveOrder( EntityUid dbUid, - string spawnId, - string name, - int cost, + CargoProductPrototype product, int qty, string sender, string description, @@ -531,10 +530,9 @@ public bool AddAndApproveOrder( Entity stationData ) { - DebugTools.Assert(_protoMan.HasIndex(spawnId)); // Make an order var id = GenerateOrderId(component); - var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description, account); + var order = new CargoOrderData(id, product, qty, sender, description, account); // Approve it now order.SetApproverData(dest, sender); @@ -543,7 +541,7 @@ Entity stationData // Log order addition _adminLogger.Add(LogType.Action, LogImpact.Low, - $"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}]"); + $"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.Product}, requester:{order.Requester}, reason:{order.Reason}]"); // Add it to the list return TryAddOrder(dbUid, account, order, component) && TryFulfillOrder(stationData, account, order, component).HasValue; @@ -618,11 +616,34 @@ private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, ProtoI /// private bool FulfillOrder(CargoOrderData order, ProtoId account, EntityCoordinates spawn, string? paperProto) { + if (!_protoMan.Resolve(order.Product, out var product)) + return false; + // Create the item itself - var item = Spawn(order.ProductId, spawn); + var item = Spawn(product.Product, spawn); + var itemXForm = Transform(item); // Ensure the item doesn't start anchored - _transformSystem.Unanchor(item, Transform(item)); + _transformSystem.Unanchor(item, itemXForm); + + // Spawn container and insert the item into it if a container is defined. + if (product.Container is { } productContainer) + { + var containerEntity = Spawn(productContainer.Entity, itemXForm.Coordinates); + _transformSystem.SetLocalRotation(containerEntity, itemXForm.LocalRotation); + + if (!_container.TryGetContainer(containerEntity, productContainer.ContainerId, out var container1) || + !_container.Insert(item, container1, force: true)) + { + DebugTools.Assert( + $"Failed to insert cargo product into its specified container. This indicates an error in the cargo product definition's YAML as the product should be insertable into its container. {nameof(CargoProductPrototype)}: {(ProtoId)order.Product.Id}"); + QueueDel(containerEntity); + } + else + { + item = containerEntity; + } + } // Create a sheet of paper to write the order details on var printed = Spawn(paperProto, spawn); @@ -637,7 +658,7 @@ private bool FulfillOrder(CargoOrderData order, ProtoId a Loc.GetString( "cargo-console-paper-print-text", ("orderNumber", order.OrderId), - ("itemName", MetaData(item).EntityName), + ("itemName", product.Name), ("orderQuantity", order.OrderQuantity), ("requester", order.Requester), ("reason", string.IsNullOrWhiteSpace(order.Reason) ? Loc.GetString("cargo-console-paper-reason-default") : order.Reason), diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs index a2224be043c..1727c2d997d 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs @@ -6,6 +6,7 @@ using Content.Shared.Cargo.Events; using Content.Shared.Cargo.Prototypes; using Content.Shared.CCVar; +using Content.Shared.HijackBeacon; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Content.Shared.Stacks; // Imperial Space Pirates: New Horizon @@ -15,7 +16,7 @@ namespace Content.Server.Cargo.Systems; public sealed partial class CargoSystem { /* - * Handles cargo shuttle / trade mechanics. + * Handles automated trade station / trade mechanics. */ private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg"); @@ -290,7 +291,7 @@ private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, { var totalCash = goods.Sum(t => t.Item3); var stackPrototype = _protoMan.Index(component.CashType); - _stack.Spawn((int)totalCash, stackPrototype, xform.Coordinates); + _stack.SpawnAtPosition((int)totalCash, stackPrototype, xform.Coordinates); } _audio.PlayPvs(ApproveSound, uid); diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs index 18ae856fda3..a35f1e7ea3e 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Cargo.Components; using Content.Server.DeviceLinking.Systems; using Content.Server.Popups; -using Content.Server.Shuttles.Systems; using Content.Server.Stack; using Content.Server.Station.Systems; using Content.Shared.Access.Systems; @@ -9,12 +8,9 @@ using Content.Server.Radio.EntitySystems; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; -using Content.Shared.Cargo.Prototypes; -using Content.Shared.CCVar; using Content.Shared.Containers.ItemSlots; using Content.Shared.Mobs.Components; using Content.Shared.Paper; -using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -76,49 +72,4 @@ public override void Update(float frameTime) UpdateTelepad(frameTime); UpdateBounty(); } - - public void UpdateBankAccount( - Entity ent, - int balanceAdded, - ProtoId account, - bool dirty = true) - { - UpdateBankAccount( - ent, - balanceAdded, - new Dictionary, double> { {account, 1} }, - dirty: dirty); - } - - /// - /// Adds or removes funds from the . - /// - /// The station. - /// The amount of funds to add or remove. - /// The distribution between individual . - /// Whether to mark the bank account component as dirty. - [PublicAPI] - public void UpdateBankAccount( - Entity ent, - int balanceAdded, - Dictionary, double> accountDistribution, - bool dirty = true) - { - if (!Resolve(ent, ref ent.Comp)) - return; - - foreach (var (account, percent) in accountDistribution) - { - var accountBalancedAdded = (int) Math.Round(percent * balanceAdded); - ent.Comp.Accounts[account] += accountBalancedAdded; - } - - var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts); - RaiseLocalEvent(ent, ref ev, true); - - if (!dirty) - return; - - Dirty(ent); - } } diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index e33449b15b0..65d0ee50cf6 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -1,23 +1,21 @@ using Content.Server.Administration; -using Content.Server.Body.Systems; using Content.Server.Cargo.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Administration; -using Content.Shared.Body.Components; using Content.Shared.Cargo; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reagent; using Content.Shared.Materials; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Research.Prototypes; using Content.Shared.Stacks; using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; -using System.Linq; -using Content.Shared.Research.Prototypes; //Imperial Space Pirates: New Horizon; Start using Content.Server.Imperial.PiratesNewHorizon.GPS.Components; using Content.Shared.Store.Components; @@ -31,8 +29,8 @@ namespace Content.Server.Cargo.Systems; public sealed partial class PricingSystem : EntitySystem // Imperial Lathes Nerf made it partial { [Dependency] private readonly IConsoleHost _consoleHost = default!; + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; @@ -40,6 +38,8 @@ public sealed partial class PricingSystem : EntitySystem // Imperial Lathes Nerf public override void Initialize() { SubscribeLocalEvent(CalculateMobPrice); + SubscribeLocalEvent(SetRandomPrice); + SubscribeLocalEvent(CalculateRandomPrice); _consoleHost.RegisterCommand("appraisegrid", "Calculates the total value of the given grids.", @@ -150,18 +150,38 @@ private void CalculateMobPrice(EntityUid uid, MobPriceComponent component, ref P return; } - var partPenalty = 0.0; - if (TryComp(uid, out var body)) + args.Price += component.Price * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty); + } + + private void SetRandomPrice(Entity entity, ref MapInitEvent args) + { + if (entity.Comp.RandomPrice == null) { - var partList = _bodySystem.GetBodyChildren(uid, body).ToList(); - var totalPartsPresent = partList.Sum(_ => 1); - var totalParts = partList.Count; + var modifier = _random.NextDouble(); + switch (entity.Comp.PricingCurve) + { + default: + case RandomPricingCurve.Linear: + break; + case RandomPricingCurve.Squared: + modifier = modifier * modifier; + break; + case RandomPricingCurve.Cubed: + modifier = modifier * modifier * modifier; + break; + } - var partRatio = totalPartsPresent / (double) totalParts; - partPenalty = component.Price * (1 - partRatio) * component.MissingBodyPartPenalty; + entity.Comp.RandomPrice = modifier * entity.Comp.MaxRandomPrice; } + } + + private void CalculateRandomPrice(Entity entity, ref PriceCalculationEvent args) + { + // TODO: Estimated pricing. + if (args.Handled) + return; - args.Price += (component.Price - partPenalty) * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty); + args.Price += entity.Comp.RandomPrice ?? 0; } private double GetSolutionPrice(Entity entity) diff --git a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs index b1b23b2732d..e7687ec3df9 100644 --- a/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs +++ b/Content.Server/CartridgeLoader/Cartridges/CrewManifestCartridgeSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.CartridgeLoader; using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CCVar; +using Content.Shared.CrewManifest; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -60,7 +61,13 @@ private void UpdateUiState(EntityUid uid, EntityUid loaderUid, CrewManifestCartr var owningStation = _stationSystem.GetOwningStation(uid); if (owningStation is null) + { + // Display "loading failed" message + var failureMessage = Loc.GetString("crew-manifest-cartridge-loading-failed"); + var failureState = new CrewManifestUiState(failureMessage, new CrewManifestEntries()); + _cartridgeLoader.UpdateCartridgeUiState(loaderUid, failureState); return; + } var (stationName, entries) = _crewManifest.GetCrewManifest(owningStation.Value); diff --git a/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs index 66934c8a871..a544ba66e0f 100644 --- a/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs +++ b/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs @@ -77,11 +77,11 @@ private void SetupPrintedTask(EntityUid uid, NanoTaskItem item) printed.Task = item; var msg = new FormattedMessage(); - msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description))); + msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-description", ("description", FormattedMessage.EscapeText(item.Description)))); msg.PushNewline(); - msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor))); + msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-requester", ("requester", FormattedMessage.EscapeText(item.TaskIsFor)))); msg.PushNewline(); - msg.AddText(item.Priority switch { + msg.AddMarkupOrThrow(item.Priority switch { NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"), NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"), NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"), diff --git a/Content.Server/Chat/Commands/LOOCCommand.cs b/Content.Server/Chat/Commands/LOOCCommand.cs index e303b9766d8..3737b5d5185 100644 --- a/Content.Server/Chat/Commands/LOOCCommand.cs +++ b/Content.Server/Chat/Commands/LOOCCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Shared.Administration; +using Content.Shared.Chat; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index b581b5df0a1..47124954d2b 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -47,6 +47,9 @@ internal sealed partial class ChatManager : IChatManager [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly SponsorsManager _sponsorsManager = default!; // Imperial sponsors [Dependency] private readonly DiscordChatLink _discordLink = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; /// /// The maximum length a player-sent message can be sent @@ -66,6 +69,8 @@ public void Initialize() _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true); + _sawmill = _logManager.GetSawmill("SERVER"); + RegisterRateLimits(); } @@ -113,7 +118,7 @@ public void DispatchServerAnnouncement(string message, Color? colorOverride = nu { var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message))); ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride); - Logger.InfoS("SERVER", message); + _sawmill.Info(message); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}"); } @@ -162,14 +167,20 @@ public void SendAdminAnnouncementMessage(ICommonSession player, string message, public void SendAdminAlert(string message) { - var clients = _adminManager.ActiveAdmins.Select(p => p.Channel); - var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message", ("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message))); - ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients); + SendAdminAlertNoFormatOrEscape(wrappedMessage); + } + + public void SendAdminAlertNoFormatOrEscape(string message) + { + var clients = _adminManager.ActiveAdmins.Select(p => p.Channel); + + ChatMessageToMany(ChatChannel.AdminAlert, message, message, default, false, true, clients); } + public void SendAdminAlert(EntityUid player, string message) { var mindSystem = _entityManager.System(); diff --git a/Content.Server/Chat/SuicideSystem.cs b/Content.Server/Chat/SuicideSystem.cs index dca2959f98e..9f901b2ad8f 100644 --- a/Content.Server/Chat/SuicideSystem.cs +++ b/Content.Server/Chat/SuicideSystem.cs @@ -2,7 +2,7 @@ using Content.Server.Hands.Systems; using Content.Shared.Administration.Logs; using Content.Shared.Chat; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; diff --git a/Content.Server/Chat/Systems/AutoEmoteSystem.cs b/Content.Server/Chat/Systems/AutoEmoteSystem.cs index 8e02f7f53f1..ebb22f6314e 100644 --- a/Content.Server/Chat/Systems/AutoEmoteSystem.cs +++ b/Content.Server/Chat/Systems/AutoEmoteSystem.cs @@ -47,7 +47,11 @@ public override void Update(float frameTime) if (autoEmotePrototype.WithChat) { - _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); + _chatSystem.TryEmoteWithChat(uid, + autoEmotePrototype.EmoteId, + autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, + ignoreActionBlocker: autoEmotePrototype.IgnoreActionBlocker, + forceEmote: autoEmotePrototype.Force); } else { diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs deleted file mode 100644 index c275964a224..00000000000 --- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System.Collections.Frozen; -using Content.Shared.Chat; -using Content.Server.Popups; -using Content.Shared.Chat.Prototypes; -using Content.Shared.Emoting; -using Content.Shared.Speech; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Chat.Systems; - -// emotes using emote prototype -public partial class ChatSystem -{ - [Dependency] private readonly PopupSystem _popupSystem = default!; - - private FrozenDictionary _wordEmoteDict = FrozenDictionary.Empty; - - protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj) - { - base.OnPrototypeReload(obj); - if (obj.WasModified()) - CacheEmotes(); - } - - private void CacheEmotes() - { - var dict = new Dictionary(); - var emotes = _prototypeManager.EnumeratePrototypes(); - foreach (var emote in emotes) - { - foreach (var word in emote.ChatTriggers) - { - var lowerWord = word.ToLower(); - if (dict.TryGetValue(lowerWord, out var value)) - { - var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {value.ID}"; - Log.Error(errMsg); - continue; - } - - dict.Add(lowerWord, emote); - } - } - - _wordEmoteDict = dict.ToFrozenDictionary(); - } - - /// - /// Makes selected entity to emote using and sends message to chat. - /// - /// The entity that is speaking - /// The id of emote prototype. Should has valid - /// Whether or not this message should appear in the adminlog window - /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. - public bool TryEmoteWithChat( - EntityUid source, - string emoteId, - ChatTransmitRange range = ChatTransmitRange.Normal, - bool hideLog = false, - string? nameOverride = null, - bool ignoreActionBlocker = false, - bool forceEmote = false - ) - { - if (!_prototypeManager.TryIndex(emoteId, out var proto)) - return false; - return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote); - } - - /// - /// Makes selected entity to emote using and sends message to chat. - /// - /// The entity that is speaking - /// The emote prototype. Should has valid - /// Whether or not this message should appear in the adminlog window - /// Whether or not this message should appear in the chat window - /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. - public bool TryEmoteWithChat( - EntityUid source, - EmotePrototype emote, - ChatTransmitRange range = ChatTransmitRange.Normal, - bool hideLog = false, - string? nameOverride = null, - bool ignoreActionBlocker = false, - bool forceEmote = false - ) - { - if (!forceEmote && !AllowedToUseEmote(source, emote)) - return false; - - var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker); - - // check if proto has valid message for chat - if (didEmote && emote.ChatMessages.Count != 0) - { - // not all emotes are loc'd, but for the ones that are we pass in entity - var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source)); - SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); - } - - return didEmote; - } - - /// - /// Makes selected entity to emote using without sending any messages to chat. - /// - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. - public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false) - { - if (!_prototypeManager.TryIndex(emoteId, out var proto)) - return false; - - return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker); - } - - /// - /// Makes selected entity to emote using without sending any messages to chat. - /// - /// True if an emote was performed. False if the emote is unvailable, cancelled, etc. - public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false) - { - if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker) - return false; - - return TryInvokeEmoteEvent(uid, proto); - } - - /// - /// Tries to find and play relevant emote sound in emote sounds collection. - /// - /// True if emote sound was played. - public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null) - { - return TryPlayEmoteSound(uid, proto, emote.ID, audioParams); - } - - /// - /// Tries to find and play relevant emote sound in emote sounds collection. - /// - /// True if emote sound was played. - public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null) - { - if (proto == null) - return false; - - // try to get specific sound for this emote - if (!proto.Sounds.TryGetValue(emoteId, out var sound)) - { - // no specific sound - check fallback - sound = proto.FallbackSound; - if (sound == null) - return false; - } - - // optional override params > general params for all sounds in set > individual sound params - var param = audioParams ?? proto.GeneralParams ?? sound.Params; - // Imperial Medieval AgePitch Begin - if (TryComp(uid, out var comp)) - { - float age = comp.Age; - - // Базовые возрастные группы и их характерные pitch значения - if (age < 14f) - { - // Дети: высокий голос, но не писклявый - float childFactor = (14f - age) / 14f; // от 0 до 1 - param.Pitch += 0.3f + childFactor * 0.4f; // +0.3 до +0.7 - } - else if (age < 25f) - { - // Подростки и молодые взрослые: естественный высокий голос - float youthFactor = (25f - age) / 11f; // от 0 до 1 - param.Pitch += 0.1f + youthFactor * 0.2f; // +0.1 до +0.3 - } - else if (age < 50f) - { - // Взрослые: нейтральный голос с легкими вариациями - float adultFactor = (age - 25f) / 25f; // от 0 до 1 - param.Pitch += 0f - adultFactor * 0.1f; // 0 до -0.1 - } - else - { - // Пожилые: более низкий голос, но не глухой - float seniorFactor = (age - 50f) / 50f; // от 0 до 1 (ограничиваем эффект) - float pitchReduction = seniorFactor * 0.25f; // максимум -0.25 - param.Pitch -= Math.Min(pitchReduction, 0.25f); - } - - // Добавляем небольшую случайную вариацию для естественности (±5%) - param.Pitch *= 1f + (_random.NextFloat() - 0.5f) * 0.1f; - - // Ограничиваем общий диапазон разумными пределами - param.Pitch = Math.Clamp(param.Pitch, 0.7f, 1.8f); - } - // Imperial Medieval AgePitch End - _audio.PlayPvs(sound, uid, param); - return true; - } - /// - /// Checks if a valid emote was typed, to play sounds and etc and invokes an event. - /// - /// - /// - /// True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be. - private bool TryEmoteChatInput(EntityUid uid, string textInput) - { - var actionTrimmedLower = TrimPunctuation(textInput.ToLower()); - if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote)) - return true; - - if (!AllowedToUseEmote(uid, emote)) - return true; - - return TryInvokeEmoteEvent(uid, emote); - - static string TrimPunctuation(string textInput) - { - var trimEnd = textInput.Length; - while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1])) - { - trimEnd--; - } - - var trimStart = 0; - while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart])) - { - trimStart++; - } - - return textInput[trimStart..trimEnd]; - } - } - /// - /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity. - /// - /// The entity that is speaking - /// The emote being used - /// - private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote) - { - // If emote is in AllowedEmotes, it will bypass whitelist and blacklist - if (TryComp(source, out var speech) && - speech.AllowedEmotes.Contains(emote.ID)) - { - return true; - } - - // Check the whitelist and blacklist - if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || - _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)) - { - return false; - } - - // Check if the emote is available for all - if (!emote.Available) - { - return false; - } - - return true; - } - - /// - /// Creates and raises and then to let other systems do things like play audio. - /// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player - /// explaining why the emote didn't happen. - /// - /// The entity which is emoting - /// The emote which is being performed - /// True if the emote was performed, false otherwise. - private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto) - { - var beforeEv = new BeforeEmoteEvent(uid, proto); - RaiseLocalEvent(uid, ref beforeEv); - - if (beforeEv.Cancelled) - { - if (beforeEv.Blocker != null) - { - _popupSystem.PopupEntity( - Loc.GetString( - "chat-system-emote-cancelled-blocked", - ("emote", Loc.GetString(proto.Name).ToLower()), - ("blocker", beforeEv.Blocker.Value) - ), - uid, - uid - ); - } - else - { - _popupSystem.PopupEntity( - Loc.GetString("chat-system-emote-cancelled-generic", - ("emote", Loc.GetString(proto.Name).ToLower())), - uid, - uid - ); - } - - return false; - } - - var ev = new EmoteEvent(proto); - RaiseLocalEvent(uid, ref ev); - - return true; - } -} - -/// -/// Raised by chat system when entity made some emote. -/// Use it to play sound, change sprite or something else. -/// -[ByRefEvent] -public sealed class EmoteEvent : HandledEntityEventArgs -{ - public readonly EmotePrototype Emote; - - public EmoteEvent(EmotePrototype emote) - { - Emote = emote; - Handled = false; - } -} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 09802129ea3..24ab8f801d0 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -61,7 +61,6 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; [Dependency] private readonly LanguageSystem _language = default!; // imperial medieval Languages [Dependency] private readonly SponsorsManager _sponsorsManager = default!; @@ -74,7 +73,7 @@ public sealed partial class ChatSystem : SharedChatSystem public override void Initialize() { base.Initialize(); - CacheEmotes(); + Subs.CVar(_configurationManager, CCVars.LoocEnabled, OnLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true); Subs.CVar(_configurationManager, CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true); @@ -126,24 +125,16 @@ private void OnGameChange(GameRunLevelChangedEvent ev) } } - /// - /// Sends an in-character chat message to relevant clients. - /// - /// The entity that is speaking - /// The message being spoken or emoted - /// The chat type - /// Whether or not this message should appear in the chat window - /// Whether or not this message should appear in the adminlog window - /// - /// The player doing the speaking - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - public void TrySendInGameICMessage( + /// + public override void TrySendInGameICMessage( EntityUid source, string message, InGameICChatType desiredType, - bool hideChat, bool hideLog = false, + bool hideChat, + bool hideLog = false, IConsoleShell? shell = null, - ICommonSession? player = null, string? nameOverride = null, + ICommonSession? player = null, + string? nameOverride = null, bool checkRadioPrefix = true, bool ignoreActionBlocker = false, Color? color = null, // Impreial Medieval Magic @@ -152,18 +143,8 @@ public void TrySendInGameICMessage( TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix, ignoreActionBlocker, color, language); // imperial medieval Languages } - /// - /// Sends an in-character chat message to relevant clients. - /// - /// The entity that is speaking - /// The message being spoken or emoted - /// The chat type - /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... - /// - /// The player doing the speaking - /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - /// If set to true, action blocker will not be considered for whether an entity can send this message. - public void TrySendInGameICMessage( + /// + public override void TrySendInGameICMessage( EntityUid source, string message, InGameICChatType desiredType, @@ -241,7 +222,7 @@ public void TrySendInGameICMessage( // This message may have a radio prefix, and should then be whispered to the resolved radio channel if (checkRadioPrefix && (language ?? _language.GetCurrentLanguage(source)).LanguageType is Generic gen) // imperial medieval Tweaked { - if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) // Accent fix + if (TryProcessRadioMessage(source, message, out var modMessage, out var channel)) // Accent fix / Imperial { SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker); return; @@ -263,7 +244,8 @@ public void TrySendInGameICMessage( } } - public void TrySendInGameOOCMessage( + /// + public override void TrySendInGameOOCMessage( EntityUid source, string message, InGameOOCChatType type, @@ -299,6 +281,12 @@ public void TrySendInGameOOCMessage( 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,14 +300,8 @@ public void TrySendInGameOOCMessage( #region Announcements - /// - /// Dispatches an announcement to all. - /// - /// The contents of the message - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Optional color for the announcement message - public void DispatchGlobalAnnouncement( + /// + public override void DispatchGlobalAnnouncement( string message, string? sender = null, bool playSound = true, @@ -339,17 +321,8 @@ public void DispatchGlobalAnnouncement( _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}"); } - /// s - /// Dispatches an announcement to players selected by filter. - /// - /// Filter to select players who will recieve the announcement - /// The contents of the message - /// The entity making the announcement (used to determine the station) - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Sound to play - /// Optional color for the announcement message - public void DispatchFilteredAnnouncement( + /// + public override void DispatchFilteredAnnouncement( Filter filter, string message, EntityUid? source = null, @@ -369,15 +342,8 @@ public void DispatchFilteredAnnouncement( _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}"); } - /// - /// Dispatches an announcement on a specific station - /// - /// The entity making the announcement (used to determine the station) - /// The contents of the message - /// The sender (Communications Console in Communications Console Announcement) - /// Play the announcement sound - /// Optional color for the announcement message - public void DispatchStationAnnouncement( + /// + public override void DispatchStationAnnouncement( EntityUid source, string message, string? sender = null, @@ -601,7 +567,7 @@ private void SendEntityWhisper( } } - private void SendEntityEmote( + protected override void SendEntityEmote( EntityUid source, string action, ChatTransmitRange range, @@ -633,9 +599,9 @@ private void SendEntityEmote( //SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, wrappedMessage, source, range, author, ignoreLanguage: true); // imperial medieval commented if (!hideLog) if (name != Name(source)) - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}"); else - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}"); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}"); } // ReSharper disable once InconsistentNaming @@ -679,7 +645,7 @@ private void SendDeadChat(EntityUid source, ICommonSession player, string messag ("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 {player:Player}: {message}"); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}"); } else { @@ -687,7 +653,7 @@ private void SendDeadChat(EntityUid source, ICommonSession player, string messag ("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")), ("playerName", (playerName)), ("message", FormattedMessage.EscapeText(message))); - _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}"); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}"); } _chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId); @@ -845,7 +811,7 @@ private string SanitizeInGameOOCMessage(string message) public string TransformSpeech(EntityUid sender, string message) { var ev = new TransformSpeechEvent(sender, message); - RaiseLocalEvent(ev); + RaiseLocalEvent(sender, ev, true); return ev.Message; } @@ -979,96 +945,3 @@ public string BuildGibberishString(IReadOnlyList charOptions, int length) public record ExpandICChatRecipientsEvent(EntityUid Source, float VoiceRange, Dictionary Recipients) { } - -/// -/// Raised broadcast in order to transform speech.transmit -/// -public sealed class TransformSpeechEvent : EntityEventArgs -{ - public EntityUid Sender; - public string Message; - - public TransformSpeechEvent(EntityUid sender, string message) - { - Sender = sender; - Message = message; - } -} - -public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs -{ - public EntityUid Sender; - public bool IgnoreBlocker; - - public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker) - { - Sender = sender; - IgnoreBlocker = ignoreBlocker; - } -} - -/// -/// Raised on an entity when it speaks, either through 'say' or 'whisper'. -/// -public sealed class EntitySpokeEvent : EntityEventArgs -{ - public readonly EntityUid Source; - public readonly string Message; - public readonly string? ObfuscatedMessage; // not null if this was a whisper - /// - /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio - /// message gets sent on this channel, this should be set to null to prevent duplicate messages. - /// - public RadioChannelPrototype? Channel; - public readonly LanguagePrototype Language; // imperial medieval languages - public readonly bool Whisper; // imperial medieval languages - public readonly bool CheckNrp; // imperial medieval nrp - - public EntitySpokeEvent(EntityUid source, string message, LanguagePrototype language, RadioChannelPrototype? channel, string? obfuscatedMessage, bool whisper = false, bool checkNrp = true) // imperial medieval languages tweaked - { - Source = source; - Message = message; - Channel = channel; - ObfuscatedMessage = obfuscatedMessage; - Language = language; // imperial medieval languages - Whisper = whisper; // imperial medieval languages - CheckNrp = checkNrp; // imperial medieval nrp - } -} - -/// -/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. -/// -// ReSharper disable once InconsistentNaming -public enum InGameICChatType : byte -{ - Speak, - Emote, - Whisper -} - -/// -/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. -/// -public enum InGameOOCChatType : byte -{ - Looc, - Dead -} - -// imperial medieval languages: -// Перемещён ChatTransmitRange в Content.Shared -// /// -// /// Controls transmission of chat. -// /// -// public enum ChatTransmitRange : byte -// { -// /// Acts normal, ghosts can hear across the map, etc. -// Normal, -// /// Normal but ghosts are still range-limited. -// GhostRangeLimit, -// /// Hidden from the chat window. -// HideChat, -// /// Ghosts can't hear or see it at all. Regular players can if in-range. -// NoGhosts -// } diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs index db887a35f2f..092f809a5e0 100644 --- a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs +++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs @@ -1,3 +1,5 @@ +using Content.Shared.Damage.Systems; + namespace Content.Server.Chat.Systems; using Content.Shared.Chat; diff --git a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs index a70c2196aba..fc4116345fd 100644 --- a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs +++ b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs @@ -35,11 +35,11 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - foreach (var entry in reagent.Metabolisms.Values) + foreach (var entry in reagent.Metabolisms.Metabolisms.Values) { foreach (var effect in entry.Effects) { - shell.WriteLine(effect.GuidebookEffectDescription(_prototype, EntityManager.EntitySysManager) ?? + shell.WriteLine(reagent.GuidebookReagentEffectDescription(_prototype, EntityManager.EntitySysManager, effect, entry.MetabolismRate) ?? Loc.GetString($"cmd-dumpreagentguidetext-skipped", ("effect", effect.GetType()))); } } diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs index 0309d07ed9d..398fddae0c7 100644 --- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs +++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs @@ -26,5 +26,11 @@ public sealed partial class ChemMasterComponent : Component [DataField("clickSound"), ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); + + /// + /// Which source the chem master should draw from when making pills/bottles. + /// + [DataField] + public ChemMasterDrawSource DrawSource = ChemMasterDrawSource.Internal; } } diff --git a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs index 64350cbda17..e4dc4fb8f94 100644 --- a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; +using Content.Shared.DragDrop; using Content.Shared.FixedPoint; using Content.Shared.Labels.EntitySystems; using Content.Shared.Storage; @@ -49,6 +50,9 @@ public override void Initialize() SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); + // Subscribing to DragDropTargetEvent is a quick fix to ensure the UI updates when fluids are dragged and dropped into the ChemMaster, since Shared.Fluids.EntitySystems.SolutionDumpingSystem.cs bypasses UpdateChemicals(). + // TODO: Remove when proper support for infinite volume solutions is added. + SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(OnSetModeMessage); @@ -57,6 +61,7 @@ public override void Initialize() SubscribeLocalEvent(OnReagentButtonMessage); SubscribeLocalEvent(OnCreatePillsMessage); SubscribeLocalEvent(OnOutputToBottleMessage); + SubscribeLocalEvent(OnSetDrawSourceMessage); } private void SubscribeUpdateUiState(Entity ent, ref T ev) @@ -77,7 +82,7 @@ private void UpdateUiState(Entity ent, bool updateLabel = f var state = new ChemMasterBoundUserInterfaceState( chemMaster.Mode, chemMaster.SortingType, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer), - bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel); + bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel, chemMaster.DrawSource); _userInterfaceSystem.SetUiState(owner, ChemMasterUiKey.Key, state); } @@ -135,6 +140,17 @@ private void OnReagentButtonMessage(Entity chemMaster, ref ClickSound(chemMaster); } + private void OnSetDrawSourceMessage(Entity chemMaster, ref ChemMasterOutputDrawSourceMessage message) + { + //Ensure draw source is valid, either from the internal buffer or the inserted beaker + if (!Enum.IsDefined(message.DrawSource)) + return; + + chemMaster.Comp.DrawSource = message.DrawSource; + UpdateUiState(chemMaster); + ClickSound(chemMaster); + } + private void TransferReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer) { var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName); @@ -208,9 +224,9 @@ private void OnCreatePillsMessage(Entity chemMaster, ref Ch return; var needed = message.Dosage * message.Number; - if (!WithdrawFromBuffer(chemMaster, needed, user, out var withdrawal)) - return; + if (!WithdrawFromSource(chemMaster, needed, user, out var withdrawal)) + return; _labelSystem.Label(container, message.Label); for (var i = 0; i < message.Number; i++) @@ -219,7 +235,10 @@ private void OnCreatePillsMessage(Entity chemMaster, ref Ch _storageSystem.Insert(container, item, out _, user: user, storage); _labelSystem.Label(item, message.Label); - _solutionContainerSystem.EnsureSolutionEntity(item, SharedChemMaster.PillSolutionName,out var itemSolution ,message.Dosage); + _solutionContainerSystem.EnsureSolutionEntity(item, + SharedChemMaster.PillSolutionName, + out var itemSolution, + message.Dosage); if (!itemSolution.HasValue) return; @@ -256,7 +275,7 @@ private void OnOutputToBottleMessage(Entity chemMaster, ref if (message.Label.Length > SharedChemMaster.LabelMaxLength) return; - if (!WithdrawFromBuffer(chemMaster, message.Dosage, user, out var withdrawal)) + if (!WithdrawFromSource(chemMaster, message.Dosage, user, out var withdrawal)) return; _labelSystem.Label(container, message.Label); @@ -270,34 +289,77 @@ private void OnOutputToBottleMessage(Entity chemMaster, ref ClickSound(chemMaster); } - private bool WithdrawFromBuffer( + private bool WithdrawFromSource( Entity chemMaster, - FixedPoint2 neededVolume, EntityUid? user, + FixedPoint2 neededVolume, + EntityUid? user, [NotNullWhen(returnValue: true)] out Solution? outputSolution) { outputSolution = null; - if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var solution)) - { - return false; - } + Solution? solution; + Entity? soln = null; - if (solution.Volume == 0) + switch (chemMaster.Comp.DrawSource) { - if (user.HasValue) - _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), user.Value); - return false; - } + case ChemMasterDrawSource.Internal: + if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out solution)) + return false; - // ReSharper disable once InvertIf - if (neededVolume > solution.Volume) - { - if (user.HasValue) - _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), user.Value); - return false; + if (solution.Volume == 0) + { + if (user is { } uid) + _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), uid); + + return false; + } + if (neededVolume > solution.Volume) + { + if (user is { } uid) + _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), uid); + + return false; + } + + break; + + case ChemMasterDrawSource.External: + if (_itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName) is not {} container) + { + if (user.HasValue) + _popupSystem.PopupCursor(Loc.GetString("chem-master-window-no-beaker-text"), user.Value); + return false; + } + + if (!_solutionContainerSystem.TryGetFitsInDispenser(container, out soln, out solution)) + return false; + + if (solution.Volume == 0) + { + if (user is { } uid) + _popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-empty-text"), uid); + + return false; + } + if (neededVolume > solution.Volume) + { + if (user is { } uid) + _popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-low-text"), uid); + + return false; + } + + break; + + default: + return false; } outputSolution = solution.SplitSolution(neededVolume); + + if (soln.HasValue) + _solutionContainerSystem.UpdateChemicals(soln.Value); + return true; } diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs deleted file mode 100644 index 6088d01c592..00000000000 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ /dev/null @@ -1,6 +0,0 @@ - -using Content.Shared.Chemistry.EntitySystems; - -namespace Content.Server.Chemistry.EntitySystems; - -public sealed class InjectorSystem : SharedInjectorSystem; diff --git a/Content.Server/Chemistry/EntitySystems/ReactionMixerSystem.cs b/Content.Server/Chemistry/EntitySystems/ReactionMixerSystem.cs deleted file mode 100644 index 3913afbd07e..00000000000 --- a/Content.Server/Chemistry/EntitySystems/ReactionMixerSystem.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.DoAfter; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Chemistry.EntitySystems; -using Content.Server.Popups; - -namespace Content.Server.Chemistry.EntitySystems; - -public sealed partial class ReactionMixerSystem : EntitySystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnShake); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) - { - if (!args.Target.HasValue || !args.CanReach || !entity.Comp.MixOnInteract) - return; - - if (!MixAttempt(entity, args.Target.Value, out var solution)) - return; - - var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.TimeToMix, new ReactionMixDoAfterEvent(), entity, args.Target.Value, entity); - - _doAfterSystem.TryStartDoAfter(doAfterArgs); - } - - private void OnDoAfter(Entity entity, ref ReactionMixDoAfterEvent args) - { - //Do again to get the solution again - if (!MixAttempt(entity, args.Target!.Value, out var solution)) - return; - - _popup.PopupEntity(Loc.GetString(entity.Comp.MixMessage, ("mixed", Identity.Entity(args.Target!.Value, EntityManager)), ("mixer", Identity.Entity(entity.Owner, EntityManager))), args.User, args.User); - - _solutionContainers.UpdateChemicals(solution!.Value, true, entity.Comp); - - var afterMixingEvent = new AfterMixingEvent(entity, args.Target!.Value); - RaiseLocalEvent(entity, afterMixingEvent); - } - - private void OnShake(Entity entity, ref ShakeEvent args) - { - if (!MixAttempt(entity, entity, out var solution)) - return; - - _solutionContainers.UpdateChemicals(solution!.Value, true, entity.Comp); - - var afterMixingEvent = new AfterMixingEvent(entity, entity); - RaiseLocalEvent(entity, afterMixingEvent); - } - - private bool MixAttempt(EntityUid ent, EntityUid target, out Entity? solution) - { - solution = null; - var mixAttemptEvent = new MixingAttemptEvent(ent); - RaiseLocalEvent(ent, ref mixAttemptEvent); - if (mixAttemptEvent.Cancelled) - { - return false; - } - - if (!_solutionContainers.TryGetMixableSolution(target, out solution, out _)) - return false; - - return true; - } -} diff --git a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs index 3cba02a5994..2d751ba4418 100644 --- a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs @@ -146,10 +146,10 @@ private void OnDispenseReagentMessage(Entity reagentD { // force open container, if applicable, to avoid confusing people on why it doesn't dispense _openable.SetOpen(storedContainer, true); - _solutionTransferSystem.Transfer(reagentDispenser, + _solutionTransferSystem.Transfer(new SolutionTransferData(reagentDispenser, storedContainer, src.Value, outputContainer.Value, dst.Value, - (int)reagentDispenser.Comp.DispenseAmount); + (int)reagentDispenser.Comp.DispenseAmount)); } UpdateUiState(reagentDispenser); diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs index 7b4deea9f40..e6e16422957 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs @@ -148,7 +148,7 @@ private bool TryInjectTargets(Entity injecto // Take our portion of the adjusted solution for this target var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream); // Inject our portion into the target's bloodstream - if (_bloodstream.TryAddToChemicals(targetBloodstream.AsNullable(), individualInjection)) + if (_bloodstream.TryAddToBloodstream(targetBloodstream.AsNullable(), individualInjection)) anySuccess = true; } diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs index 588b0c75a53..706a6fdc778 100644 --- a/Content.Server/Cloning/CloningPodSystem.cs +++ b/Content.Server/Cloning/CloningPodSystem.cs @@ -11,7 +11,8 @@ using Content.Shared.CCVar; using Content.Shared.Chemistry.Components; using Content.Shared.Cloning; -using Content.Shared.Damage; +using Content.Shared.Chat; +using Content.Shared.Damage.Components; using Content.Shared.DeviceLinking.Events; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; @@ -28,6 +29,8 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Damage.Systems; namespace Content.Server.Cloning; @@ -53,10 +56,12 @@ public sealed class CloningPodSystem : EntitySystem [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly CloningSystem _cloning = default!; [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public readonly Dictionary ClonesWaitingForMind = new(); public readonly ProtoId SettingsId = "CloningPod"; public const float EasyModeCloningCost = 0.7f; + private static readonly ProtoId BloodId = "Blood"; public override void Initialize() { @@ -177,7 +182,7 @@ public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity(bodyToClone, out var damageable) && - damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg)) + _damageable.GetAllDamage((bodyToClone, damageable)).DamageDict.TryGetValue("Cellular", out var cellularDmg)) { var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1); chance *= failChanceModifier; @@ -301,7 +306,7 @@ private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod) while (i < 1) { tileMix?.AdjustMoles(Gas.Ammonia, 6f); - bloodSolution.AddReagent("Blood", 50); + bloodSolution.AddReagent(BloodId, 50); if (_robustRandom.Prob(0.2f)) i++; } diff --git a/Content.Server/Cloning/CloningSystem.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index 84ef0503051..995f3e32eb4 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -1,15 +1,19 @@ using Content.Server.Forensics; using Content.Server.Speech.EntitySystems; using Content.Shared.Cloning.Events; +using Content.Shared.Clothing.Components; +using Content.Shared.Clothing.EntitySystems; using Content.Shared.FixedPoint; using Content.Shared.Inventory; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; using Content.Shared.Movement.Components; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Movement.Systems; using Content.Shared.Paper; -using Content.Shared.Stacks; using Content.Shared.Speech.Components; +using Content.Shared.Stacks; using Content.Shared.Storage; using Content.Shared.Store; using Content.Shared.Store.Components; @@ -32,6 +36,8 @@ public sealed partial class CloningSystem [Dependency] private readonly PaperSystem _paper = default!; [Dependency] private readonly VocalSystem _vocal = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly SharedChameleonClothingSystem _chameleonClothing = default!; + [Dependency] private readonly PullingSystem _pulling = default!; public override void Initialize() { @@ -47,20 +53,22 @@ public override void Initialize() SubscribeLocalEvent(OnCloneItemPaper); SubscribeLocalEvent(OnCloneItemForensics); SubscribeLocalEvent(OnCloneItemStore); + SubscribeLocalEvent(OnCloneItemChameleon); // These are for cloning components that cannot be cloned using CopyComp. // Put them into CloningSettingsPrototype.EventComponents to have them be applied to the clone. SubscribeLocalEvent(OnCloneVocal); SubscribeLocalEvent(OnCloneStorage); SubscribeLocalEvent(OnCloneInventory); - SubscribeLocalEvent(OnCloneInventory); + SubscribeLocalEvent(OnCloneMovementSpeedModifier); + SubscribeLocalEvent(OnClonePuller); } private void OnCloneItemStack(Entity ent, ref CloningItemEvent args) { // if the clone is a stack as well, adjust the count of the copy if (TryComp(args.CloneUid, out var cloneStackComp)) - _stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp); + _stack.SetCount((args.CloneUid, cloneStackComp), ent.Comp.Count); } private void OnCloneItemLabel(Entity ent, ref CloningItemEvent args) @@ -96,6 +104,12 @@ private void OnCloneItemStore(Entity ent, ref CloningItemEvent a } } + private void OnCloneItemChameleon(Entity ent, ref CloningItemEvent args) + { + // copy the prototype the original is mimicing + _chameleonClothing.SetSelectedPrototype(args.CloneUid, ent.Comp.Default); + } + private void OnCloneVocal(Entity ent, ref CloningEvent args) { if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) @@ -120,11 +134,19 @@ private void OnCloneInventory(Entity ent, ref CloningEvent a _inventory.CopyComponent(ent.AsNullable(), args.CloneUid); } - private void OnCloneInventory(Entity ent, ref CloningEvent args) + private void OnCloneMovementSpeedModifier(Entity ent, ref CloningEvent args) { if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; _movementSpeedModifier.CopyComponent(ent.AsNullable(), args.CloneUid); } + + private void OnClonePuller(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + _pulling.CopyPullerComponent(ent.AsNullable(), args.CloneUid); + } } diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 6e0f38ad51e..abff99a4072 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -1,9 +1,10 @@ -using Content.Server.Humanoid; using Content.Shared.Administration.Logs; +using Content.Shared.Body; using Content.Shared.Cloning; using Content.Shared.Cloning.Events; using Content.Shared.Database; using Content.Shared.Humanoid; +using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Implants; using Content.Shared.Implants.Components; @@ -26,7 +27,6 @@ namespace Content.Server.Cloning; /// public sealed partial class CloningSystem : SharedCloningSystem { - [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; @@ -35,30 +35,36 @@ public sealed partial class CloningSystem : SharedCloningSystem [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedStorageSystem _storage = default!; [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly NameModifierSystem _nameMod = default!; + [Dependency] private readonly IdentitySystem _identity = default!; - /// - /// Spawns a clone of the given humanoid mob at the specified location or in nullspace. - /// - public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId settingsId, [NotNullWhen(true)] out EntityUid? clone) + public override bool TryCloning( + EntityUid original, + MapCoordinates? coords, + ProtoId settingsId, + [NotNullWhen(true)] out EntityUid? clone) { clone = null; if (!_prototype.Resolve(settingsId, out var settings)) return false; // invalid settings - if (!TryComp(original, out var humanoid)) + if (!TryComp(original, out var humanoid)) return false; // whatever body was to be cloned, was not a humanoid if (!_prototype.Resolve(humanoid.Species, out var speciesPrototype)) return false; // invalid species - var attemptEv = new CloningAttemptEvent(settings); - RaiseLocalEvent(original, ref attemptEv); - if (attemptEv.Cancelled && !settings.ForceCloning) - return false; // cannot clone, for example due to the unrevivable trait + if (!settings.ForceCloning) + { + var attemptEv = new CloningAttemptEvent(settings); + RaiseLocalEvent(original, ref attemptEv); + if (attemptEv.Cancelled) + return false; // cannot clone, for example due to the unrevivable trait + } clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value); - _humanoidSystem.CloneAppearance(original, clone.Value); + _visualBody.CopyAppearanceFrom(original, clone.Value); CloneComponents(original, clone.Value, settings); @@ -75,16 +81,35 @@ public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId settings) + { + if (!_prototype.Resolve(settings, out var proto)) + return; + + CloneComponents(original, clone, proto); + } + + public override void CloneComponents( + EntityUid original, + EntityUid clone, + CloningSettingsPrototype settings) { var componentsToCopy = settings.Components; var componentsToEvent = settings.EventComponents; @@ -130,11 +155,12 @@ public override void CloneComponents(EntityUid original, EntityUid clone, Clonin RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied using CopyComp } - /// - /// Copies the equipment the original has to the clone. - /// This uses the original prototype of the items, so any changes to components that are done after spawning are lost! - /// - public void CopyEquipment(Entity original, Entity clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) + public override void CopyEquipment( + Entity original, + Entity clone, + SlotFlags slotFlags, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) { if (!Resolve(original, ref original.Comp) || !Resolve(clone, ref clone.Comp)) return; @@ -152,15 +178,11 @@ public void CopyEquipment(Entity original, Entity - /// Copies an item and its storage recursively, placing all items at the same position in grid storage. - /// This uses the original prototype of the items, so any changes to components that are done after spawning are lost! - /// - /// - /// This is not perfect and only considers item in storage containers. - /// Some components have their own additional spawn logic on map init, so we cannot just copy all containers. - /// - public EntityUid? CopyItem(EntityUid original, EntityCoordinates coords, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) + public override EntityUid? CopyItem( + EntityUid original, + EntityCoordinates coords, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) { // we use a whitelist and blacklist to be sure to exclude any problematic entities if (!_whitelist.CheckBoth(original, blacklist, whitelist)) @@ -196,12 +218,11 @@ public void CopyEquipment(Entity original, Entity - /// Copies an item's storage recursively to another storage. - /// The storage grids should have the same shape or it will drop on the floor. - /// Basically the same as CopyItem, but we don't copy the outermost container. - /// - public void CopyStorage(Entity original, Entity target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) + public override void CopyStorage( + Entity original, + Entity target, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) { if (!Resolve(original, ref original.Comp, false) || !Resolve(target, ref target.Comp, false)) return; @@ -220,17 +241,12 @@ public void CopyStorage(Entity original, Entity - /// Copies all implants from one mob to another. - /// Might result in duplicates if the target already has them. - /// Can copy the storage inside a storage implant according to a whitelist and blacklist. - /// - /// Entity to copy implants from. - /// Entity to copy implants to. - /// If true will copy storage of the implants (E.g storage implant) - /// Whitelist for the storage copy (If copyStorage is true) - /// Blacklist for the storage copy (If copyStorage is true) - public void CopyImplants(Entity original, EntityUid target, bool copyStorage = false, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) + public override void CopyImplants( + Entity original, + EntityUid target, + bool copyStorage = false, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) { if (!Resolve(original, ref original.Comp, false)) return; // they don't have any implants to copy! @@ -257,6 +273,5 @@ public void CopyImplants(Entity original, EntityUid target, if (copyStorage) CopyStorage(originalImplant, targetImplant.Value, whitelist, blacklist); // only needed for storage implants } - } } diff --git a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs index a645a108905..a18ccd2e711 100644 --- a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs +++ b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Cloning.Components; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -15,7 +16,7 @@ public sealed class RandomCloneSpawnerSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -34,7 +35,7 @@ private void OnMapInit(Entity ent, ref MapInitEvent return; } - var allHumans = _mind.GetAliveHumans(); + var allHumans = _target.GetAliveHumans(); if (allHumans.Count == 0) return; diff --git a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs index f734d3eb3ec..8a654b4ab93 100644 --- a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs @@ -1,14 +1,10 @@ -using System.Linq; -using Content.Server.Emp; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Emp; using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement.Components; -using Content.Shared.Inventory; using Content.Shared.Prototypes; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Server.Clothing.Systems; @@ -16,20 +12,17 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IdentitySystem _identity = default!; - [Dependency] private readonly IRobustRandom _random = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnSelected); - - SubscribeLocalEvent(OnEmpPulse); } private void OnMapInit(EntityUid uid, ChameleonClothingComponent component, MapInitEvent args) { - SetSelectedPrototype(uid, component.Default, true, component); + SetSelectedPrototype(uid, component.Default, true, component: component); } private void OnSelected(EntityUid uid, ChameleonClothingComponent component, ChameleonPrototypeSelectedMessage args) @@ -37,21 +30,6 @@ private void OnSelected(EntityUid uid, ChameleonClothingComponent component, Cha SetSelectedPrototype(uid, args.SelectedId, component: component); } - private void OnEmpPulse(EntityUid uid, ChameleonClothingComponent component, ref EmpPulseEvent args) - { - if (!component.AffectedByEmp) - return; - - if (component.EmpContinuous) - component.NextEmpChange = _timing.CurTime + TimeSpan.FromSeconds(1f / component.EmpChangeIntensity); - - var pick = GetRandomValidPrototype(component.Slot, component.RequireTag); - SetSelectedPrototype(uid, pick, component: component); - - args.Affected = true; - args.Disabled = true; - } - private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null) { if (!Resolve(uid, ref component)) @@ -61,10 +39,7 @@ private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = nul UI.SetUiState(uid, ChameleonUiKey.Key, state); } - /// - /// Change chameleon items name, description and sprite to mimic other entity prototype. - /// - public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false, + public override void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false, bool validate = true, ChameleonClothingComponent? component = null) { if (!Resolve(uid, ref component, false)) @@ -78,8 +53,10 @@ public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdat // make sure that it is valid change if (string.IsNullOrEmpty(protoId) || !_proto.TryIndex(protoId, out EntityPrototype? proto)) return; - if (!IsValidTarget(proto, component.Slot, component.RequireTag)) + + if (validate && !IsValidTarget(proto, component.Slot, component.RequireTag)) return; + component.Default = protoId; UpdateIdentityBlocker(uid, component, proto); @@ -88,14 +65,6 @@ public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdat Dirty(uid, component); } - /// - /// Get a random prototype for a given slot. - /// - public string GetRandomValidPrototype(SlotFlags slot, string? tag = null) - { - return _random.Pick(GetValidTargets(slot, tag).ToList()); - } - public override void Update(float frameTime) { base.Update(frameTime); @@ -106,7 +75,7 @@ public override void Update(float frameTime) if (!chameleon.EmpContinuous) continue; - if (_timing.CurTime < chameleon.NextEmpChange) + if (Timing.CurTime < chameleon.NextEmpChange) continue; // randomly pick cloth element from available and apply it diff --git a/Content.Server/Clothing/Systems/OutfitSystem.cs b/Content.Server/Clothing/Systems/OutfitSystem.cs index c3fea28bcf8..c1187657fd6 100644 --- a/Content.Server/Clothing/Systems/OutfitSystem.cs +++ b/Content.Server/Clothing/Systems/OutfitSystem.cs @@ -1,15 +1,19 @@ using Content.Server.Hands.Systems; using Content.Server.Preferences.Managers; +using Content.Server.Storage.EntitySystems; using Content.Shared.Access.Components; using Content.Shared.Clothing; using Content.Shared.Hands.Components; using Content.Shared.Humanoid; +using Content.Shared.Interaction.Components; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Content.Shared.Station; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Storage; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -22,10 +26,12 @@ public sealed class OutfitSystem : EntitySystem [Dependency] private readonly HandsSystem _handSystem = default!; [Dependency] private readonly InventorySystem _invSystem = default!; [Dependency] private readonly SharedStationSpawningSystem _spawningSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly StorageSystem _storageSystem = default!; - public bool SetOutfit(EntityUid target, string gear, Action? onEquipped = null) + public bool SetOutfit(EntityUid target, string gear, Action? onEquipped = null, bool unremovable = false) { - if (!EntityManager.TryGetComponent(target, out InventoryComponent? inventoryComponent)) + if (!TryComp(target, out InventoryComponent? inventoryComponent)) return false; if (!_prototypeManager.TryIndex(gear, out var startingGear)) @@ -34,7 +40,7 @@ public bool SetOutfit(EntityUid target, string gear, Action(target).Coordinates); + var equipmentEntity = Spawn(gearStr, Comp(target).Coordinates); if (slot.Name == "id" && - EntityManager.TryGetComponent(equipmentEntity, out PdaComponent? pdaComponent) && - EntityManager.TryGetComponent(pdaComponent.ContainedId, out var id)) + TryComp(equipmentEntity, out PdaComponent? pdaComponent) && + TryComp(pdaComponent.ContainedId, out var id)) { - id.FullName = EntityManager.GetComponent(target).EntityName; + id.FullName = Comp(target).EntityName; } _invSystem.TryEquip(target, equipmentEntity, slot.Name, silent: true, force: true, inventory: inventoryComponent); + if (unremovable) + EnsureComp(equipmentEntity); onEquipped?.Invoke(target, equipmentEntity); } } - if (EntityManager.TryGetComponent(target, out HandsComponent? handsComponent)) + var coords = Transform(target).Coordinates; + foreach (var (slotName, storageContainers) in startingGear.Storage) + { + if (storageContainers.Count == 0) + continue; + + if (!_invSystem.TryGetSlotEntity(target, slotName, out var slotEnt)) + continue; + + if (TryComp(slotEnt, out var storage)) + { + foreach (var entProto in storageContainers) + { + var spawnedEntity = SpawnAtPosition(entProto, coords); + _storageSystem.Insert(slotEnt.Value, spawnedEntity, out _, user: null, storageComp: storage, playSound: false); + } + } + else if (TryComp(slotEnt, out var itemSlots)) + { + foreach (var entProto in storageContainers) + { + var spawnedEntity = SpawnAtPosition(entProto, coords); + _itemSlotsSystem.TryInsertEmpty((slotEnt.Value, itemSlots), spawnedEntity, null, excludeUserAudio: true); + } + } + } + + if (TryComp(target, out HandsComponent? handsComponent)) { - var coords = EntityManager.GetComponent(target).Coordinates; foreach (var prototype in startingGear.Inhand) { - var inhandEntity = EntityManager.SpawnEntity(prototype, coords); + var inhandEntity = Spawn(prototype, coords); _handSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent); } } @@ -87,8 +121,8 @@ public bool SetOutfit(EntityUid target, string gear, Action(target, out var comp) - ? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species) + profile ??= TryComp(target, out var comp) + ? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species, comp.Sex) : new HumanoidCharacterProfile(); // Try to get the user's existing loadout for the role profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout); diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs index d592895c8b2..f545d605884 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -1,28 +1,27 @@ -using Content.Server.Popups; -using Content.Shared.Popups; -using Content.Shared.Mobs; using Content.Server.Chat; using Content.Server.Chat.Systems; -using Content.Server.Clothing.Systems; -using Content.Shared.Chat.Prototypes; -using Robust.Shared.Random; -using Content.Shared.Stunnable; -using Content.Shared.Damage.Prototypes; -using Content.Shared.Damage; -using Robust.Shared.Prototypes; using Content.Server.Emoting.Systems; +using Content.Server.Clothing.Systems; +using Content.Server.Popups; using Content.Server.Speech.EntitySystems; -using Content.Shared.Cluwne; -using Robust.Shared.Audio.Systems; -using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; using Content.Shared.Clumsy; using Content.Shared.Chat; +using Content.Shared.Cluwne; +using Content.Shared.Damage.Systems; +using Content.Shared.Mobs; +using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Prototypes; namespace Content.Server.Cluwne; public sealed class CluwneSystem : EntitySystem { - private static readonly ProtoId GeneticDamageGroup = "Genetic"; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -49,15 +48,14 @@ public override void Initialize() /// /// On death removes active comps and gives genetic damage to prevent cloning, reduce this to allow cloning. /// - private void OnMobState(EntityUid uid, CluwneComponent component, MobStateChangedEvent args) + private void OnMobState(Entity ent, ref MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead) { - RemComp(uid); - RemComp(uid); - RemComp(uid); - var damageSpec = new DamageSpecifier(_prototypeManager.Index(GeneticDamageGroup), 300); - _damageableSystem.TryChangeDamage(uid, damageSpec); + RemComp(ent.Owner); + RemComp(ent.Owner); + RemComp(ent.Owner); + _damageableSystem.TryChangeDamage(ent.Owner, ent.Comp.RevertDamage); } } @@ -66,52 +64,65 @@ private void OnMobState(EntityUid uid, CluwneComponent component, MobStateChange /// /// OnStartup gives the cluwne outfit, ensures clumsy, and makes sure emote sounds are laugh. /// - private void OnComponentStartup(EntityUid uid, CluwneComponent component, ComponentStartup args) + private void OnComponentStartup(Entity ent, ref ComponentStartup args) { - if (component.EmoteSoundsId == null) + if (ent.Comp.EmoteSoundsId == null) return; - _prototypeManager.TryIndex(component.EmoteSoundsId, out EmoteSounds); - EnsureComp(uid); - _autoEmote.AddEmote(uid, "CluwneGiggle"); - EnsureComp(uid); + _prototypeManager.TryIndex(ent.Comp.EmoteSoundsId, out EmoteSounds); + + + if (ent.Comp.RandomEmote && ent.Comp.AutoEmoteId != null) + { + EnsureComp(ent.Owner); + _autoEmote.AddEmote(ent.Owner, ent.Comp.AutoEmoteId); + } + + EnsureComp(ent.Owner); + + var transformMessage = Loc.GetString(ent.Comp.TransformMessage, ("target", ent.Owner)); - _popupSystem.PopupEntity(Loc.GetString("cluwne-transform", ("target", uid)), uid, PopupType.LargeCaution); - _audio.PlayPvs(component.SpawnSound, uid); + _popupSystem.PopupEntity(transformMessage, ent.Owner, PopupType.LargeCaution); + _audio.PlayPvs(ent.Comp.SpawnSound, ent.Owner); - _nameMod.RefreshNameModifiers(uid); + _nameMod.RefreshNameModifiers(ent.Owner); - _outfitSystem.SetOutfit(uid, "CluwneGear"); + + _outfitSystem.SetOutfit(ent.Owner, ent.Comp.OutfitId, unremovable: true); } /// /// Handles the timing on autoemote as well as falling over and honking. /// - private void OnEmote(EntityUid uid, CluwneComponent component, ref EmoteEvent args) + private void OnEmote(Entity ent, ref EmoteEvent args) { if (args.Handled) return; - args.Handled = _chat.TryPlayEmoteSound(uid, EmoteSounds, args.Emote); - if (_robustRandom.Prob(component.GiggleRandomChance)) + if (!ent.Comp.RandomEmote) + return; + + args.Handled = _chat.TryPlayEmoteSound(ent.Owner, EmoteSounds, args.Emote); + + if (_robustRandom.Prob(ent.Comp.GiggleRandomChance)) { - _audio.PlayPvs(component.SpawnSound, uid); - _chat.TrySendInGameICMessage(uid, "honks", InGameICChatType.Emote, ChatTransmitRange.Normal); + _audio.PlayPvs(ent.Comp.SpawnSound, ent.Owner); + _chat.TrySendInGameICMessage(ent.Owner, Loc.GetString(ent.Comp.GiggleEmote), InGameICChatType.Emote, ChatTransmitRange.Normal); } - else if (_robustRandom.Prob(component.KnockChance)) + else if (_robustRandom.Prob(ent.Comp.KnockChance)) { - _audio.PlayPvs(component.KnockSound, uid); - _stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(component.ParalyzeTime)); - _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal); + _audio.PlayPvs(ent.Comp.KnockSound, ent.Owner); + _stunSystem.TryUpdateParalyzeDuration(ent.Owner, TimeSpan.FromSeconds(ent.Comp.ParalyzeTime)); + _chat.TrySendInGameICMessage(ent.Owner, Loc.GetString(ent.Comp.KnockEmote), InGameICChatType.Emote, ChatTransmitRange.Normal); } } /// /// Applies "Cluwnified" prefix /// - private void OnRefreshNameModifiers(Entity entity, ref RefreshNameModifiersEvent args) + private void OnRefreshNameModifiers(Entity ent, ref RefreshNameModifiersEvent args) { - args.AddModifier("cluwne-name-prefix"); + args.AddModifier(ent.Comp.NamePrefix); } } diff --git a/Content.Server/Codewords/CodewordFactionPrototype.cs b/Content.Server/Codewords/CodewordFactionPrototype.cs index 72d24b1dcdf..62552c1966e 100644 --- a/Content.Server/Codewords/CodewordFactionPrototype.cs +++ b/Content.Server/Codewords/CodewordFactionPrototype.cs @@ -10,11 +10,11 @@ public sealed partial class CodewordFactionPrototype : IPrototype { /// [IdDataField] - public string ID { get; } = default!; + public string ID { get; private set; } = default!; /// /// The generator to use for this faction. /// [DataField(required:true)] - public ProtoId Generator { get; } = default!; + public ProtoId Generator { get; private set; } = default!; } diff --git a/Content.Server/Codewords/CodewordGeneratorPrototype.cs b/Content.Server/Codewords/CodewordGeneratorPrototype.cs index 15e50ebf73c..b17a40fc6a2 100644 --- a/Content.Server/Codewords/CodewordGeneratorPrototype.cs +++ b/Content.Server/Codewords/CodewordGeneratorPrototype.cs @@ -11,13 +11,13 @@ public sealed partial class CodewordGeneratorPrototype : IPrototype { /// [IdDataField] - public string ID { get; } = default!; + public string ID { get; private set; } = default!; /// /// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from /// [DataField] - public List> Words { get; } = + public List> Words { get; private set; } = [ "Adjectives", "Verbs", diff --git a/Content.Server/CombatMode/CombatModeSystem.cs b/Content.Server/CombatMode/CombatModeSystem.cs index e04463c534d..af36bb59478 100644 --- a/Content.Server/CombatMode/CombatModeSystem.cs +++ b/Content.Server/CombatMode/CombatModeSystem.cs @@ -1,12 +1,5 @@ -using Content.Server.NPC.HTN; using Content.Shared.CombatMode; namespace Content.Server.CombatMode; -public sealed class CombatModeSystem : SharedCombatModeSystem -{ - protected override bool IsNpc(EntityUid uid) - { - return HasComp(uid); - } -} +public sealed class CombatModeSystem : SharedCombatModeSystem; diff --git a/Content.Server/Communications/CommunicationsConsoleComponent.cs b/Content.Server/Communications/CommunicationsConsoleComponent.cs index 2f5f20e3fa6..6a6443514e5 100644 --- a/Content.Server/Communications/CommunicationsConsoleComponent.cs +++ b/Content.Server/Communications/CommunicationsConsoleComponent.cs @@ -74,5 +74,10 @@ public sealed partial class CommunicationsConsoleComponent : SharedCommunication /// [DataField] public bool AnnounceSentBy = true; + // Imperial Space Start Возможность смены уровня угрозы через консоль + [ViewVariables] + [DataField] + public bool CanChangeAlertLevel = true; + // Imperial Space End } } diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index 7d381f79efd..cfd36fd16c3 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -141,7 +141,7 @@ public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComp if (TryComp(stationUid.Value, out AlertLevelComponent? alertComp) && alertComp.AlertLevels != null) { - if (alertComp.IsSelectable) + if (alertComp.IsSelectable && comp.CanChangeAlertLevel) // Imperial Space Добавлен новый компонент, позволяющий регулировать, способна ли консоль менять уровень угрозы { levels = new(); foreach (var (id, detail) in alertComp.AlertLevels.Levels) @@ -212,6 +212,12 @@ private void OnSelectAlertLevelMessage(EntityUid uid, CommunicationsConsoleCompo if (message.Actor is not { Valid: true } mob) return; + if (!comp.CanChangeAlertLevel) // Imperial Space Добавлен новый компонент, позволяющий регулировать, способна ли консоль менять уровень угрозы + { + _popupSystem.PopupCursor(Loc.GetString("comms-console-change-alert-level-denied"), message.Actor, PopupType.Medium); + return; + } + if (!CanUse(mob, uid)) { _popupSystem.PopupCursor(Loc.GetString("comms-console-permission-denied"), message.Actor, PopupType.Medium); @@ -312,7 +318,7 @@ private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent return; } - _roundEndSystem.RequestRoundEnd(uid); + _roundEndSystem.RequestRoundEnd(mob, uid); _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(mob):player} has called the shuttle."); } @@ -321,13 +327,15 @@ private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponen if (!CanCallOrRecall(comp)) return; - if (!CanUse(message.Actor, uid)) + var mob = message.Actor; + + if (!CanUse(mob, uid)) { _popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Actor); return; } - _roundEndSystem.CancelRoundEndCountdown(uid); + _roundEndSystem.CancelRoundEndCountdown(mob, uid); _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(message.Actor):player} has recalled the shuttle."); } } diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 852db3ae224..c3e09f9359b 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -211,7 +211,7 @@ session.Status is SessionStatus.Connected or SessionStatus.InGame * TODO: Jesus H Christ what is this utter mess of a function * TODO: Break this apart into is constituent steps. */ - private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( + private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( NetConnectingArgs e) { // Check if banned. @@ -233,7 +233,7 @@ session.Status is SessionStatus.Connected or SessionStatus.InGame return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null); } - var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); + var bans = await _db.GetBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false); if (bans.Count > 0) { var firstBan = bans[0]; diff --git a/Content.Server/Construction/Completions/BuildMech.cs b/Content.Server/Construction/Completions/BuildMech.cs index e11c79d851e..345df9cf594 100644 --- a/Content.Server/Construction/Completions/BuildMech.cs +++ b/Content.Server/Construction/Completions/BuildMech.cs @@ -1,7 +1,7 @@ using Content.Server.Mech.Systems; -using Content.Server.Power.Components; using Content.Shared.Construction; using Content.Shared.Mech.Components; +using Content.Shared.Power.Components; using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Shared.Containers; @@ -71,4 +71,3 @@ public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager enti entityManager.QueueDeleteEntity(uid); } } - diff --git a/Content.Server/Construction/Completions/GivePrototype.cs b/Content.Server/Construction/Completions/GivePrototype.cs index f05feb70c03..22c5473c8de 100644 --- a/Content.Server/Construction/Completions/GivePrototype.cs +++ b/Content.Server/Construction/Completions/GivePrototype.cs @@ -27,14 +27,14 @@ public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager enti if (EntityPrototypeHelpers.HasComponent(Prototype)) { var stackSystem = entityManager.EntitySysManager.GetEntitySystem(); - var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid); + var stacks = stackSystem.SpawnMultipleNextToOrDrop(Prototype, Amount, userUid ?? uid); if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp)) return; foreach (var item in stacks) { - stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp); + stackSystem.TryMergeToHands(item, (userUid.Value, handsComp)); } } else diff --git a/Content.Server/Construction/Completions/SetStackCount.cs b/Content.Server/Construction/Completions/SetStackCount.cs index f1e3f9fb9ea..409d6abf952 100644 --- a/Content.Server/Construction/Completions/SetStackCount.cs +++ b/Content.Server/Construction/Completions/SetStackCount.cs @@ -12,7 +12,7 @@ public sealed partial class SetStackCount : IGraphAction public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - entityManager.EntitySysManager.GetEntitySystem().SetCount(uid, Amount); + entityManager.EntitySysManager.GetEntitySystem().SetCount((uid, null), Amount); } } } diff --git a/Content.Server/Construction/Completions/SpawnPrototype.cs b/Content.Server/Construction/Completions/SpawnPrototype.cs index c42ecb611f3..5aca29edf47 100644 --- a/Content.Server/Construction/Completions/SpawnPrototype.cs +++ b/Content.Server/Construction/Completions/SpawnPrototype.cs @@ -28,7 +28,7 @@ public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager enti { var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); var stack = entityManager.GetComponent(stackEnt); - entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt, Amount, stack); + entityManager.EntitySysManager.GetEntitySystem().SetCount((stackEnt, stack), Amount); } else { diff --git a/Content.Server/Construction/Conditions/MinHealth.cs b/Content.Server/Construction/Conditions/MinHealth.cs deleted file mode 100644 index 980f6a49ca4..00000000000 --- a/Content.Server/Construction/Conditions/MinHealth.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Content.Server.Destructible; -using Content.Shared.Construction; -using Content.Shared.Damage; -using Content.Shared.Examine; -using Content.Shared.FixedPoint; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Content.Server.Construction.Conditions; - -/// -/// Requires that the structure has at least some amount of health -/// -[DataDefinition] -public sealed partial class MinHealth : IGraphCondition -{ - /// - /// If ByProportion is true, Threshold is a value less than or equal to 1, but more than 0, - /// which is compared to the percent of health remaining in the structure. - /// Else, Threshold is any positive value with at most 2 decimal points of percision, - /// which is compared to the current health of the structure. - /// - [DataField] - public FixedPoint2 Threshold = 1; - [DataField] - public bool ByProportion = false; - - [DataField] - public bool IncludeEquals = true; - - public bool Condition(EntityUid uid, IEntityManager entMan) - { - if (!entMan.TryGetComponent(uid, out DestructibleComponent? destructibleComp) || - !entMan.TryGetComponent(uid, out DamageableComponent? damageComp)) - { - return false; - } - - var destructionSys = entMan.System(); - var maxHealth = destructionSys.DestroyedAt(uid, destructibleComp); - var curHealth = maxHealth - damageComp.TotalDamage; - var proportionHealth = curHealth / maxHealth; - - if (IncludeEquals) - { - if (ByProportion) - { - return proportionHealth >= Threshold; - } - else - { - return curHealth >= Threshold; - } - } - else - { - if (ByProportion) - { - return proportionHealth > Threshold; - } - else - { - return curHealth > Threshold; - } - } - } - - public bool DoExamine(ExaminedEvent args) - { - var entMan = IoCManager.Resolve(); - var entity = args.Examined; - - if (Condition(entity, entMan)) - { - return false; - } - args.PushMarkup(Loc.GetString("construction-examine-condition-low-health")); - - return true; - } - - public IEnumerable GenerateGuideEntry() - { - yield return new ConstructionGuideEntry() - { - Localization = "construction-step-condition-low-health" - }; - } -} diff --git a/Content.Server/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs index 97adf77d802..9d3ee7fd0a4 100644 --- a/Content.Server/Construction/ConstructionSystem.Initial.cs +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -189,7 +189,7 @@ void ShutdownContainers() // TODO allow taking from several stacks. // Also update crafting steps to check if it works. - var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack); + var splitStack = _stackSystem.Split((entity, stack), materialStep.Amount, user.ToCoordinates(0, 0)); if (splitStack == null) continue; diff --git a/Content.Server/Construction/ConstructionSystem.Interactions.cs b/Content.Server/Construction/ConstructionSystem.Interactions.cs index f334915570a..1a7ec1e9b57 100644 --- a/Content.Server/Construction/ConstructionSystem.Interactions.cs +++ b/Content.Server/Construction/ConstructionSystem.Interactions.cs @@ -15,6 +15,7 @@ using Content.Shared.Radio.EntitySystems; using Content.Shared.Stacks; using Content.Shared.Temperature; +using Content.Shared.Temperature.Components; using Content.Shared.Tools.Systems; using Robust.Shared.Containers; using Robust.Shared.Utility; @@ -423,7 +424,7 @@ private HandleResult HandleInteraction(EntityUid uid, object ev, ConstructionGra if ((!temperatureChangeStep.MinTemperature.HasValue || temp >= temperatureChangeStep.MinTemperature.Value) && (!temperatureChangeStep.MaxTemperature.HasValue || temp <= temperatureChangeStep.MaxTemperature.Value)) { - return HandleResult.True; + return validation ? HandleResult.Validated : HandleResult.True; } return HandleResult.False; @@ -435,7 +436,7 @@ private HandleResult HandleInteraction(EntityUid uid, object ev, ConstructionGra break; if (partAssemblyStep.Condition(uid, EntityManager)) - return HandleResult.True; + return validation ? HandleResult.Validated : HandleResult.True; return HandleResult.False; } diff --git a/Content.Server/Construction/ConstructionSystem.Machine.cs b/Content.Server/Construction/ConstructionSystem.Machine.cs index eb922f198c7..ce7f17f9b94 100644 --- a/Content.Server/Construction/ConstructionSystem.Machine.cs +++ b/Content.Server/Construction/ConstructionSystem.Machine.cs @@ -49,7 +49,7 @@ private void CreateBoardAndStockParts(EntityUid uid, MachineComponent component) foreach (var (stackType, amount) in machineBoard.StackRequirements) { - var stack = _stackSystem.Spawn(amount, stackType, xform.Coordinates); + var stack = _stackSystem.SpawnAtPosition(amount, stackType, xform.Coordinates); if (!_container.Insert(stack, partContainer)) throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}"); } diff --git a/Content.Server/Construction/FlatpackSystem.cs b/Content.Server/Construction/FlatpackSystem.cs index 11303e7f2ee..baa123a9284 100644 --- a/Content.Server/Construction/FlatpackSystem.cs +++ b/Content.Server/Construction/FlatpackSystem.cs @@ -1,11 +1,9 @@ using Content.Server.Audio; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Construction; using Content.Shared.Construction.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Power; -using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Construction; @@ -35,16 +33,8 @@ private void OnStartPack(Entity ent, ref FlatpackCreat if (!_itemSlots.TryGetSlot(uid, comp.SlotId, out var itemSlot) || itemSlot.Item is not { } board) return; - Dictionary cost; - if (TryComp(board, out var machine)) - cost = GetFlatpackCreationCost(ent, (board, machine)); - else if (TryComp(board, out var computer) && computer.Prototype != null) - cost = GetFlatpackCreationCost(ent, null); - else - { - Log.Error($"Encountered invalid flatpack board while packing: {ToPrettyString(board)}"); + if (!TryGetFlatpackCreationCost(ent, board, out var cost)) return; - } if (!MaterialStorage.CanChangeMaterialAmount(uid, cost)) return; @@ -80,29 +70,15 @@ private void FinishPacking(Entity ent, bool interrupte if (!_itemSlots.TryGetSlot(uid, comp.SlotId, out var itemSlot) || itemSlot.Item is not { } board) return; - Dictionary cost; - EntProtoId proto; - if (TryComp(board, out var machine)) - { - cost = GetFlatpackCreationCost(ent, (board, machine)); - proto = machine.Prototype; - } - else if (TryComp(board, out var computer) && computer.Prototype != null) - { - cost = GetFlatpackCreationCost(ent, null); - proto = computer.Prototype; - } - else - { - Log.Error($"Encountered invalid flatpack board while packing: {ToPrettyString(board)}"); + if (!TryGetFlatpackCreationCost(ent, board, out var cost) || + !TryGetFlatpackResultPrototype(board, out var proto)) return; - } if (!MaterialStorage.TryChangeMaterialAmount((ent, null), cost)) return; var flatpack = Spawn(comp.BaseFlatpackPrototype, Transform(ent).Coordinates); - SetupFlatpack(flatpack, proto, board); + SetupFlatpack(flatpack, proto.Value, board); Del(board); } diff --git a/Content.Server/Construction/MachineFrameSystem.cs b/Content.Server/Construction/MachineFrameSystem.cs index b8624aeef28..3af1c6ab5f7 100644 --- a/Content.Server/Construction/MachineFrameSystem.cs +++ b/Content.Server/Construction/MachineFrameSystem.cs @@ -182,7 +182,7 @@ private bool TryInsertStack(EntityUid uid, EntityUid used, MachineFrameComponent return true; } - var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack); + var splitStack = _stack.Split((used, stack), needed, Transform(uid).Coordinates); if (splitStack == null) return false; diff --git a/Content.Server/Containers/ContainerCommand.cs b/Content.Server/Containers/ContainerCommand.cs new file mode 100644 index 00000000000..914c9025a05 --- /dev/null +++ b/Content.Server/Containers/ContainerCommand.cs @@ -0,0 +1,87 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Robust.Shared.Containers; +using Robust.Shared.Toolshed; + +namespace Content.Server.Containers; + +[ToolshedCommand, AdminCommand(AdminFlags.Debug)] +public sealed class ContainerCommand : ToolshedCommand +{ + private SharedContainerSystem? _container; + + [CommandImplementation("contents")] + public IEnumerable ContainerQuery([PipedArgument] IEnumerable storageEnts, string id) => + storageEnts.SelectMany(x => ContainerQueryBase(x, id)); + + + public IEnumerable ContainerQueryBase(EntityUid ent, string id) + { + _container ??= GetSys(); + + if (!_container.TryGetContainer(ent, id, out var container)) + return []; + + return container.ContainedEntities; + } + + [CommandImplementation("get")] + public IEnumerable ContainerGet([PipedArgument] IEnumerable storageEnts, string id) => + storageEnts.Select(x => ContainerGetBase(x, id)).Where(s => s != null).Select(s => s!); + + [CommandImplementation("id")] + public IEnumerable ContainerId([PipedArgument] IEnumerable containers) => + containers.Select(x => x.ID); + + + public BaseContainer? ContainerGetBase(EntityUid ent, string id) + { + _container ??= GetSys(); + + if (!_container.TryGetContainer(ent, id, out var container)) + return null; + + return container; + } + + [CommandImplementation("insertmultiple")] + public BaseContainer ContainerInsert([PipedArgument] BaseContainer container, bool doForce, IEnumerable ents) + { + _container ??= GetSys(); + + foreach (var ent in ents) + { + if (doForce) + { + _container.Insert(ent, container, null, true); + } + else + { + _container.InsertOrDrop(ent, container); + } + } + return container; + } + + [CommandImplementation("insert")] + public BaseContainer ContainerInsert([PipedArgument] BaseContainer container, bool doForce, EntityUid ent) + { + return ContainerInsert(container, doForce, [ent]); + } + + [CommandImplementation("list")] + public IEnumerable ContainerList([PipedArgument] EntityUid ent) + { + _container ??= GetSys(); + + return _container.GetAllContainers(ent).Select(container => container.ID); + } + [CommandImplementation("getall")] + public IEnumerable ContainerGetAll([PipedArgument] EntityUid ent) + { + _container ??= GetSys(); + + return _container.GetAllContainers(ent); + } +} diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index aa021518555..01c6570e1d8 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -1,30 +1,24 @@ - - $(TargetFramework) - 12 - false false ..\bin\Content.Server\ true Exe - 1998 + $(NoWarn);1998 RA0032;nullable - enable true + + + - - - - @@ -34,4 +28,9 @@ + + + + + diff --git a/Content.Server/Content.Server.csproj.lscache b/Content.Server/Content.Server.csproj.lscache new file mode 100644 index 00000000000..cfcb8f27da6 --- /dev/null +++ b/Content.Server/Content.Server.csproj.lscache @@ -0,0 +1,3359 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Server +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Server +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=../bin/Content.Server/Content.Server.dll +TargetRefPath=obj/Debug/ref/Content.Server.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1998,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.Server.dll +/refout:obj\Debug\refint\Content.Server.dll +/target:exe +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:RA0032,nullable,SYSLIB0011 + +[sourceFiles] +Access/ + AccessReaderChangeEvent.cs + AccessWireAction.cs + AddAccessLogCommand.cs + Components/ + AgentIDCardComponent.cs + IdBindComponent.cs + PresetIdCardComponent.cs + LogWireAction.cs + Systems/ + AccessOverriderSystem.cs + AccessSystem.cs + AgentIDCardSystem.cs + IdBindSystem.cs + IdCardConsoleSystem.cs + IdCardSystem.cs + JobStatusSystem.cs + PresetIdCardSystem.cs +Actions/ + ActionOnInteractComponent.cs + ActionOnInteractSystem.cs + ActionsSystem.cs + Commands/ + AddActionCommand.cs + RemoveActionCommand.cs +Acz/ContentMagicAczProvider.cs +Administration/ + AdminCommandAttribute.cs + AdminPermsChangedEventArgs.cs + AdminRank.cs + BanList/BanListEui.cs + BanPanelEui.cs + Commands/ + AddEntityStorageCommand.cs + AddPolymorphActionCommand.cs + AddReagent.cs + AdminLogBulk.cs + AdminWhoCommand.cs + AGhostCommand.cs + AnnounceUiCommand.cs + BanCommand.cs + BanExemptionCommands.cs + BanListCommand.cs + BanPanelCommand.cs + CameraCommand.cs + ChangeCvarCommand.cs + ClearBluespaceLockerLinks.cs + ControlMob.cs + DeAdminCommand.cs + DeleteComponentCommand.cs + DepartmentBanCommand.cs + DirtyCommand.cs + DSay.cs + EraseCommand.cs + ExplosionCommand.cs + FaxUiCommand.cs + FollowCommand.cs + ForceGhostCommand.cs + JobWhitelistCommands.cs + LinkBluespaceLocker.cs + LoadGameMapCommand.cs + OpenAdminLogsCommand.cs + OpenAdminNotesCommand.cs + OpenPermissionsCommand.cs + OpenUserVisibleNotesCommand.cs + OSay.cs + OwoifyCommand.cs + PanicBunkerCommand.cs + PardonCommand.cs + PersistenceSaveCommand.cs + PlayerPanelCommand.cs + PlayGlobalSoundCommand.cs + PlayTimeCommands.cs + PromoteHostCommand.cs + ReAdminCommand.cs + ReadyAllCommand.cs + RemoveBodyPartCommand.cs + RemoveEntityStorageCommand.cs + RemoveExtraComponents.cs + RemoveMechanismCommand.cs + RoleBanCommand.cs + RoleBanListCommand.cs + RoleUnbanCommand.cs + SetAdminOOC.cs + SetMindCommand.cs + SetOutfitCommand.cs + SetSolutionCapacity.cs + SetSolutionTemperature.cs + SetSolutionThermalEnergy.cs + ShowGhostsCommand.cs + ShuttleCommands.cs + StealthminCommand.cs + StripAllCommand.cs + ThrowScoreboardCommand.cs + TippyCommand.cs + VariantizeCommand.cs + WarpCommand.cs + Components/SuperBonkComponent.cs + ContentNetworkResourceManager.cs + Logs/ + AdminLogManager.Cache.cs + AdminLogManager.cs + AdminLogManager.Json.cs + AdminLogsEui.cs + AdminLogSystem.cs + Converters/ + AdminLogConverter.cs + AdminLogConverterAttribute.cs + DamageSpecifierConverter.cs + EntityCoordinatesConverter.cs + EntityStringRepresentationConverter.cs + EntityUidConverter.cs + FixedPoint2Converter.cs + GasMixtureStringRepresentationConverter.cs + MindStringRepresentationConverter.cs + PlayerSessionConverter.cs + IAdminLogManager.cs + LogFilter.cs + Managers/ + AdminManager.cs + AdminManager.Metrics.cs + BanManager.cs + BanManager.Notification.cs + CVarControlManager.cs + IAdminManager.cs + IBanManager.cs + IWatchlistWebhookManager.cs + MultiServerKickManager.cs + WatchlistWebhookManager.cs + Notes/ + AdminMessageEui.cs + AdminNotesEui.cs + AdminNotesExtensions.cs + AdminNotesManager.cs + AdminNotesSystem.cs + IAdminNotesManager.cs + UserNotesEui.cs + PlayerLocator.cs + PlayerPanelEui.cs + QuickDialogSystem.cs + QuickDialogSystem.OpenDialog.cs + ServerApi.cs + ServerApi.Utility.cs + Systems/ + AdminSystem.cs + AdminTestArenaSystem.cs + AdminVerbSystem.Antags.cs + AdminVerbSystem.cs + AdminVerbSystem.Smites.cs + AdminVerbSystem.Tools.cs + BufferingSystem.cs + BwoinkSystem.cs + SuperBonkSystem.cs + Toolshed/ + AdminsCommand.cs + MarkedCommand.cs + RejuvenateCommand.cs + SolutionCommand.cs + TagCommand.cs + UI/ + AdminAnnounceEui.cs + AdminCameraEui.cs + EditSolutionsEui.cs + PermissionsEui.cs + SetOutfitEui.cs + SpawnExplosionEui.cs +Advertise/EntitySystems/SpeakOnUIClosedSystem.cs +Afk/ + AfkManager.cs + AFKSystem.cs + Events/ + AFKEvent.cs + UnAFKEvent.cs + IsAfkCommand.cs +Alert/Commands/ + ClearAlert.cs + ShowAlert.cs +Alert/ServerAlertsSystem.cs +AlertLevel/ + AlertLevelComponent.cs + AlertLevelDisplaySystem.cs + AlertLevelPrototype.cs + AlertLevelSystem.cs + Commands/SetAlertLevelCommand.cs +Ame/ + AmeNodeGroup.cs + Components/ + AmeControllerComponent.cs + AmeShieldComponent.cs + EntitySystems/ + AmeControllerSystem.cs + AmeShieldingSystem.cs +Animals/Components/ + EggLayerComponent.cs + ParrotListenerComponent.cs +Animals/Systems/ + EggLayerSystem.cs + ParrotMemorySystem.cs +Announcements/ + AnnounceCommand.cs + RoundAnnouncementPrototype.cs +Anomaly/ + AnomalyScannerSystem.cs + AnomalySystem.Commands.cs + AnomalySystem.cs + AnomalySystem.Generator.cs + AnomalySystem.Vessel.cs + Components/ + AnomalyGeneratorComponent.cs + AnomalyVesselComponent.cs + AntiAnomalyZoneComponent.cs + BluespaceAnomalyComponent.cs + ExplosionAnomalyComponent.cs + GasProducerAnomalyComponent.cs + GeneratingAnomalyGeneratorComponent.cs + InjectionAnomalyComponent.cs + ProjectileAnomalyComponent.cs + PuddleCreateAnomalyComponent.cs + ReagentProducerAnomalyComponent.cs + SecretDataAnomalyComponent.cs + ShuffleParticlesAnomalyComponent.cs + TechAnomalyComponent.cs + TempAffectingAnomalyComponent.cs + Effects/ + AnomalyCoreSystem.cs + BluespaceAnomalySystem.cs + ElectricityAnomalySystem.cs + EntityAnomalySystem.cs + ExplosionAnomalySystem.cs + GasProducerAnomalySystem.cs + GravityAnomalySystem.cs + InjectionAnomalySystem.cs + InnerBodyAnomalySystem.cs + ProjectileAnomalySystem.cs + PuddleCreateAnomalySystem.cs + PyroclasticAnomalySystem.cs + ReagentProducerAnomalySystem.cs + SecretDataAnomalySystem.cs + ShuffleParticlesAnomalySystem.cs + TechAnomalySystem.cs + TempAffectingAnomalySystem.cs + TileAnomalySystem.cs +Antag/ + AntagMultipleRoleSpawnerSystem.cs + AntagObjectivesSystem.cs + AntagRandomObjectivesSystem.cs + AntagRandomSpawnRule.cs + AntagSelectionPlayerPool.cs + AntagSelectionSystem.API.cs + AntagSelectionSystem.cs + AntagSpawnerSystem.cs + Components/ + AntagImmuneComponent.cs + AntagMultipleRoleSpawnerComponent.cs + AntagObjectivesComponent.cs + AntagRandomObjectivesComponent.cs + AntagRandomSpawnComponent.cs + AntagSelectionComponent.cs + AntagSpawnerComponent.cs + GhostRoleAntagSpawnerComponent.cs + Mimic/MobReplacementRuleComponent.cs + MobReplacementRuleSystem.cs +Arcade/ + ArcadeSystem.cs + BlockGame/ + BlockGame.cs + BlockGame.GameState.cs + BlockGame.Pieces.cs + BlockGame.Ui.cs + BlockGameArcadeComponent.cs + BlockGameArcadeSystem.cs + SpaceVillainGame/ + SpaceVillainArcadeComponent.cs + SpaceVillainArcadeSystem.cs + SpaceVillainGame.cs + SpaceVillainGame.Fighter.cs + SpaceVillainGame.Ui.cs + WireActions/ + ArcadeInvincibilityWireActions.cs + ArcadeOverflowWireAction.cs +Armor/ArmorSystem.cs +AssemblyInfo.cs +Atmos/Commands/ + AddAtmosCommand.cs + AddGasCommand.cs + DeleteGasCommand.cs + FillGasCommand.cs + ListGasesCommand.cs + PauseAtmosCommand.cs + RemoveGasCommand.cs + SetAtmosTemperatureCommand.cs + SetMapAtmosCommand.cs + SetTemperatureCommand.cs + ShowAtmosCommand.cs + SubstepAtmosCommand.cs +Atmos/Components/ + AirFilterComponent.cs + AirIntakeComponent.cs + AirtightComponent.cs + AtmosExposedComponent.cs + AtmosFixMarkerComponent.cs + BarotraumaComponent.cs + GasAnalyzerComponent.cs + GasMixtureHolderComponent.cs + HeatExchangerComponent.cs + IgniteOnCollideComponent.cs + IgniteOnMeleeHitComponent.cs + PipeRestrictOverlapComponent.cs + PressureImmunityComponent.cs + PressureProtectionComponent.cs +Atmos/Consoles/ + AtmosAlertsComputerSystem.cs + AtmosMonitoringConsoleSystem.cs +Atmos/EntitySystems/ + AirFilterSystem.cs + AirtightSystem.cs + AtmosDebugOverlaySystem.cs + AtmosphereSystem.API.cs + AtmosphereSystem.BenchmarkHelpers.cs + AtmosphereSystem.Commands.cs + AtmosphereSystem.cs + AtmosphereSystem.CVars.cs + AtmosphereSystem.DeltaPressure.cs + AtmosphereSystem.ExcitedGroup.cs + AtmosphereSystem.Gases.cs + AtmosphereSystem.GridAtmosphere.cs + AtmosphereSystem.HighPressureDelta.cs + AtmosphereSystem.Hotspot.cs + AtmosphereSystem.LINDA.cs + AtmosphereSystem.Map.cs + AtmosphereSystem.Monstermos.cs + AtmosphereSystem.Processing.cs + AtmosphereSystem.Superconductivity.cs + AtmosphereSystem.Utils.cs + AtmosPipeLayersSystem.cs + AutomaticAtmosSystem.cs + BarotraumaSystem.cs + DeltaPressureSystem.cs + FirestarterSystem.cs + FlammableSystem.cs + GasAnalyzerSystem.cs + GasMinerSystem.cs + GasTankSystem.cs + GasTileOverlaySystem.cs + HeatExchangerSystem.cs + PipeRestrictOverlapSystem.cs +Atmos/ + GasMixtureStringRepresentation.cs + IGasReactionEffect.cs + Monitor/Components/ + AirAlarmComponent.cs + AtmosAlarmableComponent.cs + AtmosMonitorComponent.cs + FireAlarmComponent.cs + Monitor/Systems/ + AirAlarmModes.cs + AirAlarmSystem.cs + AtmosAlarmableSystem.cs + AtmosDeviceNetwork.cs + AtmosMonitoringSystem.cs + FireAlarmSystem.cs + Monitor/WireActions/ + AirAlarmPanicWire.cs + AtmosAlarmableAlarmWire.cs + Piping/Binary/Components/ + GasPassiveGateComponent.cs + GasPortComponent.cs + GasRecyclerComponent.cs + SignalControlledValveComponent.cs + Piping/Binary/EntitySystems/ + GasPassiveGateSystem.cs + GasPressurePumpSystem.cs + GasPressureRegulatorSystem.cs + GasRecyclerSystem.cs + GasValveSystem.cs + GasVolumePumpSystem.cs + SignalControlledValveSystem.cs + Piping/Components/ + AtmosPipeColorComponent.cs + AtmosUnsafeUnanchorComponent.cs + GasPipeManifoldComponent.cs + Piping/EntitySystems/ + AtmosDeviceSystem.cs + AtmosPipeAppearanceSystem.cs + AtmosPipeColorSystem.cs + AtmosUnsafeUnanchorSystem.cs + GasPipeManifoldSystem.cs + Piping/Trinary/Components/ + GasFilterComponent.cs + GasMixerComponent.cs + PressureControlledValveComponent.cs + Piping/Trinary/EntitySystems/ + GasFilterSystem.cs + GasMixerSystem.cs + PressureControlledValveSystem.cs + Piping/Unary/Components/ + GasCondenserComponent.cs + GasOutletInjectorComponent.cs + GasPassiveVentComponent.cs + GasPortableComponent.cs + GasVentPumpComponent.cs + GasVentScrubberComponent.cs + Piping/Unary/EntitySystems/ + GasCanisterSystem.cs + GasCondenserSystem.cs + GasOutletInjectorSystem.cs + GasPassiveVentSystem.cs + GasPortableSystem.cs + GasThermoMachineSystem.cs + GasVentPumpSystem.cs + GasVentScrubberSystem.cs + Portable/ + PortableScrubberComponent.cs + PortableScrubberSystem.cs + SpaceHeaterComponent.cs + SpaceHeaterSystem.cs + Reactions/ + AmmoniaOxygenReaction.cs + FrezonCoolantReaction.cs + FrezonProductionReaction.cs + GasReactionPrototype.cs + N2ODecompositionReaction.cs + PlasmaFireReaction.cs + TritiumFireReaction.cs + WaterVaporReaction.cs + Rotting/RottingSystem.cs + TileMixtureEnumerator.cs +Audio/ + AmbientOnPoweredComponent.cs + AmbientSoundSystem.cs + ContentAudioSystem.cs + Jukebox/JukeboxSystem.cs + ServerGlobalSoundSystem.cs +Beam/ + BeamSystem.cs + Components/BeamComponent.cs +Bed/Cryostorage/CryostorageSystem.cs +Bible/ + BibleSystem.cs + Components/ + BibleComponent.cs + BibleUserComponent.cs + FamiliarComponent.cs + SummonableComponent.cs + SummonableRespawningComponent.cs +Body/ + Commands/AddHandCommand.cs + Components/ + RespiratorComponent.cs + ThermalRegulatorComponent.cs + Systems/ + BloodstreamSystem.cs + InternalsSystem.cs + RespiratorSystem.cs + ThermalRegulatorSystem.cs + VisualBodySystem.cs +Botany/Components/ + BotanySwabComponent.cs + LogComponent.cs + PlantHolderComponent.cs + ProduceComponent.cs + SeedComponent.cs + SeedExtractorComponent.cs +Botany/ + SeedPrototype.cs + Systems/ + BotanySwabSystem.cs + BotanySystem.Produce.cs + BotanySystem.Seed.cs + LogSystem.cs + MutationSystem.cs + PlantHolderSystem.cs + SeedExtractorSystem.cs +Buckle/Systems/ + AntiRotOnBuckleSystem.cs + BuckleSystem.cs + IgniteOnBuckleSystem.cs +Camera/CameraRecoilSystem.cs +CardboardBox/CardboardBoxSystem.cs +Cargo/Components/ + CargoBountyLabelComponent.cs + CargoPalletComponent.cs + CargoPalletConsoleComponent.cs + CargoSellBlacklistComponent.cs + MobPriceComponent.cs + RandomPriceComponent.cs + StackPriceComponent.cs + StaticPriceComponent.cs + StationCargoBountyDatabaseComponent.cs + StationCargoOrderDatabaseComponent.cs +Cargo/Systems/ + CargoSystem.Bounty.cs + CargoSystem.cs + CargoSystem.Funds.cs + CargoSystem.Orders.cs + CargoSystem.Shuttle.cs + CargoSystem.Telepad.cs + PriceGunSystem.cs + PricingSystem.cs +CartridgeLoader/ + CartridgeLoaderSystem.cs + Cartridges/ + AstroNavCartridgeComponent.cs + AstroNavCartridgeSystem.cs + CrewManifestCartridgeComponent.cs + CrewManifestCartridgeSystem.cs + LogProbeCartridgeComponent.cs + LogProbeCartridgeSystem.cs + MedTekCartridgeComponent.cs + MedTekCartridgeSystem.cs + NanoTaskCartridgeSystem.cs + NetProbeCartridgeComponent.cs + NetProbeCartridgeSystem.cs + NewsReaderCartridgeComponent.cs + NotekeeperCartridgeComponent.cs + NotekeeperCartridgeSystem.cs + WantedListCartridge.cs +Changeling/Systems/ChangelingIdentitySystem.cs +CharacterInfo/CharacterInfoSystem.cs +Charges/ChargesSystem.cs +Chat/ + AnnounceOnSpawnComponent.cs + AutoEmoteComponent.cs + ChatUser.cs + Commands/ + AdminChatCommand.cs + LOOCCommand.cs + MeCommand.cs + OOCCommand.cs + SayCommand.cs + SetLOOCCommand.cs + SetOOCCommand.cs + SuicideCommand.cs + WhisperCommand.cs + EmoteOnDamageComponent.cs + Managers/ + ChatManager.cs + ChatManager.RateLimit.cs + ChatSanitizationManager.cs + IChatManager.cs + IChatSanitizationManager.cs + SuicideSystem.cs + Systems/ + AnnounceOnSpawnSystem.cs + AutoEmoteSystem.cs + ChatNotificationSystem.cs + ChatSystem.cs + EmoteOnDamageSystem.cs + TypingIndicator/TypingIndicatorSystem.cs + V2/Commands/ + DeleteChatMessageCommand.cs + NukeChatMessagesCommand.cs + V2/ + Messages.cs + Repository/ChatRepository.cs +Chemistry/ + Commands/DumpReagentGuideText.cs + Components/ + ActiveSolutionHeaterComponent.cs + BaseSolutionInjectOnEventComponent.cs + ChemMasterComponent.cs + DeleteOnSolutionEmptyComponent.cs + MeleeChemicalInjectorComponent.cs + RandomFillSolutionComponent.cs + ReagentDispenserComponent.cs + SolutionHeaterComponent.cs + SolutionInjectOnEmbedComponent.cs + SolutionInjectOnProjectileHitComponent.cs + SolutionInjectWhileEmbeddedComponent.cs + TransformableContainerComponent.cs + VaporComponent.cs + Containers/EntitySystems/SolutionContainerSystem.cs + EntitySystems/ + ChemistryGuideDataSystem.cs + ChemMasterSystem.cs + DeleteOnSolutionEmptySystem.cs + ReagentDispenserSystem.cs + SolutionContainerMixerSystem.cs + SolutionHeaterSystem.cs + SolutionInjectOnEventSystem.cs + SolutionInjectWhileEmbeddedSystem.cs + SolutionRandomFillSystem.cs + TransformableContainerSystem.cs + VaporSystem.cs + TileReactions/ + CleanDecalsReaction.cs + CleanTileReaction.cs + CreateEntityTileReaction.cs + ExtinguishTileReaction.cs + FlammableTileReaction.cs + PryTileReaction.cs + SpillIfPuddlePresentTileReaction.cs + SpillTileReaction.cs +Chunking/ChunkingSystem.cs +Clock/ClockSystem.cs +Cloning/ + AcceptCloningEui.cs + CloningConsoleSystem.cs + CloningPodSystem.cs + CloningSystem.cs + CloningSystem.Subscriptions.cs + Components/ + ActiveCloningPodComponent.cs + BeingClonedComponent.cs + CloningConsoleComponent.cs + RandomCloneSpawnerComponent.cs + RandomCloneSpawnerSystem.cs +Clothing/ + ClothingSystem.cs + Systems/ + ChameleonClothingSystem.cs + CursedMaskSystem.cs + OutfitSystem.cs +Cluwne/CluwneSystem.cs +Codewords/ + CodewordComponent.cs + CodewordFactionPrototype.cs + CodewordGeneratorPrototype.cs + CodewordManagerComponent.cs + CodewordSystem.cs +CombatMode/CombatModeSystem.cs +Commands/ + ActionCommands.cs + CommandUtils.cs + ContentCompletionHelper.cs +Communications/ + CommsHackerSystem.cs + CommunicationsConsoleComponent.cs + CommunicationsConsoleSystem.cs +Configurable/ConfigurationSystem.cs +Connection/ + ConnectionManager.cs + ConnectionManager.Whitelist.cs + GrantConnectBypassCommand.cs + IPIntel/ + IPIntel.cs + IPIntelAPI.cs + UserDataExt.cs + Whitelist/ + BlacklistCommands.cs + Conditions/ + ConditionAlwaysMatch.cs + ConditionManualBlacklistMembership.cs + ConditionManualWhitelistMembership.cs + ConditionNotesDateRange.cs + ConditionNotesPlaytimeRange.cs + ConditionPlayerCount.cs + ConditionPlaytime.cs + WhitelistCondition.cs + WhitelistPrototype.cs +Construction/Commands/ + FixRotationsCommand.cs + TileReplaceCommand.cs + TileWallsCommand.cs +Construction/Completions/ + AddContainer.cs + AdminLog.cs + AppearanceChange.cs + AttemptElectrocute.cs + BuildMech.cs + ConditionalAction.cs + DamageEntity.cs + DeleteEntitiesInContainer.cs + DeleteEntity.cs + DestroyEntity.cs + EmptyAllContainers.cs + EmptyContainer.cs + GivePrototype.cs + MachineFrameRegenerateProgress.cs + MoveContainer.cs + PlaySound.cs + PopupEveryone.cs + PopupUser.cs + RaiseEvent.cs + SetAnchor.cs + SetStackCount.cs + SetWiresPanelSecurity.cs + SnapToGrid.cs + SpawnPrototype.cs + SpawnPrototypeAtContainer.cs + VisualizerDataInt.cs +Construction/Components/ + ComputerComponent.cs + ConstructionComponent.cs + MachineComponent.cs + MachineFrameComponent.cs +Construction/Conditions/ + AirlockBolted.cs + AllConditions.cs + AllWiresCut.cs + AmeShieldIntegrity.cs + AnyConditions.cs + ComponentInTile.cs + ContainerEmpty.cs + ContainerNotEmpty.cs + DoorWelded.cs + EntityAnchored.cs + HasTag.cs + Locked.cs + MachineFrameComplete.cs + MinSolution.cs + SolutionEmpty.cs + StorageWelded.cs + WirePanel.cs +Construction/ + ConstructionSystem.Computer.cs + ConstructionSystem.cs + ConstructionSystem.Graph.cs + ConstructionSystem.Guided.cs + ConstructionSystem.Initial.cs + ConstructionSystem.Interactions.cs + ConstructionSystem.Machine.cs + ConstructionSystem.Pathfinding.cs + FlatpackSystem.cs + MachineFrameSystem.cs +Containers/ + ContainerCommand.cs + EmptyOnMachineDeconstructComponent.cs + EmptyOnMachineDeconstructSystem.cs + ThrowInsertContainerComponent.cs + ThrowInsertContainerSystem.cs +Coordinates/ + SpawnRandomOffsetComponent.cs + SpawnRandomOffsetSystem.cs +Corvax/ + ChatFilter/ChatSystem.cs + GameTicking/ + RoundEndedEvent.cs + RoundStartedEvent.cs + RoundNotifications/RoundNotificationsSystem.cs + StationGoal/ + StationGoalCommand.cs + StationGoalPaperSystem.cs +CPUJob/JobQueues/Queues/PathfindingJobQueue.cs +Crayon/CrayonSystem.cs +CrewManifest/ + CrewManifestEui.cs + CrewManifestSystem.cs + CrewManifestViewerComponent.cs +CriminalRecords/Systems/ + CriminalRecordsConsoleSystem.cs + CriminalRecordsHackerSystem.cs + CriminalRecordsSystem.cs +Cuffs/CuffableSystem.cs +Damage/Commands/ + GodModeCommand.cs + HurtCommand.cs +Damage/Components/ + DamageOnHitComponent.cs + DamageOnLandComponent.cs + DamageOnToolInteractComponent.cs + DamageRandomPopupComponent.cs + ExaminableDamageComponent.cs + IgniteOnHeatDamageComponent.cs +Damage/ + ForceSay/DamageForceSaySystem.cs + Systems/ + DamageOnHitSystem.cs + DamageOnLandSystem.cs + DamageOnToolInteractSystem.cs + DamageOtherOnHitSystem.cs + DamageRandomPopupSystem.cs + ExaminableDamageSystem.cs + GodmodeSystem.cs + StaminaSystem.cs +Database/ + BanDef.cs + BanMatcher.cs + DatabaseRecords.cs + DataNodeJsonExtensions.cs + EFCoreExtensions.cs + ServerDbBase.cs + ServerDbEntryManager.cs + ServerDbManager.cs + ServerDbManagerExt.cs + ServerDbPostgres.cs + ServerDbPostgres.Notifications.cs + ServerDbSqlite.cs + UnbanDef.cs + UserDbDataManager.cs +Decals/Commands/ + AddDecalCommand.cs + EditDecalCommand.cs + RemoveDecalCommand.cs +Decals/DecalSystem.cs +Defusable/ + Components/DefusableComponent.cs + Systems/DefusableSystem.cs + WireActions/ + ActivateWireAction.cs + BoltWireAction.cs + BoomWireAction.cs + DelayWireAction.cs + ProceedWireAction.cs +Delivery/ + CargoDeliveryDataComponent.cs + DeliverySystem.cs + DeliverySystem.Spawning.cs +Destructible/ + DestructibleComponent.cs + DestructibleSystem.BenchmarkHelpers.cs + DestructibleSystem.cs + Thresholds/Behaviors/ + BurnBodyBehavior.cs + ChangeConstructionNodeBehavior.cs + DoActsBehavior.cs + DumpCanisterBehavior.cs + DumpRestockInventory.cs + EjectVendorItems.cs + EmptyAllContainersBehaviour.cs + EmptyContainersBehaviour.cs + ExplodeBehavior.cs + GibBehavior.cs + IThresholdBehavior.cs + OpenBehavior.cs + PlaySoundBehavior.cs + PopupBehavior.cs + SolutionExplosionBehavior.cs + SpawnEntitiesBehavior.cs + SpawnGasBehavior.cs + SpillBehavior.cs + TimerStartBehavior.cs + TriggerBehavior.cs + VomitBehavior.cs + WeightedSpawnEntityBehavior.cs + Thresholds/DamageThreshold.cs +DeviceLinking/Components/ + ActiveSignalTimerComponent.cs + AutoLinkReceiverComponent.cs + AutoLinkTransmitterComponent.cs + DoorSignalControlComponent.cs + EdgeDetectorComponent.cs + GunSignalControlComponent.cs + LogicGateComponent.cs + MemoryCellComponent.cs + Overload/ + SoundOnOverloadComponent.cs + SpawnOnOverloadComponent.cs + PowerSensorComponent.cs + SignallerComponent.cs + SignalSwitchComponent.cs + SignalTimerComponent.cs +DeviceLinking/Systems/ + AutoLinkSystem.cs + DeviceLinkOverloadSystem.cs + DeviceLinkSystem.cs + DoorSignalControlSystem.cs + EdgeDetectorSystem.cs + GunSignalControlSystem.cs + LogicGateSystem.cs + MemoryCellSystem.cs + PowerSensorSystem.cs + RandomGateSystem.cs + SignallerSystem.cs + SignalSwitchSystem.cs + SignalTimerSystem.cs +DeviceNetwork/Components/ + ApcNetworkComponent.cs + DeviceNetworkRequiresPowerComponent.cs + Devices/ApcNetSwitchComponent.cs + SingletonDeviceNetServerComponent.cs + StationLimitedNetworkComponent.cs + WiredNetworkComponent.cs + WirelessNetworkComponent.cs +DeviceNetwork/Systems/ + ApcNetworkSystem.cs + DeviceListSystem.cs + DeviceNetworkJammerSystem.cs + DeviceNetworkRequiresPowerSystem.cs + DeviceNetworkSystem.cs + Devices/ApcNetSwitchSystem.cs + NetworkConfiguratorSystem.cs + SingletonDeviceNetServerSystem.cs + StationLimitedNetworkSystem.cs + WiredNetworkSystem.cs + WirelessNetworkSystem.cs +Dice/DiceSystem.cs +Discord/DiscordLink/ + DiscordChatLink.cs + DiscordLink.cs + DiscordSawmillLogger.cs +Discord/ + DiscordWebhook.cs + WebhookData.cs + WebhookEmbed.cs + WebhookEmbedField.cs + WebhookEmbedFooter.cs + WebhookIdentifier.cs + WebhookMentions.cs + WebhookMessages/VoteWebhooks.cs + WebhookPayload.cs + WebhookUser.cs +Disposal/ + Mailing/MailingUnitSystem.cs + Tube/ + DisposalBendComponent.cs + DisposalJunctionComponent.cs + DisposalRouterComponent.cs + DisposalSignallerComponent.cs + DisposalSignallerSystem.cs + DisposalSignalRouterComponent.cs + DisposalSignalRouterSystem.cs + DisposalTaggerComponent.cs + DisposalTransitComponent.cs + DisposalTubeComponent.cs + DisposalTubeSystem.cs + GetDisposalsConnectableDirectionsEvent.cs + GetDisposalsNextDirectionEvent.cs + TubeConnectionsCommand.cs + Unit/ + BeingDisposedComponent.cs + BeingDisposedSystem.cs + DisposableSystem.cs + DisposalHolderComponent.cs + DisposalUnitSystem.cs + DoInsertDisposalUnitEvent.cs +DoAfter/DoAfterSystem.cs +Doors/ + Electronics/Systems/DoorElectronicsSystem.cs + Systems/ + AirlockSystem.cs + DoorSystem.cs + FirelockSystem.cs + TurnstileSystem.cs + WireActions/ + AirlockWireIdentifier.cs + DoorBoltLightWireAction.cs + DoorBoltWireAction.cs + DoorSafetyWireAction.cs + DoorTimingWireAction.cs +Dragon/Components/ + DragonComponent.cs + DragonRiftComponent.cs +Dragon/ + DragonRiftSystem.cs + DragonSystem.cs +Drowsiness/DrowsinessSystem.cs +Drunk/DrunkSystem.cs +Effects/ColorFlashEffectSystem.cs +Electrocution/Components/ + ActivatedElectrifiedComponent.cs + ElectrocutionComponent.cs +Electrocution/ + ElectrocuteCommand.cs + ElectrocutionNode.cs + ElectrocutionSystem.cs + RandomInsulationComponent.cs +Emoting/ + Components/BodyEmotesComponent.cs + Systems/BodyEmotesSystem.cs +Emp/EmpSystem.cs +Engineering/ + Components/SpawnAfterInteractComponent.cs + EntitySystems/SpawnAfterInteractSystem.cs +Ensnaring/EnsnareableSystem.cs +EntityConditions/Conditions/BreathingEntityConditionSystem.cs +EntityEffects/Effects/Atmos/ + CreateGasEntityEffectSystem.cs + FlammableEntityEffectSystem.cs + IgniteEntityEffectSystem.cs +EntityEffects/Effects/ + Body/OxygenateEntityEffectsSystem.cs + Botany/PlantAttributes/ + PlantAdjustHealthEntityEffectSystem.cs + PlantAdjustMutationLevelEntityEffectSystem.cs + PlantAdjustMutationModEntityEffectSystem.cs + PlantAdjustNutritionEntityEffectSystem.cs + PlantAdjustPestsEntityEffectSystem.cs + PlantAdjustPotencyEntityEffectSystem.cs + PlantAdjustToxinsEntityEffectSystem.cs + PlantAdjustWaterEntityEffectSystem.cs + PlantAdjustWeedsEntityEffectSystem.cs + PlantAffectGrowthEntityEffectSystem.cs + PlantChangeStatEntityEffectSystem.cs + PlantCryoxadoneEntityEffectSystem.cs + PlantDestroySeedsEntityEffectSystem.cs + PlantDiethylamineEntityEffectSystem.cs + PlantPhalanximineEntityEffectSystem.cs + PlantRemoveKudzuEntityEffectSystem.cs + PlantRestoreSeedsEntityEffectSystem.cs + RobustHarvestEntityEffectSystem.cs + Botany/ + PlantMutateChemicalsEntityEffectSystem.cs + PlantMutateGasesEntityEffectSystem.cs + PlantMutateHarvestEntityEffectSystem.cs + PlantMutateSpeciesChangeEntityEffectSystem.cs + ExplodeEntityEffectSystem.cs + MakeSentientEntityEffectSystem.cs + PolymorphEntityEffectSystem.cs + Solution/AreaReactionEntityEffectSystem.cs + Transform/ExplosionEntityEffectSystem.cs +EntityList/SpawnEntityListCommand.cs +Entry/ + EntryPoint.cs + IgnoredComponents.cs +EUI/ + BaseEui.cs + EuiManager.cs +Examine/ExamineSystem.cs +ExCable/ExCableNodeGroup.cs +Explosion/Components/ + ExplosionAirtightGridComponent.cs + ProjectileGrenadeComponent.cs +Explosion/EntitySystems/ + ExplosionGridTileFlood.cs + ExplosionSpaceTileFlood.cs + ExplosionSystem.Airtight.cs + ExplosionSystem.cs + ExplosionSystem.CVars.cs + ExplosionSystem.GridMap.cs + ExplosionSystem.Processing.cs + ExplosionSystem.TileFill.cs + ExplosionSystem.Visuals.cs + ExplosionTileFlood.cs + ProjectileGrenadeSystem.cs + ScatteringGrenadeSystem.cs +Fax/ + AdminUI/AdminFaxEui.cs + FaxConstants.cs + FaxSystem.cs +FeedbackSystem/ + FeedbackCommand.cs + FeedbackSystem.cs + OpenFeedbackPopupCommand.cs + ServerFeedbackManager.cs +Flash/FlashSystem.cs +Fluids/Components/ + EvaporationSparkleComponent.cs + FootstepTrackComponent.cs +Fluids/EntitySystems/ + AbsorbentSystem.cs + PuddleDebugDebugOverlaySystem.cs + PuddleSystem.cs + PuddleSystem.Spillable.cs + SmokeSystem.cs + SpraySystem.cs +Fluids/ShowFluidsCommand.cs +Forensics/Components/ + CleansForensicsComponent.cs + DnaSubstanceTraceComponent.cs + FiberComponent.cs + ForensicPadComponent.cs + ForensicScannerComponent.cs + ForensicsComponent.cs + IgnoresFingerprintsComponent.cs + ResidueComponent.cs +Forensics/Systems/ + FingerprintMaskSystem.cs + ForensicPadSystem.cs + ForensicScannerSystem.cs + ForensicsSystem.cs +GameTicking/Commands/ + DelayStartCommand.cs + DynamicRuleCommand.cs + EndRoundCommand.cs + ForceMapCommand.cs + ForcePresetCommand.cs + GoLobbyCommand.cs + JoinGameCommand.cs + ObserveCommand.cs + RespawnCommand.cs + RestartRoundCommand.cs + SetGamePresetCommand.cs + StartRoundCommand.cs + ToggleDisallowLateJoinCommand.cs + ToggleReadyCommand.cs +GameTicking/Events/ + GetDisallowedJobsEvent.cs + IsRoleAllowedEvent.cs + NoJobsAvailableSpawningEvent.cs + RoundStartingEvent.cs +GameTicking/ + GameTicker.cs + GameTicker.CVars.cs + GameTicker.GamePreset.cs + GameTicker.GameRule.cs + GameTicker.Lobby.cs + GameTicker.LobbyBackground.cs + GameTicker.Player.cs + GameTicker.Replays.cs + GameTicker.RoundFlow.cs + GameTicker.Spawning.cs + GameTicker.StatusShell.cs + ManifestEntry.cs + Presets/GamePresetPrototype.cs + Rules/ + AntagLoadProfileRuleSystem.cs + Components/ + AntagLoadProfileRuleCOmponent.cs + ChangelingRuleComponent.cs + DeathMatchRuleComponent.cs + DragonRuleComponent.cs + InactivityRuleComponent.cs + KillCalloutRuleComponent.cs + LoadMapRuleComponent.cs + MaxTimeRestartRuleComponent.cs + NukeOperativeSpawnerComponent.cs + NukeopsRuleComponent.cs + NukeOpsShuttleComponent.cs + ParadoxCloneRuleComponent.cs + RespawnDeadRuleComponent.cs + RespawnTrackerComponent.cs + RevolutionaryRuleComponent.cs + RoundstartStationVariationRuleComponent.cs + RuleGridsComponent.cs + SandboxRuleComponent.cs + SecretRuleComponent.cs + StationVariationPassRuleComponent.cs + SubGamemodesComponent.cs + SurvivorRuleComponent.cs + ThiefRuleComponent.cs + TraitorRuleComponent.cs + XenoborgsRuleComponent.cs + ZombieRuleComponent.cs + DeathMatchRuleSystem.cs + DragonRuleSystem.cs + DynamicRuleSystem.cs + GameRuleSystem.cs + GameRuleSystem.Utility.cs + InactivityTimeRestartRuleSystem.cs + KillCalloutRuleSystem.cs + LoadMapRuleSystem.cs + MaxTimeRestartRuleSystem.cs + NukeopsRuleSystem.cs + ParadoxCloneRuleSystem.cs + RespawnRuleSystem.cs + RevolutionaryRuleSystem.cs + RoundstartStationVariationRuleSystem.cs + RuleGridsSystem.cs + SandboxRuleSystem.cs + SecretRuleSystem.cs + SubGamemodesSystem.cs + SurvivorRuleSystem.cs + ThiefRuleSystem.cs + TraitorRuleSystem.cs + VariationPass/ + BaseEntityReplaceVariationPassSystem.cs + Components/ + CutWireVariationPassComponent.cs + EntityReplaceVariationPassComponent.cs + EntitySpawnVariationPassComponent.cs + PoweredLightVariationPassComponent.cs + PuddleMessVariationPassComponent.cs + ReinforcedWallReplaceVariationPassComponent.cs + ReplacementMarkers/ + ReinforcedWallReplacementMarkerComponent.cs + SolarPanelReplacementMarkerComponent.cs + WallReplacementMarkerComponent.cs + SolarPanelReplaceVariationPassComponent.cs + WallReplaceVariationPassComponent.cs + CutWireVariationPassSystem.cs + EntitySpawnVariationPassSystem.cs + PoweredLightVariationPassSystem.cs + PuddleMessVariationPassSystem.cs + ReinforcedWallReplaceVariationPassSystem.cs + SolarPanelReplaceVariationPassSystem.cs + VariationPassSystem.cs + WallReplaceVariationPassSystem.cs + XenoborgsRuleSystem.cs + ZombieRuleSystem.cs +Gateway/Components/ + GatewayComponent.cs + GatewayGeneratorComponent.cs + GatewayGeneratorDestinationComponent.cs +Gateway/Systems/ + GatewayGeneratorSystem.cs + GatewaySystem.cs +Gatherable/Components/ + GatherableComponent.cs + GatheringProjectileComponent.cs + SoundOnGatherComponent.cs +Gatherable/ + GatherableSystem.cs + GatherableSystem.Projectile.cs +Ghost/ + Components/SpookySpeakerComponent.cs + GhostBooEvent.cs + GhostCommand.cs + GhostSystem.cs + ReturnToBodyEui.cs + Roles/Components/ + GhostRoleComponent.cs + GhostRoleRaffleComponent.cs + GhostTakeoverAvailableComponent.cs + TakeGhostRoleEvent.cs + ToggleableGhostRoleComponent.cs + Roles/ + Events/GhostRoleSpawnerUsedEvent.cs + GhostRoleSystem.cs + MakeGhostRoleCommand.cs + MakeRaffledGhostRoleCommand.cs + Raffles/ + GhostRoleRaffleConfig.cs + GhostRoleRaffleDeciderPrototype.cs + IGhostRoleRaffleDecider.cs + RngGhostRoleRaffleDecider.cs + ToggleableGhostRoleSystem.cs + UI/ + GhostRolesEui.cs + MakeGhostRoleEui.cs + SpookySpeakerSystem.cs +GhostKick/GhostKickManager.cs +Gibbing/Systems/GibOnRoundEndSystem.cs +GlobalUsings.cs +Gravity/ + FloatingVisualizerSystem.cs + GravityGeneratorSystem.cs + GravitySystem.cs +GridPreloader/ + GridPreloaderComponent.cs + GridPreloaderSystem.cs +Guardian/ + CanHostGuardianComponent.cs + GuardianComponent.cs + GuardianCreatorComponent.cs + GuardianHostComponent.cs + GuardianSystem.cs +Guidebook/GuidebookDataSystem.cs +GuideGenerator/ + ChemistryJsonGenerator.cs + ReactionJsonGenerator.cs + ReagentEntry.cs + UniversalJsonConverter.cs +Hands/Systems/HandsSystem.cs +Holiday/Christmas/ + LimitedItemGiverComponent.cs + LimitedItemGiverSystem.cs + RandomGiftComponent.cs + RandomGiftSystem.cs + SantaComponent.cs +Holiday/Greet/ + Custom.cs + DefaultHolidayGreet.cs +Holiday/ + HolidayPrototype.cs + HolidaySystem.cs + HolidayVisualsComponent.cs + Interfaces/ + IHolidayCelebrate.cs + IHolidayGreet.cs + IHolidayShouldCelebrate.cs + MonthEnum.cs + ShouldCelebrate/ + ChineseNewYear.cs + Computus.cs + DayOfYear.cs + DefaultHolidayShouldCelebrate.cs + FridayThirteenth.cs + WeekdayInMonth.cs +Holopad/HolopadSystem.cs +HotPotato/HotPotatoSystem.cs +Humanoid/Components/ + RandomHumanoidAppearanceComponent.cs + RandomHumanoidSpawnerComponent.cs +Humanoid/ + HideableHumanoidLayersSystem.cs + Systems/ + RandomHumanoidAppearanceSystem.cs + RandomHumanoidSystem.cs +IconSmoothing/RandomIconSmoothSystem.cs +IgnitionSource/IgnitionSourceSystem.cs +ImmovableRod/ + ImmovableRodComponent.cs + ImmovableRodSystem.cs +Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/ + Components/HydrogenGasFilterComponent.cs + Systems/HydrogenGasFilterSystem.cs +Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/ + Components/HydrogenGasMixerComponent.cs + Systems/HydrogenGasMixerSystem.cs +Imperial/Atmos/Hydrogen/Pipes/ + HydrogenPressurePump/Systems/HydrogenGasPressurePumpSystem.cs + HydrogenVolumePump/Systems/HydrogenGasVolumePumpSystem.cs +Imperial/Atmos/PPGenerator/Components/ + PPCirculatorComponent.cs + PPGeneratorComponent.cs +Imperial/Atmos/PPGenerator/ + NodeGroups/NodeGroup.cs + Systems/PPGeneratorSystem.cs +Imperial/Atmos/Reactions/ + DeuteriumProductionReaction.cs + HydrogenFireReaction.cs + HydrogenProductionReaction.cs + OzoniumProductionReaction.cs + PhazoniumProductionReaction.cs + ThermoniumProductionReaction.cs +Imperial/Cargo/ + Components/PriceModiferComponent.cs + Systems/PricingSystem.Modifier.cs +Imperial/ChemistryRework/Systems/ + ChangeMarkingColorEntityEffectSystem.cs + GravityEntityEffectSystem.cs + ImperialFlashEntityEffectSystem.cs + Luminescence.cs + RemoveMarkEntityEffectSystem.cs + ResurrectionEntityEffectSystem.cs +Imperial/ChemistryRework/TileReactions/ + GravityTileReaction.cs + ReplaceTileReaction.cs + TeleportTileReaction.cs +Imperial/Crook/ + Components/MetalDetectorComponent.cs + Systems/MetalDetectorSystem.cs +Imperial/ + Damage/DamageModifierStorageSystem.cs + DeimonFly/Storage/PunishOnStorageTakeSystem.cs + Effects/BloodTrail/ + BloodTrailComponent.cs + BloodTrailSystem.cs + ElectroMouse/ElectroMouseSystem.cs + EmergencyButton/EmergencyButtonSystem.cs + EnergyCore/Components/ + CoreCollisionComponent.cs + CoreShielding.cs + CoreTechnicalRuleComponent.cs + DeauthorizationCodeComponent.cs + EnergyCore.GeneratorComponent.cs + EnergyCore.PendingDetonationComponent.cs + EnergyCore.ScreenStatusComponent.cs + EnergyCoreComponent.cs + StationFragmentCodes.cs + EnergyCore/ + EnergyCoreCommands.cs + Events/ + CommandEvents.cs + EnergyCoreEvents.cs + InitEvents.cs + Systems/ + CoreCollisionSystem.cs + CoreTechnicalRuleSystem.cs + EnergyCore.AccessComputerSystem.cs + EnergyCore.GeneratorSystem.cs + EnergyCore.PendingDetonationSystem.cs + EnergyCore.ScreenStatusSystem.cs + EnergyCoreSystem.cs + NukeopsRuleSystemImperial.cs + Entry.cs + ErtCall/ + ErtCallCommand.cs + ErtCallPresetPrototype.cs + ErtCallSystem.cs + ShuttleNameComponent.cs + ShuttleNameSystem.cs + EverythingStamp/EverythingStampSystem.cs + ExplosiveClothing/ + ClothingFactionExplosionComponent.cs + ClothingFactionExplosionSystem.cs + ExplosiveProjectile/ + ExplosiveProjectileComponent.cs + ExplosiveProjectileComponent.ResultOff.cs + ExplosiveProjectileComponent.ResultOn.cs + ExplosiveProjectileSystem.cs + ExplosiveProjectileSystem.ResultOff.cs + ExplosiveProjectileSystem.ResultOn.cs + FactionChange/FactionChangeCommand.cs + FutureDiary/ + FutureDiaryComponent.cs + FutureDiarySystem.cs + GasMaskFullProtection/ + GasMaskFullProtectionComponent.cs + GasMaskFullProtectionSystem.cs + GasMaskFullProtectionUserComponent.cs + GlobalPacifismEndRound/GlobalPacifismEndRoundSystem.cs + HandledCuffer/ + HandledCuffer.cs + HandledCufferComponent.cs + Helpers/ + EnergyCoreHelpers/CoreSearchSystem.cs + MouseInputSystem/MouseInputSystem.cs + ImperialBorgs/ + BorgHypoSystem.cs + MimicrySystem.cs + ImperialLightning/ + Commands/SetNonCancelableLightningTargetCommand.cs + ImperialLightningReciverComponent.cs + ImperialLightningSenderComponent.cs + ImperialLightningSystem.cs + ImperialScatteringGrenade/ImperialScatteringGrenadeSystem.cs + ImperialStore/Components/ + ImperialStoreRefundComponent.cs + ImperialStoreSystem.Command.cs + ImperialStore/Conditions/ + ImperialBuyBeforeCondition.cs + ImperialBuyerAntagCondition.cs + ImperialBuyerDepartmentCondition.cs + ImperialBuyerJobCondition.cs + ImperialBuyerSpeciesCondition.cs + ImperialBuyerWhitelistCondition.cs + ImperialListingLimitedStockCondition.cs + ImperialStoreWhitelistCondition.cs + ImperialStore/ + ImpereialStoreSystem.Ui.cs + ImperialCurrencyComponent.cs + ImperialStoreSystem.cs + ImperialStoreSystem.Listings.cs + ImperialStoreSystem.Refund.cs + ImperialVehicle/ImperialVehicleSystem.cs + Implants/ + Components/NutrientPumpComponent.cs + Systems/NutrientPumpSystem.cs + JanitorsResponseTeam/ + CallJRTViaCentcomFaxComponent.cs + CallJRTViaCentcomFaxSystem.cs + KAKTYC/TitorClock/ + TitorClockComponent.cs + TitorClocksSystem.cs + Lavaland/MaterialEnergy/ + MaterialEnergyComponent.cs + MaterialEnergySystem.cs + Medieval/Administration/Commands/ + NrpPardonCommand.cs + NrpWarnCommand.cs + OpenNrpPanelCommand.cs + Medieval/Administration/Nrp/ + NrpCurseSystem.cs + NrpMessagesSystem.cs + NrpPanelEui.cs + Medieval/ + EntityEffects/Effects/AddMagicEssenceEntityEffectSystem.cs + Magic/ + MedievalMagicSystem.cs + MedievalMagicSystem.EntityAiming.cs + MedievalMagicSystem.Instant.cs + MedievalMagicSystem.Target.cs + Other/BindStoreOnEquip/ + BindStoreOnEquipComponent.cs + BindStoreOnEquipSystem.cs + Other/MagicTraining/CastTraining/ + CastTrainingComponent.cs + CastTrainingSystem.cs + Other/MagicTraining/DamageTraining/ + DamageTrainingComponent.cs + DamageTrainingSystem.cs + MedievalAddEssenceOnProjectileHitComponent.cs + Other/MagicTraining/KillingTraining/ + KillingTrainingComponent.cs + KillingTrainingSystem.cs + Other/ + Minigames/MedievalArrowsMinigame/MedievalArrowsMinigameSystem.cs + OverlayHelpers/ShowSpawnedEntitySystem.cs + PlayAudioAfterSpellCast/ + PlayAudioAfterSpellCastComponent.cs + PlayAudioAfterSpellCastSystem.cs + SpawnEntityOnNullSpaceTeleport/ + SpawnEntityOnNullSpaceTeleportComponent.cs + SpawnEntityOnNullSpaceTeleportSystem.cs + Spells/Brarrier/ + BrarrierComponent.cs + BrarrierSystem.cs + CheckInitialBarrierComponent.cs + Spells/MedievalAddComponentOnAttack/ + MedievalAddComponentOnAttackComponent.cs + MedievalAddComponentOnAttackSystem.cs + Spells/MedievalAddComponentOnInteract/ + MedievalAddComponentOnInteractComponent.cs + MedievalAddComponentOnInteractSystem.cs + Spells/MedievalBlackHoleExplosion/ + MedievalBlackHoleExplosionComponent.cs + MedievalBlackHoleExplosionSystem.cs + Spells/MedievalHomingProjectile/ + MedievalHomingProjectileComponent.cs + MedievalHomingProjectileSystem.cs + Spells/MedievalProjectilePlayerFollower/ + MedievalProjectilePlayerFollowerComponent.cs + MedievalProjectilePlayerFollowerSystem.cs + Spells/MedievalShootAfterSpawn/ + MedievalShootAfterSpawnComponent.cs + MedievalShootAfterSpawnSystem.cs + Spells/MedievalSpellOnInteract/ + MedievalSpellOnInteractComponent.cs + MedievalSpellOnInteractSystem.cs + Spells/MedievalSpellRandomRelativeSpawn/ + MedievalSpellRandomRelativeSpawnComponent.cs + MedievalSpellRandomRelativeSpawnSystem.cs + Spells/MedievalSpellSleep/ + MedievalSleepTarget.cs + MedievalSpellSleepComponent.cs + MedievalSpellSleepSystem.cs + Spells/MedievalSpellTeleportEffect/MedievalSpellTeleportEffectSystem.cs + TurnedToStone/TurnedToStoneSystem.cs + Triggers/SpawnEntityOnTrigger/ + SpawnEntityOnTriggerComponent.cs + SpawnEntityOnTriggerSystem.cs + Triggers/TriggerOnSimilarFixtureCollide/ + TriggerOnSimilarFixtureCollideComponent.cs + TriggerOnSimilarFixtureCollideSystem.cs + MeleeResource/ + MedievalMeleeResourceComponent.cs + MedievalMeleeResourceSystem.cs + Training/TrainingTargetComponent.cs + Minigames/MinigamesSystem.cs + MiningWeapons/EmpoweredAttacks/ + AttacksSystem.cs + AttacksSystem.PiercingLunge.cs + MiningWeapons/Smasher/ + SmasherSystem.Activation.cs + SmasherSystem.cs + SmasherSystem.Deactivation.cs + Ninja/ + NinjaHeadset/NinjaHeadsetSystem.cs + Objectives/LeaveNoTrace/LeaveNoTraceSystem.cs + NinjaMultitask/Components/ + DealDamageConditionComponent.cs + NinjaDamageTargetComponent.cs + PickRandomSyndicateTargetComponent.cs + NinjaMultitask/Systems/ + DealDamageConditionSystem.cs + PickRandomSyndicateTargetSystem.cs + PhaseSpace/PhaseSpaceSystem.cs + PiratesNewHorizon/GPS/Components/ + GPSTrackerPriceComponent.cs + GPSTrackerRemoverComponent.cs + PiratesNewHorizon/GPS/Systems/ + GPSTrackerPriceSystem.cs + GPSTrackerRemoverSystem.cs + PiratesNewHorizon/ + Roles/PirateRoleComponent.cs + Rules/Components/ + PiratesRuleComponent.cs + PiratesShuttleComponent.cs + Rules/Systems/PiratesRuleSystem.cs + Trigger/ + Components/TriggerOnHeatComponent.cs + Systems/TriggerSystemOnHeat.cs + PlantsAnalyzer/ + Components/PlantsAnalyzerComponent.cs + PlantsAnalyzerSystem.cs + Power/Components/ + SupermatterEventComponent.cs + SupermatterMonitorConsoleComponent.cs + SupermatterTouchComponent.cs + Power/EntitySystems/Events/ + SupermatterLightningEvent.cs + SupermatterNoneEvent.cs + SupermatterPlasmaEvent.cs + SupermatterRadiationEvent.cs + SupermatterTouchedEvent.cs + Power/EntitySystems/ + SupermatterEventSystem.cs + SupermatterGasSystem.cs + SupermatterIntegritySystem.cs + SupermatterMonitorConsoleSystem.cs + SupermatterTouchSystem.cs + PrinterDoc/PrinterDocSystem.cs + Revolution/Components/ + ConsentRevolutionaryComponent.cs + ConsentRevolutionaryCooldownComponent.cs + ConsentRevolutionaryDenyComponent.cs + Revolution/ + ConsentRevolutionSystem.cs + UI/ConsentRequestedEui.cs + SCP/ChaseMusic/ + Components/ImperialSCPChaseMusicComponent.cs + Systems/ImperialSCPChaseMusicSystem.cs + SCP/SCP106/Components/ + SCP106BedComponent.cs + SCP106Component.cs + SCP106DimensionDebuffComponent.cs + SCP106PocketDimensionMarkerComponent.cs + SCP106PuddleComponent.cs + SCP106PuddleImmunityComponent.cs + SCP106PuddleStunComponent.cs + SCP106SkullComponent.cs + SCP/SCP106/Systems/ + SCP106System.Bed.cs + SCP106System.cs + SCP106System.Puddle.cs + SCP106System.SCP106.cs + SCP106System.Skull.cs + Seriozha/ + Administration/Commands/PlayMapSoundCommand.cs + Fredik21/callmtf/MTFCallCommand.cs + ShockWave/ + ShockWaveComponent.cs + ShockWaveSystem.cs + Sponsors/SponsorsManager.cs + StationGoal/StationGoalPrototype.cs + SyndieBorg/ + SyndieBorgComponent.cs + SyndieBorgSystem.cs + TargetOverlay/TargetOverlaySystem.cs + Traits/ImperialLegsParalyzedSystem.cs + TranslateTools/Commands.cs + Vampire/ + VampireDeadEui.cs + VampirePurposesSystem.cs + VampireRequestedEui.cs + VampireSystem.Abilities.cs + VampireSystem.cs + VampireSystem.Ghoul.cs + XenoGenetcs/ + Genes/RespiratorGeneSystem.cs + ServerXenoGeneticsSystem.cs + XxRaay/079/ + NDA079AirlockAbilitySystem.cs + NDA079CableAbilitySystem.cs + NDA079CpuSystem.cs + NDA079GeneratorSystem.cs + NDA079LightFlickerAbilitySystem.cs + NDA079System.cs + XxRaay/Components/ + MessageOnMindComponent.cs + OrbitalStrikePodComponent.cs + XxRaay/ + FactionShop/FactionShopSystem.cs + FlagSystem/FlagCaptureSystem.cs + SyndieBattle/ + RespawnTouchComponent.cs + RespawnTouchSystem.cs + SyndieBattleRedemptionComponent.cs + SyndieBattleRedemptionSystem.cs + SyndieBattleRuleComponent.cs + SyndieBattleRuleSystem.cs + SyndieBattleScoreComponent.cs + Systems/ + AlertEnergySystem.cs + CameraRelayChatSystem.cs + GroqApiSchemas.cs + LLMNPCSystem.cs + MessageOnMindSystem.cs + OrbitalStrikeSystem.cs + RechargeShoulderRocketLauncherSystem.cs + SandevistanSystem.cs + ShoulderRocketLauncherSystem.cs + SmasherInnateToolsSystem.cs + SpawnItemsOnDespawnSystem.cs + SupplyCaseSystem.cs + Zero/Overthrow/OverthrowSystem.cs + _SystemsSupplementation/Zombies/ + Components/ZombieBarotraumaDamageComponent.cs + ZombifySystem.cs + _UpstreamFixes/Commands/ + ImperialSavegridCommand.cs + ImperialSavemapCommand.cs +Implants/ + AutoImplantSystem.cs + ChameleonControllerSystem.cs + Components/AutoImplantComponent.cs + ImplantedSystem.cs + ImplanterSystem.cs + RadioImplantSystem.cs + SubdermalImplantSystem.cs +Info/ + RulesManager.cs + ShowRulesCommand.cs +Instruments/ + InstrumentComponent.cs + InstrumentSystem.cs + InstrumentSystem.CVars.cs + SwappableInstrumentComponent.cs + SwappableInstrumentSystem.cs +Interaction/ + DragDropSystem.cs + InteractionSystem.cs + TilePryCommand.cs +Inventory/ + InventoryCommand.cs + ServerInventorySystem.cs + VirtualItemSystem.cs +IoC/ + ServerContentIoC.cs + ServerModuleTestingCallbacks.cs +IP/IPAddressExt.cs +Item/ItemSystem.cs +ItemRecall/ItemRecallSystem.cs +Jittering/JitteringSystem.cs +Jobs/ + AddComponentSpecial.cs + AddImplantSpecial.cs + ApplyStatusEffectSpecial.cs + GiveItemOnHolidaySpecial.cs + RemoveComponentSpecial.cs +KillTracking/ + KillTrackerComponent.cs + KillTrackingSystem.cs +Kitchen/Components/ + ActivelyMicrowavedComponent.cs + ActiveMicrowaveComponent.cs + MicrowaveComponent.cs +Kitchen/EntitySystems/ + MicrowaveSystem.cs + ReagentGrinderSystem.cs + SharpSystem.cs +Labels/Label/HandLabelerSystem.cs +LandMines/LandMineSystem.cs +Lathe/Components/ + LatheAnnouncingComponent.cs + LatheHeatProducingComponent.cs + LatheProducingComponent.cs +Lathe/ + LatheProducingSystem.cs + LatheSystem.cs +Light/Components/ + ActiveEmergencyLightComponent.cs + EmergencyLightComponent.cs + ExpendableLightComponent.cs + LightBehaviourComponent.cs + LitOnPoweredComponent.cs + SetRoofComponent.cs +Light/EntitySystems/ + EmergencyLightSystem.cs + ExpendableLightSystem.cs + HandheldLightSystem.cs + LightBulbSystem.cs + LightCycleSystem.cs + LightReplacerSystem.cs + LitOnPoweredSystem.cs + PoweredLightSystem.cs + RgbLightControllerSystem.cs + RoofSystem.cs + RotatingLightSystem.cs + SunShadowSystem.cs +Lightning/Components/ + LightningComponent.cs + LightningTargetComponent.cs +Lightning/ + LightningSystem.cs + LightningTargetSystem.cs +Lube/ + LubedSystem.cs + LubeSystem.cs +Machines/EntitySystems/MultipartMachineSystem.cs +Magic/MagicSystem.cs +Mapping/ + MappingCommand.cs + MappingManager.cs + MappingSystem.cs +Maps/ + Conditions/HolidayMapCondition.cs + GameMapManager.cs + GameMapPoolPrototype.cs + GridDraggingCommand.cs + GridDraggingSystem.cs + IGameMapManager.cs + MapMigrationSystem.cs + NameGenerators/ + NanotrasenNameGenerator.cs + StationNameGenerator.cs + PlanetCommand.cs + ResaveCommand.cs + TileGridSplitSystem.cs +MapText/ + MapTextComponent.cs + MapTextSystem.cs +MassMedia/ + Components/NewsWriterComponent.cs + Systems/NewsSystem.cs +Materials/ + Components/ProduceMaterialExtractorComponent.cs + MaterialReclaimerSystem.cs + MaterialStorageSystem.cs + OreSiloSystem.cs + ProduceMaterialExtractorSystem.cs +Mech/ + Components/MechAirComponent.cs + Equipment/ + Components/MechGrabberComponent.cs + EntitySystems/MechGrabberSystem.cs + Systems/ + MechEquipmentSystem.cs + MechSystem.cs +Medical/BiomassReclaimer/ + ActiveBiomassReclaimerComponent.cs + BiomassReclaimerComponent.cs + BiomassReclaimerSystem.cs +Medical/Components/ + CryoPodAirComponent.cs + HealthAnalyzerComponent.cs + MedicalScannerComponent.cs +Medical/CrewMonitoring/ + CrewMonitoringConsoleComponent.cs + CrewMonitoringConsoleSystem.cs + CrewMonitoringServerComponent.cs + CrewMonitoringServerSystem.cs +Medical/ + CryoPodEjectLockWireAction.cs + CryoPodSystem.cs + DefibrillatorSystem.cs + HealthAnalyzerSystem.cs + InsideCryoPodSystem.cs + MedicalScannerSystem.cs + SuitSensors/SuitSensorSystem.cs +Mind/Commands/ + MakeSentientCommand.cs + MindInfoCommand.cs + RenameCommand.cs +Mind/ + Filters/TargetObjectiveMindFilter.cs + MindSystem.cs + Toolshed/MindCommand.cs + TransferMindOnGibSystem.cs +Mindshield/MindShieldSystem.cs +Mining/ + MeteorComponent.cs + MeteorSystem.cs + MiningSystem.cs +Mobs/ + CritMobActionsSystem.cs + DeathgaspComponent.cs + DeathgaspSystem.cs +Morgue/ + CrematoriumSystem.cs + MorgueSystem.cs +Motd/ + GetMOTDCommand.cs + MOTDCommand.cs + MOTDSystem.cs + SetMOTDCommand.cs +MouseRotator/MouseRotatorSystem.cs +Movement/Components/ + EyeCursorOffsetComponent.cs + LagCompensationComponent.cs + PullMovingComponent.cs +Movement/ + LockEyesCommand.cs + RotateEyesCommand.cs + Systems/ + BoundarySystem.cs + ContentEyeSystem.cs + FloorOcclusionSystem.cs + JetpackSystem.cs + LagCompensationSystem.cs + MobCollisionSystem.cs + PullController.cs + SpriteMovementSystem.cs +NameIdentifier/NameIdentifierSystem.cs +Ninja/ + Events/BatteryChangedEvent.cs + Systems/ + BatteryDrainerSystem.cs + ItemCreatorSystem.cs + NinjaGlovesSystem.cs + NinjaSuitSystem.cs + SpaceNinjaSystem.cs + SpiderChargeSystem.cs + StunProviderSystem.cs +NodeContainer/EntitySystems/ + NodeContainerSystem.cs + NodeGroupSystem.cs +NodeContainer/NodeGroups/ + BaseNodeGroup.cs + NodeGroupAttribute.cs + NodeGroupFactory.cs + PipeNet.cs +NodeContainer/Nodes/ + AdjacentNode.cs + IRotatableNode.cs + NodeHelpers.cs + PipeNode.cs + PortablePipeNode.cs + PortPipeNode.cs +NPC/Commands/ + AddNPCCommand.cs + NPCCommand.cs + NPCDomainCommand.cs +NPC/Components/ + NPCAvoidanceComponent.cs + NPCComponent.cs + NPCImprintingOnSpawnBehaviourComponent.cs + NPCJukeComponent.cs + NPCMeleeCombatComponent.cs + NPCPathfindPointComponent.cs + NPCRangedCombatComponent.cs + NPCRetaliationComponent.cs + NPCSteeringComponent.cs + NPCUseActionOnTargetComponent.cs +NPC/ + Events/NPCSteeringEvent.cs + HTN/ + HTNBranch.cs + HTNComponent.cs + HTNCompoundPrototype.cs + HTNCompoundTask.cs + HTNPlan.cs + HTNPlanJob.cs + HTNPlanState.cs + HTNSystem.cs + HTNTask.cs + IHTNCompound.cs + IHtnConditionalShutdown.cs + Preconditions/ + ActiveHandComponentPrecondition.cs + ActiveHandEntityPrecondition.cs + ActiveHandFreePrecondition.cs + BuckledPrecondition.cs + CoordinatesInRangePrecondition.cs + CoordinatesNotInRangePrecondition.cs + GunAmmoPrecondition.cs + HandcuffedPrecondition.cs + HasOrdersPrecondition.cs + HasStatusEffectPrecondition.cs + HTNPrecondition.cs + HungryPrecondition.cs + InContainerPrecondition.cs + InFriendlyContainerPrecondition.cs + IsEmaggedPrecondition.cs + ItemTogglePrecondition.cs + KeyExistsPrecondition.cs + KeyNotExistsPrecondition.cs + Math/ + KeyBoolEqualsPrecondition.cs + KeyFloatEqualsPrecondition.cs + KeyFloatGreaterPrecondition.cs + KeyFloatLessPrecondition.cs + NeedToRackBoltPrecondition.cs + PulledPrecondition.cs + TargetInLOSPrecondition.cs + TargetInRangePrecondition.cs + ThirstyPrecondition.cs + WieldedPrecondition.cs + PrimitiveTasks/ + HTNOperator.cs + HTNPrimitiveTask.cs + Operators/Combat/ + ContainerOperator.cs + EscapeOperator.cs + JukeOperator.cs + Melee/ + MeleeAttackOperator.cs + MeleeOperator.cs + RackBoltOperator.cs + Ranged/GunOperator.cs + UnbuckleOperator.cs + UnPullOperator.cs + WieldOperator.cs + Operators/Interactions/ + AltInteractOperator.cs + DropOperator.cs + EquipOperator.cs + InteractWithOperator.cs + SwapToFreeHandOperator.cs + UseItemInHandOperator.cs + Operators/Math/ + AddFloatOperator.cs + SetBoolOperator.cs + SetFloatOperator.cs + SetRandomFloatOperator.cs + Operators/ + MoveToOperator.cs + NoOperator.cs + PickAccessibleComponentOperator.cs + PickAccessibleOperator.cs + PickRandomRotationOperator.cs + PlaySoundOperator.cs + RandomOperator.cs + RotateToTargetOperator.cs + SayKeyOperator.cs + SpeakOperator.cs + Specific/ + DisposalInsertOperator.cs + EnsureComponentOperator.cs + MedibotInjectOperator.cs + PickEntityNearMobOperator.cs + PickNearbyInjectableOperator.cs + RaiseEventForOwnerOperator.cs + Test/PickPathfindPointOperator.cs + UtilityOperator.cs + WaitOperator.cs + NPCBlackboard.cs + NPCBlackboardSerializer.cs + Pathfinding/ + GridPathfindingChunk.cs + GridPathfindingComponent.cs + PathfindingRequestEvent.cs + PathfindingSystem.AStar.cs + PathfindingSystem.BFS.cs + PathfindingSystem.Breadth.cs + PathfindingSystem.Common.cs + PathfindingSystem.cs + PathfindingSystem.Distance.cs + PathfindingSystem.Grid.cs + PathfindingSystem.Helper.cs + PathfindingSystem.Simple.cs + PathfindingSystem.Splines.cs + PathfindingSystem.Widen.cs + PathFlags.cs + PathPoly.cs + PathPortal.cs + PathRequest.cs + PathResult.cs + Queries/Considerations/ + DrinkValueCon.cs + FoodValueCon.cs + OrderedTargetCon.cs + TargetAccessibleCon.cs + TargetAmmoCon.cs + TargetAmmoMatchesCon.cs + TargetDistanceCon.cs + TargetHealthCon.cs + TargetInLOSCon.cs + TargetInLOSOrCurrentCon.cs + TargetIsAliveCon.cs + TargetIsCritCon.cs + TargetIsDeadCon.cs + TargetIsStunnedCon.cs + TargetIsVisibleCon.cs + TargetLowTempCon.cs + TargetMeleeCon.cs + TargetOnFireCon.cs + TargetTargetingCon.cs + UtilityConsideration.cs + Queries/Curves/ + BoolCurve.cs + InverseBoolCurve.cs + IUtilityCurve.cs + PresetCurve.cs + QuadraticCurve.cs + UtilityCurvePresetPrototype.cs + Queries/Queries/ + ClothingSlotFilter.cs + ComponentFilter.cs + ComponentQuery.cs + InventoryQuery.cs + NearbyHostilesQuery.cs + PuddleFilter.cs + RemoveAnchored.cs + UtilityQuery.cs + UtilityQueryFilter.cs + Queries/ + UtilityQueryPrototype.cs + UtilityService.cs + Systems/ + NPCCombatSystem.cs + NPCCombatSystem.Melee.cs + NPCCombatSystem.Ranged.cs + NPCImprintingOnSpawnBehaviourSystem.cs + NPCJukeSystem.cs + NPCPerceptionSystem.cs + NPCPerceptionSystem.RecentlyInjected.cs + NPCRetaliationSystem.cs + NPCSteeringSystem.Context.cs + NPCSteeringSystem.cs + NPCSteeringSystem.Obstacles.cs + NPCSystem.Blackboard.cs + NPCSystem.cs + NPCUseActionOnTargetSystem.cs + NPCUtilitySystem.cs + UI/NPCEui.cs +Nuke/Commands/ + SendNukeCodesCommand.cs + ToggleNukeCommand.cs +Nuke/ + NukeCodePaperComponent.cs + NukeCodePaperSystem.cs + NukeComponent.cs + NukeLabelComponent.cs + NukeLabelSystem.cs + NukeSystem.cs +NukeOps/ + WarDeclaratorComponent.cs + WarDeclaratorSystem.cs +Nutrition/Components/ + BadDrinkComponent.cs + BadFoodComponent.cs + CigarComponent.cs + FatExtractorComponent.cs + IgnoreBadFoodComponent.cs + SliceableFoodComponent.cs + SmokingPipeComponent.cs + TrashOnSolutionEmptyComponent.cs + VapeComponent.cs +Nutrition/EntitySystems/ + AnimalHusbandrySystem.cs + CreamPieSystem.cs + FatExtractorSystem.cs + SliceableFoodSystem.cs + SmokingSystem.Cigar.cs + SmokingSystem.cs + SmokingSystem.SmokingPipe.cs + SmokingSystem.Vape.cs + TrashOnSolutionEmptySystem.cs +Nutrition/ + Events/DrinkEvents.cs + Hungry.cs + SetNutrit.cs + Thirsty.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Server.AssemblyInfo.cs +Objectives/Commands/ + AddObjectiveCommand.cs + ListObjectivesCommand.cs + RemoveObjectiveCommand.cs +Objectives/Components/ + CarpRiftsConditionComponent.cs + CodeConditionSystem.cs + CounterConditionComponent.cs + DieConditionComponent.cs + DoorjackConditionComponent.cs + EscapeShuttleConditionComponent.cs + FreeObjectiveComponent.cs + HelpProgressConditionComponent.cs + HijackShuttleConditionComponent.cs + HijackTradeStationConditionComponent.cs + KeepAliveConditionComponent.cs + KillPersonConditionComponent.cs + MailFraudConditionComponent.cs + MultipleTraitorsRequirementComponent.cs + NotCommandRequirementComponent.cs + NotJobRequirementComponent.cs + NumberObjectiveComponent.cs + ObjectiveBlacklistRequirementComponent.cs + ObjectiveLimitComponent.cs + PickRandomPersonComponent.cs + PickSpecificPersonComponent.cs + RoleRequirementComponent.cs + SocialObjectiveComponent.cs + SpeciesRequirmentComponent.cs + SpiderChargeConditionComponent.cs + StealConditionComponent.cs + StealResearchConditionComponent.cs + SupercriticalAnomaliesConditionComponent.cs + SurviveConditionComponent.cs + TargetObjectiveComponent.cs + TargetOverrideComponent.cs +Objectives/ + ObjectivesSystem.cs + Systems/ + CarpRiftsConditionSystem.cs + CodeConditionSystem.cs + ConditionCounterSystem.cs + DieConditionSystem.cs + EscapeShuttleConditionSystem.cs + FreeObjectiveSystem.cs + HelpProgressConditionSystem.cs + HijackShuttleConditionSystem.cs + HijackTradeStationConditionSystem.cs + KeepAliveCondition.cs + KillPersonConditionSystem.cs + MailFraudObjectiveSystem.cs + MultipleTraitorsRequirementSystem.cs + NinjaConditionsSystem.cs + NotCommandRequirementSystem.cs + NotJobRequirementSystem.cs + NumberObjectiveSystem.cs + ObjectiveBlacklistRequirementSystem.cs + ObjectiveLimitSystem.cs + PickObjectiveTargetSystem.cs + RoleRequirementSystem.cs + SpeciesRequirementSystem.cs + StealConditionSystem.cs + SupercriticalAnomaliesConditionSystem.cs + SurviveConditionSystem.cs + TargetObjectiveSystem.cs +PAI/PAISystem.cs +Paper/ + PaperRandomStoryComponent.cs + PaperRandomStorySystem.cs +Parallax/ + BiomeSystem.Commands.cs + BiomeSystem.cs + ParallaxSystem.cs +ParticleAccelerator/Components/ + ParticleAcceleratorControlBoxComponent.cs + ParticleProjectileComponent.cs +ParticleAccelerator/EntitySystems/ + ParticleAcceleratorSystem.ControlBox.cs + ParticleAcceleratorSystem.cs + ParticleAcceleratorSystem.Emitter.cs + ParticleAcceleratorSystem.PowerBox.cs +ParticleAccelerator/Wires/ + ParticleAcceleratorInterfaceWireAction.cs + ParticleAcceleratorLimiterWireAction.cs + ParticleAcceleratorStrengthWireAction.cs + ParticleAcceleratorToggleWireAction.cs +Payload/EntitySystems/PayloadSystem.cs +PDA/ + PdaSystem.cs + Ringer/ + RingerAccessUplinkComponent.cs + RingerSystem.cs +Physics/Components/ + ChaoticJumpComponent.cs + ChasingWalkComponent.cs + RandomWalkComponent.cs +Physics/Controllers/ + ChaoticJumpSystem.cs + ChasingWalkSystem.cs + ConveyorController.cs + MoverController.cs + RandomWalkController.cs +Pinpointer/ + NavMapSystem.cs + PinpointerSystem.cs + StationMapSystem.cs + StationMapUserComponent.cs +Placement/PlacementLoggerSystem.cs +Players/JobWhitelist/ + JobWhitelistManager.cs + JobWhitelistSystem.cs +Players/ + PlayerSystem.cs + PlayTimeTracking/ + PlayTimeTrackingManager.cs + PlayTimeTrackingSystem.cs + RateLimiting/PlayerRateLimitManager.cs +PneumaticCannon/PneumaticCannonSystem.cs +Pointing/Components/ + PointingArrowAngeringComponent.cs + PointingArrowComponent.cs + RoguePointingArrowComponent.cs +Pointing/EntitySystems/ + PointingSystem.cs + RoguePointingSystem.cs +Points/PointSystem.cs +Polymorph/Components/ + PolymorphableComponent.cs + PolymorphedEntityComponent.cs +Polymorph/Systems/ + ChameleonProjectorSystem.cs + PolymorphSystem.cs + PolymorphSystem.Map.cs +Polymorph/Toolshed/ + PolymorphCommand.cs + UnpolymorphCommand.cs +Popups/PopupSystem.cs +Power/Commands/ + PowerStatCommand.cs + PowerValidateCommand.cs +Power/Components/ + ApcComponent.cs + ApcElectronicsComponent.cs + ApcPowerProviderComponent.cs + ApcPowerReceiverComponent.cs + BaseApcNetComponent.cs + BaseNetConnectorComponent.cs + BasePowerNetComponent.cs + BatteryChargerComponent.cs + BatteryDischargerComponent.cs + BatteryInterfaceComponent.cs + CableComponent.cs + CablePlacerComponent.cs + CableVisComponent.cs + ExtensionCableProviderComponent.cs + ExtensionCableReceiverComponent.cs + PowerChargeComponent.cs + PowerConsumerComponent.cs + PowerMonitoringDeviceComponent.cs + PowerNetworkBatteryComponent.cs + PowerSupplierComponent.cs + PowerSwitchComponent.cs + RiggableComponent.cs +Power/EntitySystems/ + ActivatableUIRequiresPowerSystem.cs + ApcSystem.cs + BatteryInterfaceSystem.cs + BatterySystem.cs + CableMultitoolSystem.cs + CableSystem.cs + CableSystem.Placer.cs + CableVisSystem.cs + ExtensionCableSystem.cs + PowerChargeSystem.cs + PowerMonitoringConsoleSystem.cs + PowerNetConnectorSystem.cs + PowerNetSystem.cs + PowerReceiverSystem.cs + PowerStateSystem.cs + RiggableSystem.cs + StaticPowerSystem.cs +Power/ + Events/PowerPulseEvent.cs + Generation/Teg/ + TegCirculatorComponent.cs + TegGeneratorComponent.cs + TegNodeGroup.cs + TegSensorData.cs + TegSystem.cs + Generator/ + ChemicalFuelGeneratorAdapterComponent.cs + ChemicalFuelGeneratorDirectSourceComponent.cs + GasPowerReceiverComponent.cs + GasPowerReceiverSystem.cs + GeneratorExhaustGasSystem.cs + GeneratorSignalControlComponent.cs + GeneratorSignalControlSystem.cs + GeneratorSystem.cs + PortableGeneratorSystem.cs + PowerSwitchableSystem.cs + SolidFuelGeneratorAdapterComponent.cs + NodeGroups/ + ApcNet.cs + BaseNetConnectorNodeGroup.cs + BasePowerNet.cs + IBasePowerNet.cs + PowerNet.cs + Nodes/ + CableDeviceNode.cs + CableNode.cs + CableTerminalNode.cs + CableTerminalPortNode.cs + Pow3r/ + BatteryRampPegSolver.cs + IPowerSolver.cs + NoOpSolver.cs + PowerSolverShared.cs + PowerState.cs + PowerWireAction.cs + SetBatteryPercentCommand.cs + SMES/ + SmesComponent.cs + SmesSystem.cs +PowerSink/ + PowerSinkComponent.cs + PowerSinkSystem.cs +Prayer/PrayerSystem.cs +Preferences/Managers/ + IServerPreferencesManager.cs + ServerPreferencesManager.cs +Procedural/ + DungeonAtlasTemplateComponent.cs + DungeonJob/ + DungeonJob.AutoCabling.cs + DungeonJob.Biome.cs + DungeonJob.BiomeMarkerLayer.cs + DungeonJob.BoundaryWall.cs + DungeonJob.CornerClutter.cs + DungeonJob.Corridor.cs + DungeonJob.CorridorClutter.cs + DungeonJob.CorridorDecalSkirting.cs + DungeonJob.cs + DungeonJob.DunGenNoiseDistance.cs + DungeonJob.DunGenPrefab.cs + DungeonJob.DunGenReplaceTile.cs + DungeonJob.DungeonEntrance.cs + DungeonJob.EntityTableDunGen.cs + DungeonJob.EntranceFlank.cs + DungeonJob.Exterior.cs + DungeonJob.ExternalWindow.cs + DungeonJob.Fill.cs + DungeonJob.Helpers.cs + DungeonJob.InternalWindow.cs + DungeonJob.Junction.cs + DungeonJob.MiddleConnection.cs + DungeonJob.Mobs.cs + DungeonJob.Noise.cs + DungeonJob.Ore.cs + DungeonJob.RoomEntrance.cs + DungeonJob.SplineDungeonConnector.cs + DungeonJob.WallMount.cs + DungeonJob.Worm.cs + DungeonSystem.Commands.cs + DungeonSystem.cs + DungeonSystem.Helpers.cs + DungeonSystem.Rooms.cs + RoomFillComponent.cs + RoomFillSystem.cs +Program.cs +Projectiles/ProjectileSystem.cs +Puppet/VentriloquistPuppetSystem.cs +Radiation/Components/ + RadiationBlockerComponent.cs + RadiationBlockingContainerComponent.cs + RadiationGridResistanceComponent.cs + RadiationReceiverComponent.cs +Radiation/ + Events/RadiationSystemUpdatedEvent.cs + Systems/ + GeigerSystem.cs + RadiationSystem.Blockers.cs + RadiationSystem.cs + RadiationSystem.Cvar.cs + RadiationSystem.Debug.cs + RadiationSystem.GridCast.cs +Radio/EntitySystems/ + HeadsetSystem.cs + JammerSystem.cs + RadioDeviceSystem.cs + RadioSystem.cs +Radio/RadioEvent.cs +RandomAppearance/ + RandomAppearanceComponent.cs + RandomAppearanceSystem.cs +RandomMetadata/ + RandomMetadataComponent.cs + RandomMetadataSystem.cs +RatKing/RatKingSystem.cs +Remotes/DoorRemoteSystem.cs +RequiresGrid/ + RequiresGridComponent.cs + RequiresGridSystem.cs +Research/Components/ + ResearchConsoleComponent.cs + ResearchPointSourceComponent.cs +Research/Disk/ + ResearchDiskComponent.cs + ResearchDiskSystem.cs +Research/Systems/ + ResearchStealerSystem.cs + ResearchSystem.Client.cs + ResearchSystem.Console.cs + ResearchSystem.cs + ResearchSystem.PointSource.cs + ResearchSystem.Server.cs + ResearchSystem.Technology.cs +Research/TechnologyDisk/Components/ + DiskConsoleComponent.cs + DiskConsolePrintingComponent.cs +Research/TechnologyDisk/Systems/DiskConsoleSystem.cs +Resist/ + CanEscapeInventoryComponent.cs + EscapeInventorySystem.cs + ResistLockerComponent.cs + ResistLockerSystem.cs +Respawn/SpecialRespawnSystem.cs +Revenant/ + Components/EssenceComponent.cs + EntitySystems/ + CorporealSystem.cs + EssenceSystem.cs + RevenantOverloadedLightsSystem.cs + RevenantSystem.Abilities.cs + RevenantSystem.cs +Revolutionary/Components/ + CommandStaffComponent.cs + RevolutionaryConverterComponent.cs +Revolutionary/ + DeconvertedEui.cs + RevolutionarySystem.cs +Robotics/Systems/RoboticsConsoleSystem.cs +Roles/ + AddRoleCommand.cs + Jobs/JobSystem.cs + ListRolesCommand.cs + ParadoxCloneRoleSystem.cs + RemoveRoleCommand.cs + RoleBriefingSystem.cs + RoleCodeword/RoleCodewordSystem.cs + RoleSystem.cs +Rotation/RotationVisualsSystem.cs +RoundEnd/RoundEndSystem.cs +Salvage/Expeditions/ + SalvageExpeditionComponent.cs + SalvageShuttleComponent.cs +Salvage/ + FultonSystem.cs + JobBoard/ + JobBoardCommands.cs + JobBoardLabelComponent.cs + SalvageJobBoardSystem.cs + SalvageJobsDataComponent.cs + Magnet/ + SalvageMagnetComponent.cs + SalvageMagnetDataComponent.cs + SalvageMagnetTargetComponent.cs + SalvageMobRestrictionsComponent.cs + RestrictedRangeSystem.cs + SalvageRulerCommand.cs + SalvageSystem.cs + SalvageSystem.ExpeditionConsole.cs + SalvageSystem.Expeditions.cs + SalvageSystem.Magnet.cs + SalvageSystem.Runner.cs + SpawnSalvageMissionJob.cs +Sandbox/ + Commands/ColorNetworkCommand.cs + SandboxSystem.cs +Screens/ + Components/ScreenComponent.cs + Systems/ScreenSystem.cs +Security/GenpopSystem.cs +SensorMonitoring/ + BatterySensorComponent.cs + BatterySensorSystem.cs + SensorMonitoringConsoleComponent.cs + SensorMonitoringConsoleSystem.cs + SensorMonitoringConsoleSystem.UI.cs +Sericulture/SericultureSystem.cs +ServerInfo/ServerInfoManager.cs +ServerUpdates/ServerUpdateManager.cs +Shuttles/Commands/ + DelayShuttleRoundEndCommand.cs + DockCommand.cs + DockEmergencyShuttleCommand.cs + FTLDiskCommand.cs + LaunchEmergencyShuttleCommand.cs +Shuttles/Components/ + ArrivalsBlacklistComponent.cs + ArrivalsShuttleComponent.cs + ArrivalsSourceComponent.cs + DockingComponent.cs + DockingSignalControlComponent.cs + EmergencyShuttleComponent.cs + EscapePodComponent.cs + FTLBeaconComponent.cs + FTLExclusionComponent.cs + GridFillComponent.cs + GridSpawnComponent.cs + IFFConsoleComponent.cs + PendingClockInComponent.cs + PriorityDockComponent.cs + ShuttleComponent.cs + ShuttleConsoleComponent.cs + StationAnchorComponent.cs + StationArrivalsComponent.cs + StationCargoShuttleComponent.cs + StationCentcommComponent.cs + StationEmergencyShuttleComponent.cs + ThrusterComponent.cs +Shuttles/ + DockingConfig.cs + DroneConsoleComponent.cs + Events/ + ConsoleFTLAttemptEvent.cs + ConsoleShuttleEvent.cs + DockEvent.cs + EmergencyShuttleAuthorizedEvent.cs + FTLCompletedEvent.cs + FTLRequestEvent.cs + FTLStartedEvent.cs + FTLTagEvent.cs + ShuttleConsoleFTLStartEvent.cs + ShuttleFlattenEvent.cs + UndockEvent.cs + Systems/ + ArrivalsSystem.cs + DockingSignalControlSystem.cs + DockingSystem.cs + DockingSystem.Shuttle.cs + EmergencyShuttleSystem.Console.cs + EmergencyShuttleSystem.cs + RadarConsoleSystem.cs + ShuttleConsoleSystem.cs + ShuttleConsoleSystem.Drone.cs + ShuttleConsoleSystem.FTL.cs + ShuttleSystem.cs + ShuttleSystem.FasterThanLight.cs + ShuttleSystem.GridFill.cs + ShuttleSystem.IFF.cs + ShuttleSystem.Impact.cs + SpaceGarbageSystem.cs + StationAnchorSystem.cs + ThrusterSystem.cs +Silicons/Borgs/ + BorgSwitchableTypeSystem.cs + BorgSystem.cs + BorgSystem.Transponder.cs +Silicons/Bots/ + HugBotSystem.cs + RecentlyHuggedByHugBotComponent.cs +Silicons/Laws/ + IonLawLocalizationSystem.cs + IonLawSystem.cs + IonStormSystem.cs + SiliconLawEui.cs + SiliconLawSystem.cs + StartIonStormedSystem.cs +Silicons/StationAi/ + AiInteractWireAction.cs + AiVisionWireAction.cs + StationAiFixerConsoleSystem.cs + StationAiSystem.cs +Singularity/Components/ + GravityWellComponent.cs + RadiationCollectorComponent.cs + SingularityAttractorComponent.cs + SinguloFoodComponent.cs +Singularity/EntitySystems/ + ContainmentFieldGeneratorSystem.cs + ContainmentFieldSystem.cs + EmitterSystem.cs + EventHorizonSystem.cs + GravityWellSystem.cs + RadiationCollectorSystem.cs + SingularityAttractorSystem.cs + SingularityGeneratorSystem.cs + SingularitySystem.cs +Singularity/Events/ + EntityConsumedByEventHorizonEvent.cs + EventHorizonAttemptConsumeEntityEvent.cs + EventHorizonConsumedEntityEvent.cs + EventHorizonContainedEvent.cs + TilesConsumedByEventHorizonEvent.cs +Singularity/StartSingularityEngineCommand.cs +SmartFridge/SmartFridgeSystem.cs +Solar/Components/ + SolarControlConsoleComponent.cs + SolarPanelComponent.cs +Solar/EntitySystems/ + PowerSolarControlConsoleSystem.cs + PowerSolarSystem.cs +Sound/ + EmitSoundSystem.cs + SpamEmitSoundRequirePowerSystem.cs +Spawners/Components/ + ConditionalSpawnerComponent.cs + ContainerSpawnPointComponent.cs + EntityTableSpawnerComponent.cs + GridSpawnPointWhitelistComponent.cs + ISpawnPoint.cs + RandomDecalSpawnerComponent.cs + RandomSpawnerComponent.cs + SpawnOnDespawnComponent.cs + SpawnPointComponent.cs + TimedSpawnerComponent.cs +Spawners/EntitySystems/ + ConditionalSpawnerSystem.cs + ContainerSpawnPointSystem.cs + RandomDecalSpawnerSystem.cs + SpawnerSystem.cs + SpawnOnDespawnSystem.cs + SpawnPointSystem.cs +Species/Systems/NymphSystem.cs +Speech/ + AccentSystem.cs + Components/ + AddAccentClothingComponent.cs + BackwardsAccentComponent.cs + BarkAccentComponent.cs + BleatingAccentComponent.cs + BlockListeningComponent.cs + FrenchAccentComponent.cs + FrontalLispComponent.cs + GermanAccentComponent.cs + ListenWireAction.cs + LizardAccentComponent.cs + MobsterAccentComponent.cs + MonkeyAccentComponent.cs + MothAccentComponent.cs + MumbleAccentComponent.cs + OwOAccentComponent.cs + ParrotAccentComponent.cs + PirateAccentComponent.cs + ReplacementAccentComponent.cs + RussianAccentComponent.cs + ScrambledAccentComponent.cs + SkeletonAccentComponent.cs + SlowAccentComponent.cs + SlurredAccentComponent.cs + SouthernAccentComponent.cs + SpanishAccentComponent.cs + SpeechWireAction.cs + StutteringAccentComponent.cs + UnblockableSpeechComponent.cs + VoiceOverrideComponent.cs + EmotesMenuSystem.cs + EntitySystems/ + AddAccentClothingSystem.cs + BackwardsAccentSystem.cs + BarkAccentSystem.cs + BleatingAccentSystem.cs + BlockListeningSystem.cs + DamagedSiliconAccentSystem.cs + FrenchAccentSystem.cs + FrontalLispSystem.cs + GermanAccentSystem.cs + ListeningSystem.cs + LizardAccentSystem.cs + MeleeSpeechSystem.cs + MobsterAccentSystem.cs + MonkeyAccentSystem.cs + MothAccentSystem.cs + MumbleAccentSystem.cs + OwOAccentSystem.cs + ParrotAccentSystem.cs + PirateAccentSystem.cs + RatvarianLanguageSystem.cs + ReplacementAccentSystem.cs + RussianAccentSystem.cs + ScrambledAccentSystem.cs + SkeletonAccentSystem.cs + SlowAccentSystem.cs + SlurredSystem.cs + SouthernAccentSystem.cs + SpanishAccentSystem.cs + SpeakOnActionSystem.cs + StutteringSystem.cs + UnblockableSpeechSystem.cs + VocalSystem.cs + VoiceOverrideSystem.cs + Muting/MutingSystem.cs + Prototypes/ReplacementAccentPrototype.cs + SpeechNoiseSystem.cs +Spider/SpiderSystem.cs +SprayPainter/SprayPainterSystem.cs +Spreader/ + ActiveEdgeSpreaderComponent.cs + EdgeSpreaderComponent.cs + GrowingKudzuComponent.cs + KudzuComponent.cs + KudzuSystem.cs + SpreaderGridComponent.cs + SpreaderSystem.cs + SpreadNeighborsEvent.cs +Sprite/ + RandomSpriteSystem.cs + ScaleVisualsSystem.cs +Stack/StackSystem.cs +Station/Commands/ + BankCommand.cs + JobsCommand.cs + StationCommand.cs +Station/Components/ + BecomesStationComponent.cs + StationBiomeComponent.cs + StationEventEligibleComponent.cs + StationJobsComponent.cs + StationNameSetupComponent.cs + StationSpawningComponent.cs + StationVariationHasRunComponent.cs +Station/Events/ + StationJobsGetCandidatesEvent.cs + StationPostInitEvent.cs +Station/Systems/ + StationBiomeSystem.cs + StationJobsSystem.cs + StationJobsSystem.Roundstart.cs + StationNameSystem.cs + StationSpawningSystem.cs + StationSystem.cs +StationEvents/ + BasicStationEventSchedulerSystem.cs + Components/ + AlertLevelInterceptionRuleComponent.cs + AnomalySpawnRuleComponent.cs + BasicStationEventSchedulerComponent.cs + BluespaceArtifactRuleComponent.cs + BluespaceLockerRuleComponent.cs + BreakerFlipRuleComponent.cs + BureaucraticErrorRuleComponent.cs + CargoGiftsRuleComponent.cs + ClericalErrorRuleComponent.cs + FalseAlarmRuleComponent.cs + GasLeakRuleComponent.cs + GreytideVirusComponent.cs + ImmovableRodRuleComponent.cs + IonStormRuleComponent.cs + KudzuGrowthRuleComponent.cs + MassHallucinationsRuleComponent.cs + MeteorSwarmComponent.cs + PowerGridCheckRuleComponent.cs + RampingStationEventSchedulerComponent.cs + RandomEntityStorageSpawnRuleComponent.cs + RandomSentienceRuleComponent.cs + RandomSpawnRuleComponent.cs + SentienceTargetComponent.cs + SolarFlareRuleComponent.cs + SpaceSpawnRuleComponent.cs + StationEventComponent.cs + VentClogRuleComponent.cs + VentCritterSpawnLocationComponent.cs + VentCrittersRuleComponent.cs + VentHordeRuleComponent.cs + EventManagerSystem.cs + Events/ + AlertLevelInterceptionRule.cs + AnomalySpawnRule.cs + BluespaceArtifactRule.cs + BluespaceLockerRule.cs + BreakerFlipRule.cs + BureaucraticErrorRule.cs + CargoGiftsRule.cs + ClericalErrorRule.cs + FalseAlarmRule.cs + GasLeakRule.cs + GreytideVirusRule.cs + ImmovableRodRule.cs + IonStormRule.cs + KudzuGrowthRule.cs + MassHallucinationsRule.cs + MeteorSwarmSystem.cs + PowerGridCheckRule.cs + RandomEntityStorageSpawnRule.cs + RandomSentienceRule.cs + RandomSpawnRule.cs + SolarFlareRule.cs + SpaceSpawnRule.cs + StationEventSystem.cs + VentClogRule.cs + VentCrittersRule.cs + VentHordeRule.cs + RampingStationEventSchedulerSystem.cs +StationRecords/ + Components/GeneralStationRecordConsoleComponent.cs + Systems/ + GeneralStationRecordConsoleSystem.cs + StationRecordsSystem.cs +Stealth/StealthSystem.cs +Storage/Components/ + BluespaceLockerComponent.cs + CursedEntityStorageComponent.cs + SpawnItemsOnUseComponent.cs + SpawnTableOnUseComponent.cs +Storage/EntitySystems/ + BluespaceLockerSystem.cs + CursedEntityStorageSystem.cs + EntityStorageSystem.cs + ItemCounterSystem.cs + ItemMapperSystem.cs + SpawnItemsOnUseSystem.cs + SpawnTableOnUseSystem.cs + StorageSystem.cs + StorageSystem.Fill.cs +Storage/StorageCommand.cs +Store/Conditions/ + BuyBeforeCondition.cs + BuyerAntagCondition.cs + BuyerDepartmentCondition.cs + BuyerJobCondition.cs + BuyerObjectiveWhitelistCondition.cs + BuyerSpeciesCondition.cs + BuyerWhitelistCondition.cs + ListingLimitedStockCondition.cs + StoreWhitelistCondition.cs +Store/ + StoreRefundComponent.cs + Systems/ + StoreSystem.Command.cs + StoreSystem.cs + StoreSystem.Refund.cs + StoreSystem.Ui.cs +StoreDiscount/Systems/StoreDiscountSystem.cs +Strip/StrippableSystem.cs +Stunnable/ + Components/StunOnCollideComponent.cs + StunSystem.cs + Systems/ + StunbatonSystem.cs + StunOnCollideSystem.cs +SubFloor/ + SubFloorHideSystem.cs + TrayScannerSystem.cs +SurveillanceCamera/ + CameraMapVisibilityWireAction.cs + Components/ + ActiveSurveillanceCameraMonitorComponent.cs + SurveillanceCameraMicrophoneComponent.cs + SurveillanceCameraMonitorComponent.cs + SurveillanceCameraRouterComponent.cs + SurveillanceCameraSpeakerComponent.cs + Systems/ + SurveillanceCameraMapSystem.cs + SurveillanceCameraMicrophoneSystem.cs + SurveillanceCameraMonitorSystem.cs + SurveillanceCameraRouterSystem.cs + SurveillanceCameraSpeakerSystem.cs + SurveillanceCameraSystem.Collide.cs + SurveillanceCameraSystem.cs +Tabletop/Components/ + TabletopGameComponent.cs + TabletopGamerComponent.cs +Tabletop/ + TabletopBackgammonSetup.cs + TabletopCheckerSetup.cs + TabletopChessSetup.cs + TabletopEmptySetup.cs + TabletopHologramComponent.cs + TabletopParchisSetup.cs + TabletopSession.cs + TabletopSessionPlayerData.cs + TabletopSetup.cs + TabletopSystem.cs + TabletopSystem.Map.cs + TabletopSystem.Session.cs +Telephone/TelephoneSystem.cs +Teleportation/ + HandTeleporterSystem.cs + PortalSystem.cs + TeleportLocationsSystem.cs +Temperature/Components/ + ChangeTemperatureOnCollideComponent.cs + ContainerTemperatureComponent.cs + InternalTemperatureComponent.cs + TemperatureProtectionComponent.cs +Temperature/Systems/ + EntityHeaterSystem.cs + TemperatureSystem.cs + TemperatureSystem.Damage.cs +Tesla/Components/ + LightningArcShooterComponent.cs + LightningSparkingComponent.cs + TeslaCoilComponent.cs + TeslaEnergyBallComponent.cs +Tesla/EntitySystem/ + LightningArcShooterSystem.cs + LightningSparkingSystem.cs + TeslaCoilSystem.cs + TeslaEnergyBallSystem.cs +Thief/ + Components/ThiefUndeterminedBackpackComponent.cs + Systems/ThiefUndeterminedBackpackSystem.cs +Tiles/ + RequiresTileSystem.cs + TileEntityEffectComponent.cs + TileEntityEffectSystem.cs +Tips/TipsSystem.cs +Tools/Innate/ + InnateToolComponent.cs + InnateToolSystem.cs +Tools/ToolSystem.cs +Toolshed/Commands/ + AdminDebug/ACmdCommand.cs + Misc/ + CloneCommand.cs + MsgCommand.cs + ScaleCommand.cs + StatusEffects/StatusEffectCommand.cs + Verbs/RunVerbAsCommand.cs + VisualizeCommand.cs +Toolshed/TypeParsers/StatusEffects/StatusEffectCompletionParser.cs +Traitor/Components/ + AutoTraitorComponent.cs + TraitorCodePaperComponent.cs +Traitor/Systems/ + AutoTraitorSystem.cs + TraitorCodePaperSystem.cs +Traitor/Uplink/ + Commands/AddUplinkCommand.cs + SurplusBundle/ + SurplusBundleComponent.cs + SurplusBundleSystem.cs + UplinkComponent.cs + UplinkSystem.cs +Traits/Assorted/ + BuckleOnMapInitComponent.cs + BuckleOnMapInitSystem.cs + ParacusiaSystem.cs + UnrevivableSystem.cs +Traits/TraitSystem.cs +Trigger/Systems/ + AlertLevelChangeOnTriggerSystem.cs + FireStackOnTriggerSystem.cs + GameRuleTriggerSystem.cs + GhostKickUserOnTriggerSystem.cs + IgniteOnTriggerSystem.cs + PolymorphOnTriggerSystem.cs + RattleOnTriggerSystem.cs + ReleaseGasOnTriggerSystem.cs + SmokeOnTriggerSystem.cs + SpeakOnTriggerSystem.cs +TurretController/DeployableTurretControllerSystem.cs +Turrets/DeployableTurretSystem.cs +UserInterface/ + StatValuesCommand.cs + StatValuesEui.cs +VendingMachines/ + VendingMachineContrabandWireAction.cs + VendingMachineEjectItemWireAction.cs + VendingMachineSystem.cs +VentHorde/ + Components/VentHordeSpawnerComponent.cs + Systems/VentHordeSystem.cs +Verbs/Commands/ + InvokeVerbCommand.cs + ListVerbsCommand.cs +Verbs/VerbSystem.cs +Vocalization/Components/ + DatasetVocalizerComponent.cs + RadioVocalizerComponent.cs + VocalizerComponent.cs + VocalizerRequiresPowerComponent.cs +Vocalization/Systems/ + DatasetVocalizationSystem.cs + RadioVocalizationSystem.cs + VocalizationSystem.cs +VoiceMask/VoiceMaskSystem.cs +VoiceTrigger/ + StorageVoiceControlComponent.cs + StorageVoiceControlSystem.cs +Voting/ + IVoteHandle.cs + Managers/ + IVoteManager.cs + VoteManager.cs + VoteManager.DefaultVotes.cs + VoteCommands.cs + VoteFinishedEventArgs.cs + VoteFinishedEventHandler.cs + VoteOptions.cs + VotingSystem.cs +Wagging/WaggingSystem.cs +Weapons/ + DamageMarkerSystem.cs + Melee/ + Balloon/BalloonPopperSystem.cs + MeleeWeaponSystem.cs + WeaponRandom/ + WeaponRandomComponent.cs + WeaponRandomSystem.cs + Misc/ + GrapplingGunSystem.cs + TetherGunSystem.cs + Ranged/Components/ + AmmoCounterComponent.cs + ChemicalAmmoComponent.cs + RangedDamageSoundComponent.cs + Ranged/Systems/ + ChemicalAmmoSystem.cs + FlyBySoundSystem.cs + GunSystem.AutoFire.cs + GunSystem.Ballistic.cs + GunSystem.cs + GunSystem.Revolver.cs + GunSystem.Solution.cs +Weather/Commands/ + WeatherAddCommand.cs + WeatherRemoveCommand.cs + WeatherSetCommand.cs +Weather/WeatherSystem.cs +Whitelist/WhitelistCommands.cs +Wieldable/WieldableSystem.cs +Wires/ + BaseToggleWireAction.cs + BaseWireAction.cs + ComponentWireAction.cs + CutWireOnMapInitComponent.cs + CutWireOnMapInitSystem.cs + IWireAction.cs + WireLayout.cs + WiresComponent.cs + WiresSystem.cs +Xenoarchaeology/Artifact/ + RandomArtifactSpriteSystem.cs + UnlockNodeCommand.cs + XAE/Components/ + XAEChargeBatteryComponent.cs + XAECreateGasComponent.cs + XAECreatePuddleComponent.cs + XAEEmpInAreaComponent.cs + XAEFoamComponent.cs + XAEIgniteComponent.cs + XAELightFlickerComponent.cs + XAEPolymorphComponent.cs + XAEPortalComponent.cs + XAETelepathicComponent.cs + XAETemperatureComponent.cs + XAEThrowThingsAroundComponent.cs + XAETriggerExplosivesComponent.cs + XAE/ + XAEChargeBatterySystem.cs + XAECreateGasSystem.cs + XAECreatePuddleSystem.cs + XAEEmpInAreaSystem.cs + XAEFoamSystem.cs + XAEIgniteSystem.cs + XAELightFlickerSystem.cs + XAEPolymorphSystem.cs + XAEPortalSystem.cs + XAETelepathicSystem.cs + XAETemperatureSystem.cs + XAEThrowThingsAroundSystem.cs + XAETriggerExplosivesSystem.cs + XAT/Components/ + XATGasComponent.cs + XATMagnetComponent.cs + XATPressureComponent.cs + XATTemperatureComponent.cs + XAT/ + XATGasSystem.cs + XATMagnetSystem.cs + XATPressureSystem.cs + XATTemperatureSystem.cs + XenoArtifactCommands.cs + XenoArtifactSystem.cs + XenoArtifactSystem.ProcGen.cs +Xenoarchaeology/Equipment/ + ArtifactAnalyzerSystem.cs + Systems/ArtifactCrusherSystem.cs +Xenoborgs/XenoborgSystem.cs +Zombies/ + NonSpreaderZombieComponent.cs + ZombieAccentOverrideComponent.cs + ZombieSystem.cs + ZombieSystem.Transform.cs + +[metadataReferences] +../ + Content.Packaging/obj/Debug/net10.0/ref/Content.Packaging.dll + Content.Server.Database/obj/Debug/ref/Content.Server.Database.dll + Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll + Content.Shared/obj/Debug/ref/Content.Shared.dll + RobustToolbox/ + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Packaging/obj/Debug/net10.0/ref/Robust.Packaging.dll + Robust.Server/obj/Debug/ref/Robust.Server.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared.Scripting/obj/Debug/ref/Robust.Shared.Scripting.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + jetbrains.annotations/2025.2.4/lib/netstandard2.0/JetBrains.Annotations.dll + microsoft.data.sqlite.core/10.0.6/lib/net8.0/Microsoft.Data.Sqlite.dll + microsoft.entityframeworkcore.abstractions/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll + microsoft.entityframeworkcore.relational/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll + microsoft.entityframeworkcore.sqlite.core/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.Sqlite.dll + microsoft.entityframeworkcore/10.0.6/lib/net10.0/Microsoft.EntityFrameworkCore.dll + microsoft.extensions.caching.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + microsoft.extensions.caching.memory/10.0.6/lib/net10.0/Microsoft.Extensions.Caching.Memory.dll + microsoft.extensions.configuration.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll + microsoft.extensions.dependencyinjection.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll + microsoft.extensions.dependencyinjection/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll + microsoft.extensions.dependencymodel/10.0.6/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.extensions.http/3.1.0/lib/netcoreapp3.1/Microsoft.Extensions.Http.dll + microsoft.extensions.logging.abstractions/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll + microsoft.extensions.logging/10.0.6/lib/net10.0/Microsoft.Extensions.Logging.dll + microsoft.extensions.objectpool/10.0.0/lib/net10.0/Microsoft.Extensions.ObjectPool.dll + microsoft.extensions.options/10.0.6/lib/net10.0/Microsoft.Extensions.Options.dll + microsoft.extensions.primitives/10.0.6/lib/net10.0/Microsoft.Extensions.Primitives.dll + netcord/1.0.0-alpha.388/lib/net9.0/NetCord.dll + npgsql.entityframeworkcore.postgresql/10.0.0/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll + npgsql/10.0.0/lib/net10.0/Npgsql.dll + prometheus-net/8.2.1/lib/net7.0/Prometheus.NetStandard.dll + robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + sqlitepclraw.config.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.batteries_v2.dll + sqlitepclraw.core/3.0.2/lib/netstandard2.0/SQLitePCLRaw.core.dll + sqlitepclraw.provider.e_sqlite3/3.0.2/lib/net8.0/SQLitePCLRaw.provider.e_sqlite3.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll +/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll +/ + microsoft.entityframeworkcore.analyzers/10.0.6/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll + microsoft.extensions.logging.abstractions/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll + microsoft.extensions.options/10.0.6/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll + microsoft.net.illink.tasks/10.0.0/analyzers/dotnet/cs/ + ILLink.CodeFixProvider.dll + ILLink.RoslynAnalyzer.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.Server.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Server/Corvax/ChatFilter/ChatSystem.cs b/Content.Server/Corvax/ChatFilter/ChatSystem.cs index 1973c141930..0b6f8de4b33 100644 --- a/Content.Server/Corvax/ChatFilter/ChatSystem.cs +++ b/Content.Server/Corvax/ChatFilter/ChatSystem.cs @@ -5,6 +5,7 @@ namespace Content.Server.Chat.Systems; public sealed partial class ChatSystem { + private static readonly Regex ReplaceRegex = new("\\b(\\w+)\\b"); private static readonly Dictionary SlangReplace = new() { // Game @@ -24,6 +25,7 @@ public sealed partial class ChatSystem { "дек", "детектив" }, { "деку", "детективу" }, { "дека", "детектива" }, + { "дамаг", "ранение" }, { "мш", "имплант защиты разума" }, { "мщ", "имплант защиты разума" }, // Imperial rus replace @Ser1-1y start { "майншилд", "имплант защиты разума" }, @@ -69,6 +71,10 @@ public sealed partial class ChatSystem { "деловен", "диловен"}, // Imperial rus replace @Ser1-1y { "дило", "диловен"}, { "дил", "диловен"}, + { "куллдаун", "задержка"}, + { "кулдаун", "задержка"}, + { "кд", "задержка"}, + { "скил", "навык"}, { "антаг", "враг"}, // IC { "норм", "нормально" }, @@ -130,12 +136,26 @@ public sealed partial class ChatSystem { "разлокать", "разблокировать" }, { "юзнул", "использовал" }, { "хилл", "лечение" }, + { "хил", "лечение" }, + { "подхиль", "полечи" }, + { "хильни", "полечи" }, + { "хелп", "помоги" }, + { "хелпани", "помоги" }, + { "хелпанул", "помог" }, + { "рофл", "прикол" }, + { "рофлишь", "шутишь" }, { "брут", "механические" }, { "берн", "ожоговые" }, { "бёрн", "ожоговые" }, { "пирс", "колотые" }, { "крафт", "создание" }, { "фасту", "быстро" }, + { "пуш", "вперед" }, + { "рил", "реально" }, + { "фаст", "быстро" }, + { "фастом", "быстро" }, + { "дефать", "защищать" }, + { "ливаю", "ухожу" }, { "ливай", "уходи" }, // OOC { "афк", "ссд" }, @@ -146,6 +166,11 @@ public sealed partial class ChatSystem { "фрикилл", "плохо" }, { "лкм", "левая рука" }, { "пкм", "правая рука" }, + { "робаст", "сильный"}, + { "робастный", "сильный"}, // Imperial rus replace @Ser1-1y + { "робастить", "бить"}, // Imperial rus replace @Ser1-1y + { "анробаст", "слабый" }, + { "анроб", "слабый" }, { "лак", "удача" }, { "анлак", "неудача" }, // Брейнрот, сленг @@ -172,241 +197,251 @@ public sealed partial class ChatSystem { "щовел", "щавель" }, { "щовель", "щавель" }, { "кринж", "стыд" }, - // -- Дополнения к Game -- - { "данж", "подземелье" }, - { "данджа", "подземелья" }, - { "данджу", "подземелью" }, - { "данжем", "подземельем" }, - { "данжи", "подземелья" }, - { "данжей", "подземелий" }, - { "аое", "по площади" }, - { "аоешный", "площадной" }, - { "аоешка", "заклинание по площади" }, - { "соло", "в одиночку" }, - { "солка", "одиночка" }, - { "абуз", "злоупотребление" }, - { "абузить", "злоупотреблять" }, - { "баг", "прореха в мироздании" }, - { "баганутый", "искажённый" }, - { "крафтить", "созидать" }, - { "скрафтил", "создал" }, - { "пати", "отряд" }, - { "фарм", "добыча" }, - { "фармить", "промышлять" }, - { "нафармил", "добыл" }, - { "билд", "уклад" }, - { "билдить", "выстраивать" }, - { "танк", "ратник" }, - { "танчить", "принимать удар" }, - { "имб", "заговорённый" }, - { "дисбаланс", "неравновесие" }, - { "дизбаланс", "неравновесие" }, - { "диссбаланс", "неравновесие" }, - { "мета", "ведущий уклад" }, - { "метовый", "избранный" }, - { "педаль", "педальный" }, - { "агро", "вражье внимание" }, - { "агрить", "привлекать ворога" }, - { "агриться", "гневаться" }, - { "баф", "благословение" }, - { "бафнуть", "благословить" }, - { "гайд", "манускрипт" }, - { "спавн", "материализация" }, - { "спавниться", "материализоваться" }, - { "заспавнился", "материализовался" }, - { "спаун", "материализация" }, - { "спауниться", "материализоваться" }, - { "саппорт", "опора" }, - { "сапорт", "опора" }, - { "саппортить", "поддерживать" }, - { "скип", "пропуск" }, - { "скипать", "пропускать" }, - { "скипнуть", "пропустить" }, - { "нпс", "поселянин" }, - { "стан", "оцепенение" }, - { "станить", "оцепенять" }, - { "застанил", "оценил" }, - { "ульт", "заветная способность" }, - { "ульта", "заветная способность" }, - { "ульту", "заветную способность" }, - { "ультану", "применю заветное" }, - { "ультую", "применяю заветное" }, - { "ультуй", "примени заветное" }, - { "ультанул", "применил заветное" }, - { "аим", "прицел" }, - { "аимить", "целиться" }, - { "ренж", "расстояние" }, - { "кайт", "заманивание" }, - { "кайтить", "завлекать" }, - { "кайтил", "завлекал" }, - { "закайтил", "завлёк" }, - { "деф", "оборона" }, - { "дефать", "оборонять" }, - { "дефнуть", "защитить" }, - { "раш", "натиск" }, - { "рашить", "наступать" }, - { "пуш", "вперёд" }, - { "пушить", "прорываться" }, - { "дамаг", "увечье" }, - { "дамажить", "наносить увечья" }, - { "хил", "исцеление" }, - { "хилить", "исцелять" }, - { "подхиль", "подлечи" }, - { "хильни", "исцели" }, - { "куллдаун", "передышка" }, - { "кулдаун", "передышка" }, - { "кд", "передышка" }, - { "скил", "умение" }, - { "скиловый", "умелый" }, - { "лут", "трофеи" }, - { "лутать", "собирать трофеи" }, - { "лутаю", "собираю трофеи" }, - { "лутаешь", "собираешь трофеи" }, - { "лутай", "собирай трофеи" }, - { "рандом", "воля случая" }, - { "рандомно", "по воля случая" }, - { "рандомный", "случайный" }, - - // -- Дополнения к IC -- - { "бро", "брат" }, - { "кек", "смех" }, - { "кекнуть", "посмеяться" }, - { "кекнул", "посмеялся" }, - { "лол", "смешно" }, - { "рофл", "шутка" }, - { "рофлить", "шутить" }, - { "рофлишь", "шутишь" }, - { "рофлит", "шутит" }, - { "го", "идём" }, - { "гоу", "идём" }, - { "сори", "прости" }, - { "сорян", "прости" }, - { "соря", "прости" }, - { "ало", "эй" }, - { "алло", "эй" }, - { "але", "эй" }, - { "алё", "эй" }, - { "алле", "эй" }, - { "аллё", "эй" }, - { "хайп", "шумиха" }, - { "хайпить", "шуметь" }, - { "кайп", "веселье" }, - { "мем", "байка" }, - { "мемный", "забавный" }, - { "треш", "ужас" }, - { "трешовый", "ужасный" }, - { "флекс", "хвастовство" }, - { "флексить", "бахвалиться" }, - { "сигма", "одиночка" }, - { "гигачад", "богатырь" }, - { "рил", "истинно" }, - { "фаст", "проворно" }, - { "фастом", "проворно" }, - { "ливаю", "удаляюсь" }, - { "ливни", "удались" }, - { "хелп", "помощь" }, - { "хелпани", "пособи" }, - { "хелпанул", "подсобил" }, - { "юзать", "использовать" }, - { "юзай", "используй" }, - { "уэ", "эх" }, - { "уээ", "эх" }, - { "уэээ", "эх" }, - { "уээээ", "эх" }, - { "уэээээ", "эх" }, - { "уээээээ", "эх" }, - - // -- Дополнения к OOC -- - { "робаст", "крепыш" }, - { "робастный", "крепкий" }, - { "робастить", "избивать" }, - { "анробаст", "хлипкий" }, - { "анроб", "хлипкий" }, - { "пг", "дурная игра" }, - { "рп", "добрая игра" }, - { "хрп", "добрая игра" }, - { "мрп", "средняя игра" }, - { "лрп", "дурная игра" }, - { "ерп", "взрослые забавы" }, - { "пвп", "бой с мужами" }, - { "пве", "бой с тварями" }, - { "фак", "к черту" }, - { "лаги", "помехи" }, - { "лагаю", "замедляюсь" }, - { "лагает", "замедляется" }, - { "птср", "испытание" }, - { "фрикил", "дурное убийство" }, - { "гриф", "дурное деяние" }, - { "рдм", "дурное убийство" }, - { "дм", "дурное" }, - { "перм", "вечная кара" }, - { "пермач", "вечная кара" }, - { "бан", "кара" }, - { "забанят", "покарают" }, - { "забанили", "покарали" }, - { "запермили", "вечно покарали" }, - { "запермят", "вечно покарают" }, - { "админ", "небожитель" }, - { "админы", "небожители" }, - { "админам", "небожителям" }, - { "админов", "небожителей" }, - { "нонрп", "дурное" }, - { "нрп", "дурное" }, - - // -- Дополнения к Брейнрот, сленг -- - { "донат", "приношение" }, - { "донатить", "приносить дары" }, - { "шиза", "одержимость" }, - { "шизик", "одержимый" }, - { "шизовый", "одержимый" }, - { "скример", "пугало" }, - { "амогус", "оборотень" }, - { "альтуш", "второе лицо" }, - { "трап", "западня" }, - { "квадробер", "оборотень" }, - { "кил", "убийство" }, - { "килать", "убивать" }, - { "килнул", "убил" }, - { "симп", "почитатель" }, - { "симпить", "почитать" }, - { "чиназес", "славно" }, - { "чезабретто", "славно" }, - { "кфс", "жареный птенец" }, - { "ауф", "конец" }, - { "имхо", "по моему разумению" }, - { "плаки", "горестно" }, - { "соевый", "слабый" }, - { "томбой", "пацанка" }, - { "фембой", "женственный" }, - { "веном", "отрава" }, - { "корвакс", "ворон" }, - { "санрайз", "рассвет" }, - { "империал", "имперский" }, - { "угор", "смех" }, - { "угарать", "смеяться" }, - { "фимоз", "узость" }, - { "абоба", "абсурд" }, - { "шаришь", "разумеешь" }, - { "шарю", "разумею" }, - { "манч", "яства" }, - { "вульп", "лис" }, - { "вайб", "настроение" }, - { "тильт", "уныние" }, - { "пикми", "избери меня" }, - { "абаюдна", "согласен" } + { "данж", "подземелье" }, + { "данджа", "подземелья" }, + { "данджу", "подземелью" }, + { "данжем", "подземельем" }, + { "данжи", "подземелья" }, + { "данжей", "подземелий" }, + { "аое", "по площади" }, + { "аоешный", "площадной" }, + { "аоешка", "заклинание по площади" }, + { "соло", "в одиночку" }, + { "солка", "одиночка" }, + { "абуз", "злоупотребление" }, + { "абузить", "злоупотреблять" }, + { "баг", "прореха в мироздании" }, + { "баганутый", "искажённый" }, + { "крафтить", "созидать" }, + { "скрафтил", "создал" }, + { "пати", "отряд" }, + { "фарм", "добыча" }, + { "фармить", "промышлять" }, + { "нафармил", "добыл" }, + { "билд", "уклад" }, + { "билдить", "выстраивать" }, + { "танк", "ратник" }, + { "танчить", "принимать удар" }, + { "имб", "заговорённый" }, + { "дисбаланс", "неравновесие" }, + { "дизбаланс", "неравновесие" }, + { "диссбаланс", "неравновесие" }, + { "мета", "ведущий уклад" }, + { "метовый", "избранный" }, + { "педаль", "педальный" }, + { "агро", "вражье внимание" }, + { "агрить", "привлекать ворога" }, + { "агриться", "гневаться" }, + { "баф", "благословение" }, + { "бафнуть", "благословить" }, + { "гайд", "манускрипт" }, + { "спавн", "материализация" }, + { "спавниться", "материализоваться" }, + { "заспавнился", "материализовался" }, + { "спаун", "материализация" }, + { "спауниться", "материализоваться" }, + { "саппорт", "опора" }, + { "сапорт", "опора" }, + { "саппортить", "поддерживать" }, + { "скип", "пропуск" }, + { "скипать", "пропускать" }, + { "скипнуть", "пропустить" }, + { "нпс", "поселянин" }, + { "стан", "оцепенение" }, + { "станить", "оцепенять" }, + { "застанил", "оценил" }, + { "ульт", "заветная способность" }, + { "ульта", "заветная способность" }, + { "ульту", "заветную способность" }, + { "ультану", "применю заветное" }, + { "ультую", "применяю заветное" }, + { "ультуй", "примени заветное" }, + { "ультанул", "применил заветное" }, + { "аим", "прицел" }, + { "аимить", "целиться" }, + { "ренж", "расстояние" }, + { "кайт", "заманивание" }, + { "кайтить", "завлекать" }, + { "кайтил", "завлекал" }, + { "закайтил", "завлёк" }, + { "деф", "оборона" }, + { "дефнуть", "защитить" }, + { "раш", "натиск" }, + { "рашить", "наступать" }, + { "пушить", "прорываться" }, + { "дамажить", "наносить увечья" }, + { "хилить", "исцелять" }, + { "скиловый", "умелый" }, + { "лут", "трофеи" }, + { "лутать", "собирать трофеи" }, + { "лутаю", "собираю трофеи" }, + { "лутаешь", "собираешь трофеи" }, + { "лутай", "собирай трофеи" }, + { "рандом", "воля случая" }, + { "рандомно", "по воля случая" }, + { "рандомный", "случайный" }, + // -- Дополнения к IC -- + { "бро", "брат" }, + { "кек", "смех" }, + { "кекнуть", "посмеяться" }, + { "кекнул", "посмеялся" }, + { "лол", "смешно" }, + { "рофлить", "шутить" }, + { "рофлит", "шутит" }, + { "го", "идём" }, + { "гоу", "идём" }, + { "сори", "прости" }, + { "сорян", "прости" }, + { "соря", "прости" }, + { "ало", "эй" }, + { "алло", "эй" }, + { "але", "эй" }, + { "алё", "эй" }, + { "алле", "эй" }, + { "аллё", "эй" }, + { "хайп", "шумиха" }, + { "хайпить", "шуметь" }, + { "кайп", "веселье" }, + { "мем", "байка" }, + { "мемный", "забавный" }, + { "треш", "ужас" }, + { "трешовый", "ужасный" }, + { "флекс", "хвастовство" }, + { "флексить", "бахвалиться" }, + { "сигма", "одиночка" }, + { "гигачад", "богатырь" }, + { "ливни", "удались" }, + { "юзать", "использовать" }, + { "юзай", "используй" }, + { "уэ", "эх" }, + { "уээ", "эх" }, + { "уэээ", "эх" }, + { "уээээ", "эх" }, + { "уэээээ", "эх" }, + { "уээээээ", "эх" }, + // -- Дополнения к OOC -- + { "пг", "дурная игра" }, + { "рп", "добрая игра" }, + { "хрп", "добрая игра" }, + { "мрп", "средняя игра" }, + { "лрп", "дурная игра" }, + { "ерп", "взрослые забавы" }, + { "пвп", "бой с мужами" }, + { "пве", "бой с тварями" }, + { "фак", "к черту" }, + { "лаги", "помехи" }, + { "лагаю", "замедляюсь" }, + { "лагает", "замедляется" }, + { "птср", "испытание" }, + { "фрикил", "дурное убийство" }, + { "гриф", "дурное деяние" }, + { "рдм", "дурное убийство" }, + { "дм", "дурное" }, + { "перм", "вечная кара" }, + { "пермач", "вечная кара" }, + { "бан", "кара" }, + { "забанят", "покарают" }, + { "забанили", "покарали" }, + { "запермили", "вечно покарали" }, + { "запермят", "вечно покарают" }, + { "админ", "небожитель" }, + { "админы", "небожители" }, + { "админам", "небожителям" }, + { "админов", "небожителей" }, + { "нонрп", "дурное" }, + { "нрп", "дурное" }, + // -- Дополнения к Брейнрот, сленг -- + { "донат", "приношение" }, + { "донатить", "приносить дары" }, + { "шиза", "одержимость" }, + { "шизик", "одержимый" }, + { "шизовый", "одержимый" }, + { "скример", "пугало" }, + { "амогус", "оборотень" }, + { "альтуш", "второе лицо" }, + { "трап", "западня" }, + { "квадробер", "оборотень" }, + { "кил", "убийство" }, + { "килать", "убивать" }, + { "килнул", "убил" }, + { "симп", "почитатель" }, + { "симпить", "почитать" }, + { "чиназес", "славно" }, + { "чезабретто", "славно" }, + { "кфс", "жареный птенец" }, + { "ауф", "конец" }, + { "имхо", "по моему разумению" }, + { "плаки", "горестно" }, + { "соевый", "слабый" }, + { "томбой", "пацанка" }, + { "фембой", "женственный" }, + { "веном", "отрава" }, + { "корвакс", "ворон" }, + { "санрайз", "рассвет" }, + { "империал", "имперский" }, + { "угор", "смех" }, + { "угарать", "смеяться" }, + { "фимоз", "узость" }, + { "абоба", "абсурд" }, + { "шаришь", "разумеешь" }, + { "шарю", "разумею" }, + { "манч", "яства" }, + { "вульп", "лис" }, + { "вайб", "настроение" }, + { "тильт", "уныние" }, + { "пикми", "избери меня" }, + { "абаюдна", "согласен" }, // Imperial Medieval merge: missing comma after conflict resolve + { "аок", "а, хорошо" }, + { "сикссевен", "я идиот" }, + { "сиксевен", "я идиот" }, + { "сыксевен", "я идиот" }, + { "сиксэвен", "я идиот" }, + { "сиксэвэн", "я идиот" }, + { "сыксэвен", "я идиот" }, + { "сыксэвэн", "я идиот" }, + { "сыксевэн", "я идиот" }, + { "67", "я идиот" }, + { "6767", "я идиот" }, + { "676767", "я идиот" }, + { "покойо", "?" }, + { "пакойо", "?" }, + { "друн", "друг" }, + { "жинка", "жена" }, + { "жынка", "жена" }, + { "шуешь", "шутишь" }, + { "шуеш", "шутишь" }, + { "бурмалда", "ерунда" }, + { "газан", "газон" }, + { "воздухан", "лох" } }; + private static readonly (Regex Pattern, string Replacement)[] SlangPhraseReplace = + [ + (new(@"\bсикс севен\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсыкс севен\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсикс сэвен\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсикс сэвэн\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсикс севэн\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсыкс сэвен\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсыкс сэвэн\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + (new(@"\bсыкс севэн\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "я идиот"), + ]; + private string ReplaceWords(string message) { if (string.IsNullOrEmpty(message)) return message; - return Regex.Replace(message, "\\b(\\w+)\\b", match => + foreach (var (pattern, replacement) in SlangPhraseReplace) + message = pattern.Replace(message, replacement); + + return ReplaceRegex.Replace(message, match => { - bool isUpperCase = match.Value.All(Char.IsUpper); + var isUpperCase = match.Value.All(Char.IsUpper); if (SlangReplace.TryGetValue(match.Value.ToLower(), out var replacement)) return isUpperCase ? replacement.ToUpper() : replacement; + return match.Value; }); } diff --git a/Content.Server/Corvax/StationGoal/StationGoalPaperSystem.cs b/Content.Server/Corvax/StationGoal/StationGoalPaperSystem.cs index f85f9305dce..7e4febd520e 100644 --- a/Content.Server/Corvax/StationGoal/StationGoalPaperSystem.cs +++ b/Content.Server/Corvax/StationGoal/StationGoalPaperSystem.cs @@ -43,14 +43,15 @@ public bool SendRandomGoal() //True if at least one fax received paper public bool SendStationGoal(StationGoalPrototype goal) { - var faxes = EntityManager.EntityQuery(); + var enumerator = EntityQueryEnumerator(); var wasSent = false; var funny = new StampDisplayInfo() { StampedName = Loc.GetString("stamp-component-stamped-name-centcom"), StampedColor = Color.FromHex("#006600") }; var list = new List(); list.Add(funny); - foreach (var fax in faxes) + + while (enumerator.MoveNext(out var fax, out var faxComponent)) { - if (!fax.ReceiveStationGoal) continue; + if (!faxComponent.ReceiveStationGoal) continue; var printout = new FaxPrintout( Loc.GetString(goal.Text), @@ -60,7 +61,7 @@ public bool SendStationGoal(StationGoalPrototype goal) "paper_stamp-centcom", list ); - _faxSystem.Receive(fax.Owner, printout, null, fax); + _faxSystem.Receive(fax, printout, null, faxComponent); wasSent = true; } diff --git a/Content.Server/Crayon/CrayonComponent.cs b/Content.Server/Crayon/CrayonComponent.cs deleted file mode 100644 index df20681938d..00000000000 --- a/Content.Server/Crayon/CrayonComponent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Server.UserInterface; -using Content.Shared.Crayon; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; - -namespace Content.Server.Crayon -{ - [RegisterComponent] - public sealed partial class CrayonComponent : SharedCrayonComponent - { - [DataField("useSound")] public SoundSpecifier? UseSound; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("selectableColor")] - public bool SelectableColor { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public int Charges { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("capacity")] - public int Capacity { get; set; } = 30; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("deleteEmpty")] - public bool DeleteEmpty = true; - } -} diff --git a/Content.Server/Crayon/CrayonSystem.cs b/Content.Server/Crayon/CrayonSystem.cs index f3abc2bf7a8..e6f96c5abe6 100644 --- a/Content.Server/Crayon/CrayonSystem.cs +++ b/Content.Server/Crayon/CrayonSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Administration.Logs; using Content.Server.Decals; using Content.Server.Popups; +using Content.Shared.Charges.Systems; using Content.Shared.Crayon; using Content.Shared.Database; using Content.Shared.Decals; @@ -12,7 +13,6 @@ using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.GameStates; using Robust.Shared.Prototypes; namespace Content.Server.Crayon; @@ -24,31 +24,36 @@ public sealed class CrayonSystem : SharedCrayonSystem [Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedChargesSystem _charges = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCrayonInit); + + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCrayonBoundUI); SubscribeLocalEvent(OnCrayonBoundUIColor); - SubscribeLocalEvent(OnCrayonUse, before: new[] { typeof(FoodSystem) }); - SubscribeLocalEvent(OnCrayonAfterInteract, after: new[] { typeof(FoodSystem) }); + SubscribeLocalEvent(OnCrayonUse); + SubscribeLocalEvent(OnCrayonAfterInteract, after: [typeof(IngestionSystem)]); SubscribeLocalEvent(OnCrayonDropped); - SubscribeLocalEvent(OnCrayonGetState); } - private static void OnCrayonGetState(EntityUid uid, CrayonComponent component, ref ComponentGetState args) + private void OnMapInit(Entity ent, ref MapInitEvent args) { - args.State = new CrayonComponentState(component.Color, component.SelectedState, component.Charges, component.Capacity); + // Get the first one from the catalog and set it as default + var decal = _prototypeManager.EnumeratePrototypes().FirstOrDefault(x => x.Tags.Contains("crayon")); + ent.Comp.SelectedState = decal?.ID ?? string.Empty; + Dirty(ent); } + // Runs after IngestionSystem so it doesn't bulldoze force-feeding private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args) { if (args.Handled || !args.CanReach) return; - if (component.Charges <= 0) + if (_charges.IsEmpty(uid)) { if (component.DeleteEmpty) UseUpCrayon(uid, args.User); @@ -72,17 +77,15 @@ private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, Aft if (component.UseSound != null) _audio.PlayPvs(component.UseSound, uid, AudioParams.Default.WithVariation(0.125f)); - // Decrease "Ammo" - component.Charges--; - Dirty(uid, component); + _charges.TryUseCharge(uid); _adminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{ToPrettyString(args.User):user} drew a {component.Color:color} {component.SelectedState}"); args.Handled = true; - if (component.DeleteEmpty && component.Charges <= 0) + if (component.DeleteEmpty && _charges.IsEmpty(uid)) UseUpCrayon(uid, args.User); else - _uiSystem.ServerSendUiMessage(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState)); + _uiSystem.ServerSendUiMessage(uid, CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState)); } private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args) @@ -91,14 +94,12 @@ private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEven if (args.Handled) return; - if (!_uiSystem.HasUi(uid, SharedCrayonComponent.CrayonUiKey.Key)) - { + if (!_uiSystem.HasUi(uid, CrayonUiKey.Key)) return; - } - _uiSystem.TryToggleUi(uid, SharedCrayonComponent.CrayonUiKey.Key, args.User); + _uiSystem.TryToggleUi(uid, CrayonUiKey.Key, args.User); - _uiSystem.SetUiState(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color)); + _uiSystem.SetUiState(uid, CrayonUiKey.Key, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color)); args.Handled = true; } @@ -109,35 +110,23 @@ private void OnCrayonBoundUI(EntityUid uid, CrayonComponent component, CrayonSel return; component.SelectedState = args.State; - Dirty(uid, component); } private void OnCrayonBoundUIColor(EntityUid uid, CrayonComponent component, CrayonColorMessage args) { - // you still need to ensure that the given color is a valid color + // Ensure that the given color can be changed or already matches if (!component.SelectableColor || args.Color == component.Color) return; component.Color = args.Color; Dirty(uid, component); - - } - - private void OnCrayonInit(EntityUid uid, CrayonComponent component, ComponentInit args) - { - component.Charges = component.Capacity; - - // Get the first one from the catalog and set it as default - var decal = _prototypeManager.EnumeratePrototypes().FirstOrDefault(x => x.Tags.Contains("crayon")); - component.SelectedState = decal?.ID ?? string.Empty; - Dirty(uid, component); } private void OnCrayonDropped(EntityUid uid, CrayonComponent component, DroppedEvent args) { // TODO: Use the existing event. - _uiSystem.CloseUi(uid, SharedCrayonComponent.CrayonUiKey.Key, args.User); + _uiSystem.CloseUi(uid, CrayonUiKey.Key, args.User); } private void UseUpCrayon(EntityUid uid, EntityUid user) diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 1b7e50c6515..09415d02e3a 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -14,6 +14,8 @@ using Content.Shared.IdentityManagement; using Content.Shared.Security.Components; using System.Linq; +using Content.Shared.Administration.Logs; +using Content.Shared.Database; using Content.Shared.Roles.Jobs; namespace Content.Server.CriminalRecords.Systems; @@ -24,6 +26,7 @@ namespace Content.Server.CriminalRecords.Systems; public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem { [Dependency] private readonly AccessReaderSystem _access = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly CriminalRecordsSystem _criminalRecords = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly RadioSystem _radio = default!; @@ -87,7 +90,8 @@ private void OnChangeStatus(Entity ent, ref Cri { // prevent malf client violating wanted/reason nullability if (msg.Status == SecurityStatus.Wanted != (msg.Reason != null) && - msg.Status == SecurityStatus.Suspected != (msg.Reason != null)) + msg.Status == SecurityStatus.Suspected != (msg.Reason != null) && + msg.Status == SecurityStatus.Hostile != (msg.Reason != null)) return; if (!CheckSelected(ent, msg.Actor, out var mob, out var key)) @@ -144,6 +148,8 @@ private void OnChangeStatus(Entity ent, ref Cri // figure out which radio message to send depending on transition var statusString = (oldStatus, msg.Status) switch { + (_, SecurityStatus.Hostile) => "hostile", + (_, SecurityStatus.Eliminated) => "eliminated", // person has been detained (_, SecurityStatus.Detained) => "detained", // person did something sus @@ -154,6 +160,8 @@ private void OnChangeStatus(Entity ent, ref Cri (_, SecurityStatus.Discharged) => "released", // going from any other state to wanted, AOS or prisonbreak / lazy secoff never set them to released and they reoffended (_, SecurityStatus.Wanted) => "wanted", + (SecurityStatus.Hostile, SecurityStatus.None) => "not-hostile", + (SecurityStatus.Eliminated, SecurityStatus.None) => "not-eliminated", // person is no longer sus (SecurityStatus.Suspected, SecurityStatus.None) => "not-suspected", // going from wanted to none, must have been a mistake @@ -165,8 +173,12 @@ private void OnChangeStatus(Entity ent, ref Cri // this is impossible _ => "not-wanted" }; - _radio.SendRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args), - ent.Comp.SecurityChannel, ent); + _radio.SendRadioMessage(ent, + Loc.GetString($"criminal-records-console-{statusString}", args), + ent.Comp.SecurityChannel, + ent); + + _adminLogger.Add(LogType.Identity, LogImpact.Low, $"{ToPrettyString(mob.Value):name} changed criminal status for {name} to \"{statusString}\""); UpdateUserInterface(ent); } diff --git a/Content.Server/Cuffs/CuffableSystem.cs b/Content.Server/Cuffs/CuffableSystem.cs index 2c28603c3f4..622eabd9531 100644 --- a/Content.Server/Cuffs/CuffableSystem.cs +++ b/Content.Server/Cuffs/CuffableSystem.cs @@ -15,7 +15,7 @@ public override void Initialize() SubscribeLocalEvent(OnCuffableGetState); } - private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args) + private void OnCuffableGetState(Entity entity, ref ComponentGetState args) { // there are 2 approaches i can think of to handle the handcuff overlay on players // 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same. @@ -23,12 +23,12 @@ private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref // approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it. // right now we're doing approach #1. HandcuffComponent? cuffs = null; - if (component.CuffedHandCount > 0) - TryComp(component.LastAddedCuffs, out cuffs); - args.State = new CuffableComponentState(component.CuffedHandCount, - component.CanStillInteract, + if (TryGetLastCuff((entity, entity.Comp), out var cuff)) + TryComp(cuff, out cuffs); + args.State = new CuffableComponentState(entity.Comp.CuffedHandCount, + entity.Comp.CanStillInteract, cuffs?.CuffedRSI, - $"{cuffs?.BodyIconState}-{component.CuffedHandCount}", + $"{cuffs?.BodyIconState}-{entity.Comp.CuffedHandCount}", cuffs?.Color); // the iconstate is formatted as blah-2, blah-4, blah-6, etc. // the number corresponds to how many hands are cuffed. diff --git a/Content.Server/Damage/Commands/HurtCommand.cs b/Content.Server/Damage/Commands/HurtCommand.cs index af9c1ee8fc3..849f6e543e4 100644 --- a/Content.Server/Damage/Commands/HurtCommand.cs +++ b/Content.Server/Damage/Commands/HurtCommand.cs @@ -4,7 +4,7 @@ using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; +using Content.Shared.Damage.Systems; using Robust.Shared.Console; using Robust.Shared.Prototypes; diff --git a/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs b/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs deleted file mode 100644 index 4ff0057e185..00000000000 --- a/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Damage.Systems; -using Content.Server.Imperial.Medieval.SmithingSystem; -using Content.Shared.Damage; - -namespace Content.Server.Damage.Components -{ - [Access(typeof(DamageOtherOnHitSystem), typeof(SmithingSystem))] // Imperial medieval smithing - [RegisterComponent] - public sealed partial class DamageOtherOnHitComponent : Component - { - [DataField("ignoreResistances")] - [ViewVariables(VVAccess.ReadWrite)] - public bool IgnoreResistances = false; - - [DataField("damage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier Damage = default!; - - } -} diff --git a/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs index c229ebcbf8d..270a8f55c00 100644 --- a/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs +++ b/Content.Server/Damage/ForceSay/DamageForceSaySystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Damage; using Content.Shared.Damage.Events; using Content.Shared.Damage.ForceSay; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; diff --git a/Content.Server/Damage/Systems/DamageOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOnHitSystem.cs index f129a14f799..4b13106cc8c 100644 --- a/Content.Server/Damage/Systems/DamageOnHitSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnHitSystem.cs @@ -1,8 +1,7 @@ using Content.Server.Damage.Components; -using Content.Shared.Damage; -using Robust.Shared.Player; using Content.Shared.Weapons.Melee.Events; using System.Linq; +using Content.Shared.Damage.Systems; namespace Content.Server.Damage.Systems; diff --git a/Content.Server/Damage/Systems/DamageOnLandSystem.cs b/Content.Server/Damage/Systems/DamageOnLandSystem.cs index 3cf103e6eee..8dede48deeb 100644 --- a/Content.Server/Damage/Systems/DamageOnLandSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnLandSystem.cs @@ -9,7 +9,7 @@ namespace Content.Server.Damage.Systems /// public sealed class DamageOnLandSystem : EntitySystem { - [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageableSystem = default!; public override void Initialize() { diff --git a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs index 8c0e0a13825..88fe02510fc 100644 --- a/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnToolInteractSystem.cs @@ -11,7 +11,7 @@ namespace Content.Server.Damage.Systems { public sealed class DamageOnToolInteractSystem : EntitySystem { - [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageableSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedToolSystem _toolSystem = default!; @@ -35,22 +35,22 @@ private void OnInteracted(EntityUid uid, DamageOnToolInteractComponent component && itemToggle.Activated && !welder.TankSafe) { - var dmg = _damageableSystem.TryChangeDamage(args.Target, weldingDamage, origin: args.User); - - if (dmg != null) + if (_damageableSystem.TryChangeDamage(args.Target, weldingDamage, out var dmg, origin: args.User)) + { _adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} used {ToPrettyString(args.Used):used} as a welder to deal {dmg.GetTotal():damage} damage to {ToPrettyString(args.Target):target}"); + } args.Handled = true; } else if (component.DefaultDamage is {} damage && _toolSystem.HasQuality(args.Used, component.Tools)) { - var dmg = _damageableSystem.TryChangeDamage(args.Target, damage, origin: args.User); - - if (dmg != null) + if (_damageableSystem.TryChangeDamage(args.Target, damage, out var dmg, origin: args.User)) + { _adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} used {ToPrettyString(args.Used):used} as a tool to deal {dmg.GetTotal():damage} damage to {ToPrettyString(args.Target):target}"); + } args.Handled = true; } diff --git a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs index b1f5365b016..02bc7334a6c 100644 --- a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs +++ b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs @@ -1,72 +1,54 @@ using Content.Server.Administration.Logs; -using Content.Server.Damage.Components; using Content.Server.Weapons.Ranged.Systems; -using Content.Shared.CombatMode.Pacification; using Content.Shared.Camera; using Content.Shared.Damage; -using Content.Shared.Damage.Events; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Effects; using Content.Shared.Mobs.Components; using Content.Shared.Throwing; -using Content.Shared.Wires; using Robust.Shared.Physics.Components; using Robust.Shared.Player; -namespace Content.Server.Damage.Systems -{ - public sealed class DamageOtherOnHitSystem : EntitySystem - { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly GunSystem _guns = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly DamageExamineSystem _damageExamine = default!; - [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!; - [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; +namespace Content.Server.Damage.Systems; - public override void Initialize() - { - SubscribeLocalEvent(OnDoHit); - SubscribeLocalEvent(OnDamageExamine); - SubscribeLocalEvent(OnAttemptPacifiedThrow); - } +public sealed class DamageOtherOnHitSystem : SharedDamageOtherOnHitSystem +{ + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly GunSystem _guns = default!; + [Dependency] private readonly Shared.Damage.Systems.DamageableSystem _damageable = default!; + [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!; + [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; - private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args) - { - if (TerminatingOrDeleted(args.Target)) - return; + public override void Initialize() + { + base.Initialize(); - var dmg = _damageable.TryChangeDamage(args.Target, component.Damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower); + SubscribeLocalEvent(OnDoHit); + } - // Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying. - if (dmg != null && HasComp(args.Target)) - _adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {dmg.GetTotal():damage} damage from collision"); + private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args) + { + if (TerminatingOrDeleted(args.Target)) + return; - if (dmg is { Empty: false }) - { - _color.RaiseEffect(Color.Red, new List() { args.Target }, Filter.Pvs(args.Target, entityManager: EntityManager)); - } + var dmg = _damageable.ChangeDamage(args.Target, component.Damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower); - _guns.PlayImpactSound(args.Target, dmg, null, false); - if (TryComp(uid, out var body) && body.LinearVelocity.LengthSquared() > 0f) - { - var direction = body.LinearVelocity.Normalized(); - _sharedCameraRecoil.KickCamera(args.Target, direction); - } - } + // Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying. + if (HasComp(args.Target)) + _adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {dmg.GetTotal():damage} damage from collision"); - private void OnDamageExamine(EntityUid uid, DamageOtherOnHitComponent component, ref DamageExamineEvent args) + if (!dmg.Empty) { - _damageExamine.AddDamageExamine(args.Message, _damageable.ApplyUniversalAllModifiers(component.Damage * _damageable.UniversalThrownDamageModifier), Loc.GetString("damage-throw")); + _color.RaiseEffect(Color.Red, [args.Target], Filter.Pvs(args.Target, entityManager: EntityManager)); } - /// - /// Prevent players with the Pacified status effect from throwing things that deal damage. - /// - private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) + _guns.PlayImpactSound(args.Target, dmg, null, false); + if (TryComp(uid, out var body) && body.LinearVelocity.LengthSquared() > 0f) { - args.Cancel("pacified-cannot-throw"); + var direction = body.LinearVelocity.Normalized(); + _sharedCameraRecoil.KickCamera(args.Target, direction); } } } diff --git a/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs b/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs index 8bdbf84147a..25e7bc1644a 100644 --- a/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs +++ b/Content.Server/Damage/Systems/DamageRandomPopupSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Damage.Components; using Content.Server.Popups; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Robust.Shared.Player; using Robust.Shared.Random; diff --git a/Content.Server/Damage/Systems/ExaminableDamageSystem.cs b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs index b0dfae71b7f..e2b686dd315 100644 --- a/Content.Server/Damage/Systems/ExaminableDamageSystem.cs +++ b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Damage.Components; using Content.Server.Destructible; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Rounding; using Robust.Shared.Prototypes; @@ -11,6 +12,7 @@ public sealed class ExaminableDamageSystem : EntitySystem { [Dependency] private readonly DestructibleSystem _destructible = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public override void Initialize() { @@ -39,7 +41,7 @@ private float GetDamagePercent(Entity ent) if (!TryComp(ent, out var damageable)) return 0; - var damage = damageable.TotalDamage; + var damage = _damageable.GetTotalDamage((ent, damageable)); var damageThreshold = _destructible.DestroyedAt(ent); if (damageThreshold == 0) diff --git a/Content.Server/Database/BanDef.cs b/Content.Server/Database/BanDef.cs new file mode 100644 index 00000000000..d459b475793 --- /dev/null +++ b/Content.Server/Database/BanDef.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Robust.Shared.Configuration; +using Robust.Shared.Network; + + +namespace Content.Server.Database +{ + public sealed class BanDef + { + public int? Id { get; } + public BanType Type { get; } + public ImmutableArray UserIds { get; } + public ImmutableArray<(IPAddress address, int cidrMask)> Addresses { get; } + public ImmutableArray HWIds { get; } + + public DateTimeOffset BanTime { get; } + public DateTimeOffset? ExpirationTime { get; } + public ImmutableArray RoundIds { get; } + public TimeSpan PlaytimeAtNote { get; } + public string Reason { get; } + public NoteSeverity Severity { get; set; } + public NetUserId? BanningAdmin { get; } + public UnbanDef? Unban { get; } + public ServerBanExemptFlags ExemptFlags { get; } + + public ImmutableArray? Roles { get; } + + public BanDef( + int? id, + BanType type, + ImmutableArray userIds, + ImmutableArray<(IPAddress address, int cidrMask)> addresses, + ImmutableArray hwIds, + DateTimeOffset banTime, + DateTimeOffset? expirationTime, + ImmutableArray roundIds, + TimeSpan playtimeAtNote, + string reason, + NoteSeverity severity, + NetUserId? banningAdmin, + UnbanDef? unban, + ServerBanExemptFlags exemptFlags = default, + ImmutableArray? roles = null) + { + if (userIds.Length == 0 && addresses.Length == 0 && hwIds.Length == 0) + { + throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); + } + + addresses = addresses.Select(address => + { + if (address is { address.IsIPv4MappedToIPv6: true } addr) + { + // Fix IPv6-mapped IPv4 addresses + // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. + address = (addr.address.MapToIPv4(), addr.cidrMask - 96); + } + + return address; + }) + .ToImmutableArray(); + + Id = id; + Type = type; + UserIds = userIds; + Addresses = addresses; + HWIds = hwIds; + BanTime = banTime; + ExpirationTime = expirationTime; + RoundIds = roundIds; + PlaytimeAtNote = playtimeAtNote; + Reason = reason; + Severity = severity; + BanningAdmin = banningAdmin; + Unban = unban; + ExemptFlags = exemptFlags; + + switch (Type) + { + case BanType.Server: + if (roles != null) + throw new ArgumentException("Cannot specify roles for server ban types", nameof(roles)); + break; + + case BanType.Role: + if (roles is not { Length: > 0 }) + throw new ArgumentException("Must specify roles for server ban types", nameof(roles)); + if (exemptFlags != 0) + throw new ArgumentException("Role bans cannot have exempt flags", nameof(exemptFlags)); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + + Roles = roles; + } + + public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) + { + string expires; + if (ExpirationTime is { } expireTime) + { + var duration = expireTime - BanTime; + var utc = expireTime.ToUniversalTime(); + expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f"))); + } + else + { + var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal); + expires = !string.IsNullOrWhiteSpace(appeal) + ? loc.GetString("ban-banned-permanent-appeal", ("link", appeal)) + : loc.GetString("ban-banned-permanent"); + } + + return $""" + {loc.GetString("ban-banned-1")} + {loc.GetString("ban-banned-2", ("reason", Reason))} + {expires} + {loc.GetString("ban-banned-3")} + """; + } + } +} diff --git a/Content.Server/Database/BanMatcher.cs b/Content.Server/Database/BanMatcher.cs index f477ccd822a..0302c0dc13a 100644 --- a/Content.Server/Database/BanMatcher.cs +++ b/Content.Server/Database/BanMatcher.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Linq; using System.Net; using Content.Server.IP; using Content.Shared.Database; @@ -7,7 +8,7 @@ namespace Content.Server.Database; /// -/// Implements logic to match a against a player query. +/// Implements logic to match a against a player query. /// /// /// @@ -29,7 +30,7 @@ public static class BanMatcher /// The ban information. /// Information about the player to match against. /// True if the ban matches the provided player info. - public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) + public static bool BanMatches(BanDef ban, in PlayerInfo player) { var exemptFlags = player.ExemptFlags; // Any flag to bypass BlacklistedRange bans. @@ -39,39 +40,44 @@ public static bool BanMatches(ServerBanDef ban, in PlayerInfo player) if ((ban.ExemptFlags & exemptFlags) != 0) return false; + var playerAddr = player.Address; if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP) - && player.Address != null - && ban.Address is not null - && player.Address.IsInSubnet(ban.Address.Value) + && playerAddr != null + && ban.Addresses.Any(addr => playerAddr.IsInSubnet(addr)) && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer)) { return true; } - if (player.UserId is { } id && ban.UserId == id.UserId) + if (player.UserId is { } id && ban.UserIds.Contains(id)) { return true; } - switch (ban.HWId?.Type) + foreach (var banHwid in ban.HWIds) { - case HwidType.Legacy: - if (player.HWId is { Length: > 0 } hwIdVar - && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - { - return true; - } - break; - case HwidType.Modern: - if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) - { - foreach (var hwid in modernHwIdVar) + switch (banHwid.Type) + { + case HwidType.Legacy: + if (player.HWId is { Length: > 0 } hwIdVar + && hwIdVar.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) { - if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan())) - return true; + return true; } - } - break; + + break; + case HwidType.Modern: + if (player.ModernHWIds is { Length: > 0 } modernHwIdVar) + { + foreach (var hwid in modernHwIdVar) + { + if (hwid.AsSpan().SequenceEqual(banHwid.Hwid.AsSpan())) + return true; + } + } + + break; + } } return false; diff --git a/Content.Server/Database/DataNodeJsonExtensions.cs b/Content.Server/Database/DataNodeJsonExtensions.cs new file mode 100644 index 00000000000..975c8c50607 --- /dev/null +++ b/Content.Server/Database/DataNodeJsonExtensions.cs @@ -0,0 +1,61 @@ +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Value; +using YamlDotNet.RepresentationModel; + +namespace Content.Server.Database; + +public static class DataNodeJsonExtensions +{ + private static JsonNode ToJsonNode(this MappingDataNode node) + { + return new JsonObject(node.Children.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToJsonNode()))); + } + + private static JsonNode ToJsonNode(this SequenceDataNode node) + { + return new JsonArray(node.Select(ToJsonNode).ToArray()); + } + + public static JsonNode? ToJsonNode(this DataNode node) + { + return node switch + { + ValueDataNode valueDataNode => JsonValue.Create(valueDataNode.IsNull ? null : valueDataNode.Value), + MappingDataNode mappingDataNode => mappingDataNode.ToJsonNode(), + SequenceDataNode sequenceNode => sequenceNode.ToJsonNode(), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } + + public static DataNode ToDataNode(this JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Object => new MappingDataNode(element.EnumerateObject().ToDictionary(kvp => kvp.Name, kvp => kvp.Value.ToDataNode())), + JsonValueKind.Array => new SequenceDataNode(element.EnumerateArray().Select(item => item.ToDataNode()).ToList()), + JsonValueKind.Number => new ValueDataNode(element.GetRawText()), + JsonValueKind.String => new ValueDataNode(element.GetString()), + JsonValueKind.True => new ValueDataNode("true"), + JsonValueKind.False => new ValueDataNode("false"), + JsonValueKind.Null => new ValueDataNode("null"), + _ => throw new ArgumentOutOfRangeException(nameof(element)), + }; + } + + public static DataNode ToDataNode(this JsonNode? node) + { + return node switch + { + null => ValueDataNode.Null(), + JsonValue value => new ValueDataNode(value.GetValue()), + JsonArray array => new SequenceDataNode(array.Select(item => item.ToDataNode()).ToList()), + JsonObject obj => new MappingDataNode(obj.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToDataNode())), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } +} diff --git a/Content.Server/Database/DatabaseRecords.cs b/Content.Server/Database/DatabaseRecords.cs index 30fba3434b8..63ab45a726b 100644 --- a/Content.Server/Database/DatabaseRecords.cs +++ b/Content.Server/Database/DatabaseRecords.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net; using Content.Shared.Database; using Robust.Shared.Network; @@ -12,9 +13,9 @@ public interface IAdminRemarksRecord { public int Id { get; } - public RoundRecord? Round { get; } + public ImmutableArray Rounds { get; } - public PlayerRecord? Player { get; } + public ImmutableArray Players { get; } public TimeSpan PlaytimeAtNote { get; } public string Message { get; } @@ -31,10 +32,11 @@ public interface IAdminRemarksRecord public bool Deleted { get; } } -public sealed record ServerRoleBanNoteRecord( +public sealed record BanNoteRecord( int Id, - RoundRecord? Round, - PlayerRecord? Player, + BanType Type, + ImmutableArray Rounds, + ImmutableArray Players, TimeSpan PlaytimeAtNote, string Message, NoteSeverity Severity, @@ -44,25 +46,9 @@ public sealed record ServerRoleBanNoteRecord( DateTimeOffset? LastEditedAt, DateTimeOffset? ExpirationTime, bool Deleted, - string[] Roles, PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; - -public sealed record ServerBanNoteRecord( - int Id, - RoundRecord? Round, - PlayerRecord? Player, - TimeSpan PlaytimeAtNote, - string Message, - NoteSeverity Severity, - PlayerRecord? CreatedBy, - DateTimeOffset CreatedAt, - PlayerRecord? LastEditedBy, - DateTimeOffset? LastEditedAt, - DateTimeOffset? ExpirationTime, - bool Deleted, - PlayerRecord? UnbanningAdmin, - DateTime? UnbanTime) : IAdminRemarksRecord; + DateTime? UnbanTime, + ImmutableArray Roles) : IAdminRemarksRecord; public sealed record AdminNoteRecord( int Id, @@ -79,7 +65,11 @@ public sealed record AdminNoteRecord( bool Deleted, PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, - bool Secret) : IAdminRemarksRecord; + bool Secret) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminWatchlistRecord( int Id, @@ -94,7 +84,11 @@ public sealed record AdminWatchlistRecord( DateTimeOffset? ExpirationTime, bool Deleted, PlayerRecord? DeletedBy, - DateTimeOffset? DeletedAt) : IAdminRemarksRecord; + DateTimeOffset? DeletedAt) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record AdminMessageRecord( int Id, @@ -111,15 +105,18 @@ public sealed record AdminMessageRecord( PlayerRecord? DeletedBy, DateTimeOffset? DeletedAt, bool Seen, - bool Dismissed) : IAdminRemarksRecord; - + bool Dismissed) : IAdminRemarksRecord +{ + ImmutableArray IAdminRemarksRecord.Rounds => Round != null ? [Round] : []; + ImmutableArray IAdminRemarksRecord.Players => Player != null ? [Player] : []; +} public sealed record PlayerRecord( NetUserId UserId, DateTimeOffset FirstSeenTime, string LastSeenUserName, DateTimeOffset LastSeenTime, - IPAddress LastSeenAddress, + IPAddress? LastSeenAddress, ImmutableTypedHwid? HWId); public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server); diff --git a/Content.Server/Database/EFCoreExtensions.cs b/Content.Server/Database/EFCoreExtensions.cs new file mode 100644 index 00000000000..58dbc4639b2 --- /dev/null +++ b/Content.Server/Database/EFCoreExtensions.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace Content.Server.Database; + +internal static class EFCoreExtensions +{ + extension(IQueryable query) where TEntity : class + { + public IQueryable ApplyIncludes( + IEnumerable>> properties) + { + var q = query; + foreach (var property in properties) + { + q = q.Include(property); + } + + return q; + } + + public IQueryable ApplyIncludes( + IEnumerable>> properties, + Expression> getDerived) + where TDerived : class + { + var q = query; + foreach (var property in properties) + { + q = q.Include(getDerived).ThenInclude(property); + } + + return q; + } + } +} diff --git a/Content.Server/Database/ServerBanDef.cs b/Content.Server/Database/ServerBanDef.cs deleted file mode 100644 index a09f9e959c6..00000000000 --- a/Content.Server/Database/ServerBanDef.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Net; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Robust.Shared.Configuration; -using Robust.Shared.Network; - - -namespace Content.Server.Database -{ - public sealed class ServerBanDef - { - public int? Id { get; } - public NetUserId? UserId { get; } - public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableTypedHwid? HWId { get; } - - public DateTimeOffset BanTime { get; } - public DateTimeOffset? ExpirationTime { get; } - public int? RoundId { get; } - public TimeSpan PlaytimeAtNote { get; } - public string Reason { get; } - public NoteSeverity Severity { get; set; } - public NetUserId? BanningAdmin { get; } - public ServerUnbanDef? Unban { get; } - public ServerBanExemptFlags ExemptFlags { get; } - - public ServerBanDef(int? id, - NetUserId? userId, - (IPAddress, int)? address, - TypedHwid? hwId, - DateTimeOffset banTime, - DateTimeOffset? expirationTime, - int? roundId, - TimeSpan playtimeAtNote, - string reason, - NoteSeverity severity, - NetUserId? banningAdmin, - ServerUnbanDef? unban, - ServerBanExemptFlags exemptFlags = default) - { - if (userId == null && address == null && hwId == null) - { - throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); - } - - if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6) - { - // Fix IPv6-mapped IPv4 addresses - // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - address = (addr.Item1.MapToIPv4(), addr.Item2 - 96); - } - - Id = id; - UserId = userId; - Address = address; - HWId = hwId; - BanTime = banTime; - ExpirationTime = expirationTime; - RoundId = roundId; - PlaytimeAtNote = playtimeAtNote; - Reason = reason; - Severity = severity; - BanningAdmin = banningAdmin; - Unban = unban; - ExemptFlags = exemptFlags; - } - - public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc) - { - string expires; - if (ExpirationTime is { } expireTime) - { - var duration = expireTime - BanTime; - var utc = expireTime.ToUniversalTime(); - expires = loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f"))); - } - else - { - var appeal = cfg.GetCVar(CCVars.InfoLinksAppeal); - expires = !string.IsNullOrWhiteSpace(appeal) - ? loc.GetString("ban-banned-permanent-appeal", ("link", appeal)) - : loc.GetString("ban-banned-permanent"); - } - - return $""" - {loc.GetString("ban-banned-1")} - {loc.GetString("ban-banned-2", ("reason", Reason))} - {expires} - {loc.GetString("ban-banned-3")} - """; - } - } -} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 63cd951cabf..7a9ad5bafab 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; using System.Net; using System.Runtime.CompilerServices; using System.Security.Cryptography; @@ -8,13 +9,14 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Logs; -using Content.Server.Administration.Managers; using Content.Shared.Administration.Logs; using Content.Shared.Imperial.Medieval.Language; using Content.Shared.Construction.Prototypes; using Content.Shared.Database; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Body; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; @@ -23,6 +25,7 @@ using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; using Content.Shared.Imperial.Medieval.PlayerCreations; using Content.Shared.Imperial.Medieval.Skills; @@ -32,23 +35,25 @@ namespace Content.Server.Database public abstract class ServerDbBase { private readonly ISawmill _opsLog; + private readonly ISerializationManager _serialization; // Imperial Medieval / public organ markings public event Action? OnNotificationReceived; public Dictionary Logs = new(); // Imperial Medieval Temporary Logs /// Sawmill to trace log database operations to. - public ServerDbBase(ISawmill opsLog) + public ServerDbBase(ISawmill opsLog, ISerializationManager serialization) { + _serialization = serialization; _opsLog = opsLog; } #region Preferences - public async Task GetPlayerPreferencesAsync( + public async Task GetPlayerPreferencesAsync( NetUserId userId, CancellationToken cancel = default) { await using var db = await GetDb(cancel); - var prefs = await db.DbContext + return await db.DbContext .Preference .Include(p => p.Profiles).ThenInclude(h => h.Jobs) .Include(p => p.Profiles).ThenInclude(h => h.Antags) @@ -60,50 +65,52 @@ public ServerDbBase(ISawmill opsLog) .ThenInclude(group => group.Loadouts) .AsSplitQuery() .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + } - if (prefs is null) - return null; - - var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1; - var profiles = new Dictionary(maxSlot); - foreach (var profile in prefs.Profiles) - { - profiles[profile.Slot] = ConvertProfiles(profile); - } + public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) + { + await using var db = await GetDb(); - var constructionFavorites = new List>(prefs.ConstructionFavorites.Count); - foreach (var favorite in prefs.ConstructionFavorites) - constructionFavorites.Add(new ProtoId(favorite)); + await SetSelectedCharacterSlotAsync(userId, index, db.DbContext); - return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor), constructionFavorites); + await db.DbContext.SaveChangesAsync(); } - public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) + /// + /// Only intended for use in unit tests - drops the organ marking data from a profile in the given slot + /// + /// The user whose profile to modify + /// The slot index to modify + public async Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot) { await using var db = await GetDb(); - await SetSelectedCharacterSlotAsync(userId, index, db.DbContext); + var oldProfile = await db.DbContext.Profile + .Include(p => p.Preference) + .Where(p => p.Preference.UserId == userId.UserId) + .AsSplitQuery() + .SingleOrDefaultAsync(h => h.Slot == slot); + + if (oldProfile == null) + return; + + oldProfile.OrganMarkings = null; + oldProfile.Markings = JsonSerializer.SerializeToDocument(new List()); await db.DbContext.SaveChangesAsync(); } - public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot) + public async Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? humanoid, int slot) { await using var db = await GetDb(); - if (profile is null) + if (humanoid is null) { await DeleteCharacterSlot(db.DbContext, userId, slot); await db.DbContext.SaveChangesAsync(); return; } - if (profile is not HumanoidCharacterProfile humanoid) - { - // TODO: Handle other ICharacterProfile implementations properly - throw new NotImplementedException(); - } - var oldProfile = db.DbContext.Profile .Include(p => p.Preference) .Where(p => p.Preference.UserId == userId.UserId) @@ -145,7 +152,7 @@ private static async Task DeleteCharacterSlot(ServerDbContext db, NetUserId user db.Profile.Remove(profile); } - public async Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile) + public async Task InitPrefsAsync(NetUserId userId, HumanoidCharacterProfile defaultProfile) { await using var db = await GetDb(); @@ -164,7 +171,7 @@ public async Task InitPrefsAsync(NetUserId userId, ICharacter await db.DbContext.SaveChangesAsync(); - return new PlayerPreferences(new[] { new KeyValuePair(0, defaultProfile) }, 0, Color.FromHex(prefs.AdminOOCColor), []); + return prefs; } public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot) @@ -209,8 +216,9 @@ private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int ne prefs.SelectedCharacterSlot = newSlot; } - private static HumanoidCharacterProfile ConvertProfiles(Profile profile) + private HumanoidCharacterProfile ConvertProfiles(Profile profile) { + // Imperial Medieval: port ConvertProfiles onto organ-markings HumanoidCharacterAppearance var jobs = profile.Jobs.ToDictionary(j => new ProtoId(j.JobName), j => (JobPriority) j.Priority); var antags = profile.Antags.Select(a => new ProtoId(a.AntagName)); var traits = profile.Traits.Select(t => new ProtoId(t.TraitName)); @@ -225,20 +233,36 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile) if (Enum.TryParse(profile.Gender, true, out var genderVal)) gender = genderVal; - // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - var markingsRaw = profile.Markings?.Deserialize>(); + var markings = + new Dictionary, Dictionary>>(); - List markings = new(); - if (markingsRaw != null) + var species = profile.Species; + + if (profile.OrganMarkings?.RootElement is { } element) + { + var data = element.ToDataNode(); + markings = _serialization + .Read, Dictionary>>>( + data, + notNullableOverride: true); + } + else if (profile.Markings is { } profileMarkings) { - foreach (var marking in markingsRaw) + // Legacy flat markings list — keep empty organ map if conversion manager unavailable here + List markingsList = new(); + var markingsRaw = profileMarkings.Deserialize>(); + if (markingsRaw != null) { - var parsed = Marking.ParseFromDbString(marking); - - if (parsed is null) continue; - - markings.Add(parsed); + foreach (var marking in markingsRaw) + { + var parsed = Marking.ParseFromDbString(marking); + if (parsed is null) + continue; + markingsList.Add(parsed.Value); + } } + // Imperial Medieval: ConvertMarkings needs MarkingManager; leave empty if only legacy data + _ = markingsList; } var loadouts = new Dictionary(); @@ -268,16 +292,11 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile) return new HumanoidCharacterProfile( profile.CharacterName, profile.FlavorText, - profile.Species, + species, profile.Age, sex, gender, - new HumanoidCharacterAppearance - ( - profile.HairName, - Color.FromHex(profile.HairColor), - profile.FacialHairName, - Color.FromHex(profile.FacialHairColor), + new HumanoidCharacterAppearance( Color.FromHex(profile.EyeColor), Color.FromHex(profile.SkinColor), markings @@ -294,16 +313,11 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile) ); } - private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null) + private Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null) { profile ??= new Profile(); - var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; - List markingStrings = new(); - foreach (var marking in appearance.Markings) - { - markingStrings.Add(marking.ToString()); - } - var markings = JsonSerializer.SerializeToDocument(markingStrings); + var appearance = humanoid.Appearance; + var dataNode = _serialization.WriteValue(appearance.Markings, alwaysWrite: true, notNullableOverride: true); profile.CharacterName = humanoid.Name; profile.FlavorText = humanoid.FlavorText; @@ -311,14 +325,28 @@ private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int sl profile.Age = humanoid.Age; profile.Sex = humanoid.Sex.ToString(); profile.Gender = humanoid.Gender.ToString(); - profile.HairName = appearance.HairStyleId; - profile.HairColor = appearance.HairColor.ToHex(); - profile.FacialHairName = appearance.FacialHairStyleId; - profile.FacialHairColor = appearance.FacialHairColor.ToHex(); profile.EyeColor = appearance.EyeColor.ToHex(); profile.SkinColor = appearance.SkinColor.ToHex(); profile.SpawnPriority = (int) humanoid.SpawnPriority; - profile.Markings = markings; + profile.OrganMarkings = JsonSerializer.SerializeToDocument(dataNode.ToJsonNode()); + + // support for downgrades - at some point this should be removed + var legacyMarkings = appearance.Markings + .SelectMany(organ => organ.Value.Values) + .SelectMany(i => i) + .Select(marking => marking.ToLegacyDbString()) + .ToList(); + var flattenedMarkings = appearance.Markings.SelectMany(it => it.Value) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var hairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.Hair)?.Value.FirstOrNull(); + var facialHairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.FacialHair)?.Value.FirstOrNull(); + profile.Markings = + JsonSerializer.SerializeToDocument(legacyMarkings.Select(marking => marking.ToString()).ToList()); + profile.HairName = hairMarking?.MarkingId ?? HairStyles.DefaultHairStyle; + profile.FacialHairName = facialHairMarking?.MarkingId ?? HairStyles.DefaultFacialHairStyle; + profile.HairColor = (hairMarking?.MarkingColors[0] ?? Color.Black).ToHex(); + profile.FacialHairColor = (facialHairMarking?.MarkingColors[0] ?? Color.Black).ToHex(); + profile.Slot = slot; profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable; @@ -418,7 +446,7 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// /// The ban id to look for. /// The ban with the given id or null if none exist. - public abstract Task GetServerBanAsync(int id); + public abstract Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -430,11 +458,12 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// The legacy HWId of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - public abstract Task GetServerBanAsync( + public abstract Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type); /// /// Looks up an user's ban history. @@ -447,17 +476,18 @@ public async Task AssignUserIdAsync(string name, NetUserId netUserId) /// The modern HWIDs of the user. /// Include pardoned and expired bans. /// The user's ban history. - public abstract Task> GetServerBansAsync( + public abstract Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned); + bool includeUnbanned, + BanType type); - public abstract Task AddServerBanAsync(ServerBanDef serverBan); - public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban); + public abstract Task AddBanAsync(BanDef ban); + public abstract Task AddUnbanAsync(UnbanDef unban); - public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public async Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { await using var db = await GetDb(); @@ -520,61 +550,23 @@ public async Task GetBanExemption(NetUserId userId, Cancel return flags ?? ServerBanExemptFlags.None; } - #endregion - - #region Role Bans - /* - * ROLE BANS - */ - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - public abstract Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - public abstract Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned); - - public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan); - public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban); - - public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + protected static List>> GetBanDefIncludes(BanType? type = null) { - await using var db = await GetDb(); - var roleBanDetails = await db.DbContext.RoleBan - .Where(b => b.Id == id) - .Select(b => new { b.BanTime, b.PlayerUserId }) - .SingleOrDefaultAsync(); + List>> list = + [ + b => b.Players!, + b => b.Rounds!, + b => b.Hwids!, + b => b.Unban!, + b => b.Addresses!, + ]; - if (roleBanDetails == default) - return; + if (type != BanType.Server) + list.Add(b => b.Roles!); - await db.DbContext.RoleBan - .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId) - .ExecuteUpdateAsync(setters => setters - .SetProperty(b => b.Severity, severity) - .SetProperty(b => b.Reason, reason) - .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null) - .SetProperty(b => b.LastEditedById, editedBy) - .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime) - ); + return list; } + #endregion #region Imperial Medieval @@ -1334,6 +1326,19 @@ protected async Task PlayerRecordExists(DbGuard db, NetUserId userId) if (player == null) return null; + return MakePlayerRecord(player.UserId, player); + } + + protected PlayerRecord MakePlayerRecord(Guid userId, Player? player) + { + if (player == null) + { + // We don't have a record for this player in the database. + // This is possible, for example, when banning people that never connected to the server. + // Just return fallback data here, I guess. + return new PlayerRecord(new NetUserId(userId), default, userId.ToString(), default, null, null); + } + return new PlayerRecord( new NetUserId(player.UserId), new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)), @@ -1357,7 +1362,7 @@ public abstract Task AddConnectionLogAsync(NetUserId userId, ConnectionDenyReason? denied, int serverId); - public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public async Task AddServerBanHitsAsync(int connection, IEnumerable bans) { await using var db = await GetDb(); @@ -1971,81 +1976,17 @@ private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity) entity.Dismissed); } - public async Task GetServerBanAsNoteAsync(int id) + public async Task GetBanAsNoteAsync(int id) { await using var db = await GetDb(); - var ban = await db.DbContext.Ban - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + var ban = await BanRecordQuery(db.DbContext) .SingleOrDefaultAsync(b => b.Id == id); if (ban is null) return null; - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - return new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - ban.BanTime, - MakePlayerRecord(ban.LastEditedBy), - ban.LastEditedAt, - ban.ExpirationTime, - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync(p => - p.UserId == ban.Unban.UnbanningAdmin.Value)), - ban.Unban?.UnbanTime); - } - - public async Task GetServerRoleBanAsNoteAsync(int id) - { - await using var db = await GetDb(); - - var ban = await db.DbContext.RoleBan - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .SingleOrDefaultAsync(b => b.Id == id); - - if (ban is null) - return null; - - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId); - var unbanningAdmin = - ban.Unban is null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin); - - return new ServerRoleBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - ban.BanTime, - MakePlayerRecord(ban.LastEditedBy), - ban.LastEditedAt, - ban.ExpirationTime, - ban.Hidden, - new [] { ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null) }, - MakePlayerRecord(unbanningAdmin), - ban.Unban?.UnbanTime); + return await MakeBanNoteRecord(db.DbContext, ban); } public async Task> GetAllAdminRemarks(Guid player) @@ -2066,8 +2007,7 @@ public async Task> GetAllAdminRemarks(Guid player) .ToListAsync()).Select(MakeAdminNoteRecord)); notes.AddRange(await GetActiveWatchlistsImpl(db, player)); notes.AddRange(await GetMessagesImpl(db, player)); - notes.AddRange(await GetServerBansAsNotesForUser(db, player)); - notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); + notes.AddRange(await GetBansAsNotesForUser(db, player)); return notes; } public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime) @@ -2150,7 +2090,7 @@ public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset dele await db.DbContext.SaveChangesAsync(); } - public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) + public async Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { await using var db = await GetDb(); @@ -2163,19 +2103,6 @@ public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset await db.DbContext.SaveChangesAsync(); } - public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - await using var db = await GetDb(); - - var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync(); - - roleBan.Hidden = true; - roleBan.LastEditedById = deletedBy; - roleBan.LastEditedAt = deletedAt.UtcDateTime; - - await db.DbContext.SaveChangesAsync(); - } - public async Task> GetVisibleAdminRemarks(Guid player) { await using var db = await GetDb(); @@ -2193,8 +2120,7 @@ public async Task> GetVisibleAdminRemarks(Guid player) .Include(note => note.Player) .ToListAsync()).Select(MakeAdminNoteRecord)); notesCol.AddRange(await GetMessagesImpl(db, player)); - notesCol.AddRange(await GetServerBansAsNotesForUser(db, player)); - notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player)); + notesCol.AddRange(await GetBansAsNotesForUser(db, player)); return notesCol; } @@ -2257,98 +2183,70 @@ public async Task MarkMessageAsSeen(int id, bool dismissedToo) await db.DbContext.SaveChangesAsync(); } - // These two are here because they get converted into notes later - protected async Task> GetServerBansAsNotesForUser(DbGuard db, Guid user) + private static IQueryable BanRecordQuery(ServerDbContext dbContext) { - // You can't group queries, as player will not always exist. When it doesn't, the - // whole query returns nothing - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - var bans = await db.DbContext.Ban - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) + return dbContext.Ban .Include(ban => ban.Unban) - .Include(ban => ban.Round) + .Include(ban => ban.Rounds!) + .ThenInclude(r => r.Round) .ThenInclude(r => r!.Server) + .Include(ban => ban.Addresses) + .Include(ban => ban.Players) + .Include(ban => ban.Roles) + .Include(ban => ban.Hwids) .Include(ban => ban.CreatedBy) .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) - .ToArrayAsync(); - - var banNotes = new List(); - foreach (var ban in bans) - { - var banNote = new ServerBanNoteRecord( - ban.Id, - MakeRoundRecord(ban.Round), - MakePlayerRecord(player), - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - MakePlayerRecord(ban.CreatedBy), - NormalizeDatabaseTime(ban.BanTime), - MakePlayerRecord(ban.LastEditedBy), - NormalizeDatabaseTime(ban.LastEditedAt), - NormalizeDatabaseTime(ban.ExpirationTime), - ban.Hidden, - MakePlayerRecord(ban.Unban?.UnbanningAdmin == null - ? null - : await db.DbContext.Player.SingleOrDefaultAsync( - p => p.UserId == ban.Unban.UnbanningAdmin.Value)), - NormalizeDatabaseTime(ban.Unban?.UnbanTime)); + .Include(ban => ban.Unban); + } - banNotes.Add(banNote); - } + private async Task MakeBanNoteRecord(ServerDbContext dbContext, Ban ban) + { + var playerRecords = await AsyncSelect(ban.Players, + async bp => MakePlayerRecord(bp.UserId, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == bp.UserId))); - return banNotes; + return new BanNoteRecord( + ban.Id, + ban.Type, + [..ban.Rounds!.Select(br => MakeRoundRecord(br.Round!))], + [..playerRecords], + ban.PlaytimeAtNote, + ban.Reason, + ban.Severity, + MakePlayerRecord(ban.CreatedBy!), + NormalizeDatabaseTime(ban.BanTime), + MakePlayerRecord(ban.LastEditedBy!), + NormalizeDatabaseTime(ban.LastEditedAt), + NormalizeDatabaseTime(ban.ExpirationTime), + ban.Hidden, + ban.Unban?.UnbanningAdmin == null + ? null + : MakePlayerRecord( + ban.Unban.UnbanningAdmin.Value, + await dbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.Unban.UnbanningAdmin.Value)), + NormalizeDatabaseTime(ban.Unban?.UnbanTime), + [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]); } - protected async Task> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user) + // These two are here because they get converted into notes later + protected async Task> GetBansAsNotesForUser(DbGuard db, Guid user) { - // Server side query - var bansQuery = await db.DbContext.RoleBan - .Where(ban => ban.PlayerUserId == user && !ban.Hidden) - .Include(ban => ban.Unban) - .Include(ban => ban.Round) - .ThenInclude(r => r!.Server) - .Include(ban => ban.CreatedBy) - .Include(ban => ban.LastEditedBy) - .Include(ban => ban.Unban) + // You can't group queries, as player will not always exist. When it doesn't, the + // whole query returns nothing + var bans = await BanRecordQuery(db.DbContext) + .AsSplitQuery() + .Where(ban => ban.Players!.Any(bp => bp.UserId == user) && !ban.Hidden) .ToArrayAsync(); - // Client side query, as EF can't do groups yet - var bansEnumerable = bansQuery - .GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null }) - .Select(banGroup => banGroup) - .ToArray(); - - List bans = new(); - var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user); - foreach (var banGroup in bansEnumerable) + var banNotes = new List(); + foreach (var ban in bans) { - var firstBan = banGroup.First(); - Player? unbanningAdmin = null; - - if (firstBan.Unban?.UnbanningAdmin is not null) - unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value); - - bans.Add(new ServerRoleBanNoteRecord( - firstBan.Id, - MakeRoundRecord(firstBan.Round), - MakePlayerRecord(player), - firstBan.PlaytimeAtNote, - firstBan.Reason, - firstBan.Severity, - MakePlayerRecord(firstBan.CreatedBy), - NormalizeDatabaseTime(firstBan.BanTime), - MakePlayerRecord(firstBan.LastEditedBy), - NormalizeDatabaseTime(firstBan.LastEditedAt), - NormalizeDatabaseTime(firstBan.ExpirationTime), - firstBan.Hidden, - banGroup.Select(ban => ban.RoleId.Replace(BanManager.PrefixJob, null).Replace(BanManager.PrefixAntag, null)).ToArray(), - MakePlayerRecord(unbanningAdmin), - NormalizeDatabaseTime(firstBan.Unban?.UnbanTime))); + var banNote = await MakeBanNoteRecord(db.DbContext, ban); + + banNotes.Add(banNote); } - return bans; + return banNotes; } #endregion @@ -2412,6 +2310,125 @@ public async Task RemoveJobWhitelist(Guid player, ProtoId jo #endregion + // #region Imperial Medieval + + // public async Task GetLastNrpViolationsCount(Guid player, int daysCount, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + + // var daysAgo = DateTime.UtcNow.AddDays(-daysCount); + + // var count = await db.DbContext.NrpViolations + // .Where(v => v.UserId == player && v.ViolationTime >= daysAgo) + // .CountAsync(cancel); + + // return count; + // } + + // public async Task AddNrpViolation(Guid player, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + + // var violation = new NrpViolation + // { + // UserId = player, + // ViolationTime = DateTime.UtcNow + // }; + + // await db.DbContext.NrpViolations.AddAsync(violation, cancel); + // await db.DbContext.SaveChangesAsync(cancel); + // } + // public async Task RemoveNrpViolation(Guid player, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + + // var violations = await db.DbContext.NrpViolations + // .Where(v => v.UserId == player) + // .OrderBy(v => v.ViolationTime) + // .ToListAsync(cancel); + + // if (violations.Count <= 0) + // return; + + // var violation = violations.Last(); + + // db.DbContext.NrpViolations.Remove(violation); + // await db.DbContext.SaveChangesAsync(cancel); + // } + + // public async Task<(int, int)> GetNrpResolves(Guid player, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + // var current = await db.DbContext.NrpResolves + // .Where(v => v.UserId == player) + // .FirstOrDefaultAsync(cancel) ?? new NrpResolves + // { + // UserId = player, + // Rp = 0, + // Nrp = 0, + // }; + // return (current.Rp, current.Nrp); + // } + + // public async Task> GetNrpResolves(CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + // var current = await db.DbContext.NrpResolves + // .ToListAsync(cancel); + // return current; + // } + + // public async Task AddNrpResolve(Guid player, bool isRp, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + // var current = await db.DbContext.NrpResolves + // .Where(v => v.UserId == player) + // .FirstOrDefaultAsync(cancel); + + // if (current == null) + // { + // await db.DbContext.AddAsync(new NrpResolves + // { + // UserId = player, + // Rp = isRp ? 1 : 0, + // Nrp = isRp ? 0 : 1, + // }, + // cancel); + // } + // else + // { + // if(isRp) current.Rp++; + // else current.Nrp++; + // } + // await db.DbContext.SaveChangesAsync(cancel); + // } + + // public async Task RemoveNrpResolve(Guid player, bool isRp, CancellationToken cancel) + // { + // await using var db = await GetDb(cancel); + // var current = await db.DbContext.NrpResolves + // .Where(v => v.UserId == player) + // .FirstOrDefaultAsync(cancel); + + // if (current == null) + // { + // await db.DbContext.AddAsync(new NrpResolves + // { + // UserId = player, + // Rp = 0, + // Nrp = 0, + // }, cancel); + // } + // else + // { + // if(isRp) current.Rp--; + // else current.Nrp--; + // } + // await db.DbContext.SaveChangesAsync(cancel); + // } + + // #endregion + # region IPIntel public async Task UpsertIPIntelCache(DateTime time, IPAddress ip, float score) @@ -2522,5 +2539,19 @@ public virtual void Shutdown() { } + + private static async Task> AsyncSelect( + IEnumerable? enumerable, + Func> selector) + { + var results = new List(); + + foreach (var item in enumerable ?? []) + { + results.Add(await selector(item)); + } + + return [..results]; + } } } diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 62fa71ec27c..d294dfeb467 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -20,6 +20,7 @@ using Robust.Shared.ContentPack; using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using LogLevel = Robust.Shared.Log.LogLevel; using MSLogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -31,15 +32,19 @@ public interface IServerDbManager void Shutdown(); + Task HasPendingModelChanges(); + #region Preferences - Task InitPrefsAsync( + Task InitPrefsAsync( NetUserId userId, - ICharacterProfile defaultProfile, + HumanoidCharacterProfile defaultProfile, CancellationToken cancel); Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index); - Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot); + Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot); + + Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? profile, int slot); Task SaveAdminOOCColorAsync(NetUserId userId, Color color); @@ -47,7 +52,7 @@ Task InitPrefsAsync( // Single method for two operations for transaction. Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); - Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel); + Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel); #endregion #region User Ids @@ -63,7 +68,7 @@ Task InitPrefsAsync( /// /// The ban id to look for. /// The ban with the given id or null if none exist. - Task GetServerBanAsync(int id); + Task GetBanAsync(int id); /// /// Looks up an user's most recent received un-pardoned ban. @@ -75,11 +80,12 @@ Task InitPrefsAsync( /// The legacy HWID of the user. /// The modern HWIDs of the user. /// The user's latest received un-pardoned ban, or null if none exist. - Task GetServerBanAsync( + Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds); + ImmutableArray>? modernHWIds, + BanType type = BanType.Server); /// /// Looks up an user's ban history. @@ -91,17 +97,18 @@ Task InitPrefsAsync( /// The modern HWIDs of the user. /// If true, bans that have been expired or pardoned are also included. /// The user's ban history. - Task> GetServerBansAsync( + Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true); + bool includeUnbanned=true, + BanType type = BanType.Server); - Task AddServerBanAsync(ServerBanDef serverBan); - Task AddServerUnbanAsync(ServerUnbanDef serverBan); + Task AddBanAsync(BanDef ban); + Task AddUnbanAsync(UnbanDef ban); - public Task EditServerBan( + public Task EditBan( int id, string reason, NoteSeverity severity, @@ -127,45 +134,6 @@ public Task EditServerBan( #endregion - #region Role Bans - /// - /// Looks up a role ban by id. - /// This will return a pardoned role ban as well. - /// - /// The role ban id to look for. - /// The role ban with the given id or null if none exist. - Task GetServerRoleBanAsync(int id); - - /// - /// Looks up an user's role ban history. - /// This will return pardoned role bans based on the bool. - /// Requires one of , , or to not be null. - /// - /// The IP address of the user. - /// The NetUserId of the user. - /// The Hardware Id of the user. - /// The modern HWIDs of the user. - /// Whether expired and pardoned bans are included. - /// The user's role ban history. - Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true); - - Task AddServerRoleBanAsync(ServerRoleBanDef serverBan); - Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan); - - public Task EditServerRoleBan( - int id, - string reason, - NoteSeverity severity, - DateTimeOffset? expiration, - Guid editedBy, - DateTimeOffset editedAt); - #endregion - #region Imperial Medieval Task GetLastNrpViolationsCount(Guid player, int daysCount, CancellationToken cancel = default); @@ -248,7 +216,7 @@ Task AddConnectionLogAsync( ConnectionDenyReason? denied, int serverId); - Task AddServerBanHitsAsync(int connection, IEnumerable bans); + Task AddServerBanHitsAsync(int connection, IEnumerable bans); #endregion @@ -340,8 +308,7 @@ Task AddConnectionLogAsync( Task GetAdminNote(int id); Task GetAdminWatchlist(int id); Task GetAdminMessage(int id); - Task GetServerBanAsNoteAsync(int id); - Task GetServerRoleBanAsNoteAsync(int id); + Task GetBanAsNoteAsync(int id); Task> GetAllAdminRemarks(Guid player); Task> GetVisibleAdminNotes(Guid player); Task> GetActiveWatchlists(Guid player); @@ -352,8 +319,7 @@ Task AddConnectionLogAsync( Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt); Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); - Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); + Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt); /// /// Mark an admin message as being seen by the target player. @@ -450,6 +416,7 @@ public sealed class ServerDbManager : IServerDbManager [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly ILogManager _logMgr = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; private ServerDbBase _db = default!; private LoggingProvider _msLogProvider = default!; @@ -481,11 +448,11 @@ public void Init() { case "sqlite": SetupSqlite(out var contextFunc, out var inMemory); - _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog); + _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog, _serialization); break; case "postgres": var (pgOptions, conString) = CreatePostgresOptions(); - _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog); + _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog, _serialization); break; default: throw new InvalidDataException($"Unknown database engine {engine}."); @@ -502,9 +469,9 @@ public void Shutdown() _db.Shutdown(); } - public Task InitPrefsAsync( + public Task InitPrefsAsync( NetUserId userId, - ICharacterProfile defaultProfile, + HumanoidCharacterProfile defaultProfile, CancellationToken cancel) { DbWriteOpsMetric.Inc(); @@ -517,7 +484,13 @@ public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) return RunDbCommand(() => _db.SaveSelectedCharacterIndexAsync(userId, index)); } - public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot) + public Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.MakeCharacterSlotLegacyAsync(userId, slot)); + } + + public Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? profile, int slot) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.SaveCharacterSlotAsync(userId, profile, slot)); @@ -541,7 +514,7 @@ public Task SaveConstructionFavoritesAsync(NetUserId userId, List _db.SaveConstructionFavoritesAsync(userId, constructionFavorites)); } - public Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel) + public Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel)); @@ -559,49 +532,51 @@ public Task AssignUserIdAsync(string name, NetUserId userId) return RunDbCommand(() => _db.GetAssignedUserIdAsync(name)); } - public Task GetServerBanAsync(int id) + public Task GetBanAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(id)); + return RunDbCommand(() => _db.GetBanAsync(id)); } - public Task GetServerBanAsync( + public Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds)); + return RunDbCommand(() => _db.GetBanAsync(address, userId, hwId, modernHWIds, type)); } - public Task> GetServerBansAsync( + public Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned=true) + bool includeUnbanned=true, + BanType type = BanType.Server) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); + return RunDbCommand(() => _db.GetBansAsync(address, userId, hwId, modernHWIds, includeUnbanned, type)); } - public Task AddServerBanAsync(ServerBanDef serverBan) + public Task AddBanAsync(BanDef ban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerBanAsync(serverBan)); + return RunDbCommand(() => _db.AddBanAsync(ban)); } - public Task AddServerUnbanAsync(ServerUnbanDef serverUnban) + public Task AddUnbanAsync(UnbanDef unban) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban)); + return RunDbCommand(() => _db.AddUnbanAsync(unban)); } - public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public Task EditBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt)); + return RunDbCommand(() => _db.EditBan(id, reason, severity, expiration, editedBy, editedAt)); } public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags) @@ -616,40 +591,46 @@ public Task GetBanExemption(NetUserId userId, Cancellation return RunDbCommand(() => _db.GetBanExemption(userId, cancel)); } - #region Role Ban - public Task GetServerRoleBanAsync(int id) + #region Imperial Medieval + + public Task GetLastNrpViolationsCount(Guid player, int daysCount, CancellationToken cancel) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsync(id)); + return RunDbCommand(() => _db.GetLastNrpViolationsCount(player, daysCount, cancel)); } - public Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned = true) + public Task AddNrpViolation(Guid player, CancellationToken cancel) { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned)); + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.AddNrpViolation(player, cancel)); + } + public Task RemoveNrpViolation(Guid player, CancellationToken cancel) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.RemoveNrpViolation(player, cancel)); } - public Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) + public Task<(int, int)> GetNrpResolves(Guid player, CancellationToken cancel) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan)); + return RunDbCommand(() => _db.GetNrpResolves(player, cancel)); } - public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) + public Task> GetNrpResolves(CancellationToken cancel) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban)); + return RunDbCommand(() => _db.GetNrpResolves(cancel)); } - public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt) + public Task AddNrpResolve(Guid player, bool isRp, CancellationToken cancel) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.AddNrpResolve(player, isRp, cancel)); + } + public Task RemoveNrpResolve(Guid player, bool isRp, CancellationToken cancel) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt)); + return RunDbCommand(() => _db.RemoveNrpResolve(player, isRp, cancel)); } #endregion @@ -727,45 +708,7 @@ public Task EditBook(string text, string name, string author, string description return RunDbCommand(() => _db.EditBook(text, name, author, description, cancel)); } - public Task GetLastNrpViolationsCount(Guid player, int daysCount, CancellationToken cancel) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetLastNrpViolationsCount(player, daysCount, cancel)); - } - - public Task AddNrpViolation(Guid player, CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddNrpViolation(player, cancel)); - } - public Task RemoveNrpViolation(Guid player, CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.RemoveNrpViolation(player, cancel)); - } - - public Task<(int, int)> GetNrpResolves(Guid player, CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.GetNrpResolves(player, cancel)); - } - - public Task> GetNrpResolves(CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.GetNrpResolves(cancel)); - } - - public Task AddNrpResolve(Guid player, bool isRp, CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.AddNrpResolve(player, isRp, cancel)); - } - public Task RemoveNrpResolve(Guid player, bool isRp, CancellationToken cancel) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.RemoveNrpResolve(player, isRp, cancel)); - } + // Imperial Medieval: NRP methods defined once above (duplicate merge block removed) public Task GetFlavorImage(Guid uid, CancellationToken cancel, int? slot = null) { @@ -889,7 +832,7 @@ public Task AddConnectionLogAsync( return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId)); } - public Task AddServerBanHitsAsync(int connection, IEnumerable bans) + public Task AddServerBanHitsAsync(int connection, IEnumerable bans) { DbWriteOpsMetric.Inc(); return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans)); @@ -1150,16 +1093,10 @@ public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtN return RunDbCommand(() => _db.GetAdminMessage(id)); } - public Task GetServerBanAsNoteAsync(int id) + public Task GetBanAsNoteAsync(int id) { DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id)); - } - - public Task GetServerRoleBanAsNoteAsync(int id) - { - DbReadOpsMetric.Inc(); - return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id)); + return RunDbCommand(() => _db.GetBanAsNoteAsync(id)); } public Task> GetAllAdminRemarks(Guid player) @@ -1221,16 +1158,10 @@ public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt) return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt)); } - public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) + public Task HideBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) { DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt)); - } - - public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt) - { - DbWriteOpsMetric.Inc(); - return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt)); + return RunDbCommand(() => _db.HideBanFromNotes(id, deletedBy, deletedAt)); } public Task MarkMessageAsSeen(int id, bool dismissedToo) @@ -1310,6 +1241,11 @@ private async void HandleDatabaseNotification(DatabaseNotification notification) } } + public Task HasPendingModelChanges() + { + return RunDbCommand(() => _db.HasPendingModelChanges()); + } + // Wrapper functions to run DB commands from the thread pool. // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread. // For SQLite, this will also enable read parallelization (within limits). diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index c0346708377..bea75fe941b 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Server.Database @@ -30,8 +31,9 @@ public ServerDbPostgres(DbContextOptions options, string connectionString, IConfigurationManager cfg, ISawmill opsLog, - ISawmill notifyLog) - : base(opsLog) + ISawmill notifyLog, + ISerializationManager serialization) + : base(opsLog, serialization) { var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency); @@ -58,24 +60,26 @@ public ServerDbPostgres(DbContextOptions options, } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var query = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(p => p.Id == id); + .ApplyIncludes(GetBanDefIncludes()) + .Where(p => p.Id == id) + .AsSplitQuery(); var ban = await query.SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -86,7 +90,7 @@ public ServerDbPostgres(DbContextOptions options, var exempt = await GetBanExemptionCore(db, userId); var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); - var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer) + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer, type) .OrderByDescending(b => b.BanTime); var ban = await query.FirstOrDefaultAsync(); @@ -94,11 +98,12 @@ public ServerDbPostgres(DbContextOptions options, return ConvertBan(ban); } - public override async Task> GetServerBansAsync(IPAddress? address, + public override async Task> GetBansAsync(IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { if (address == null && userId == null && hwId == null) { @@ -107,12 +112,11 @@ public override async Task> GetServerBansAsync(IPAddress? add await using var db = await GetDbImpl(); - var exempt = await GetBanExemptionCore(db, userId); + var exempt = type == BanType.Role ? null : await GetBanExemptionCore(db, userId); var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId); - var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer); - + var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer, type); var queryBans = await query.ToArrayAsync(); - var bans = new List(queryBans.Length); + var bans = new List(queryBans.Length); foreach (var ban in queryBans) { @@ -127,7 +131,8 @@ public override async Task> GetServerBansAsync(IPAddress? add return bans; } - private static IQueryable MakeBanLookupQuery( + // This has to return IDs instead of direct objects because otherwise all the includes are too complicated. + private static IQueryable MakeBanLookupQuery( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, @@ -135,308 +140,113 @@ private static IQueryable MakeBanLookupQuery( DbGuardImpl db, bool includeUnbanned, ServerBanExemptFlags? exemptFlags, - bool newPlayer) + bool newPlayer, + BanType type) { DebugTools.Assert(!(address == null && userId == null && hwId == null)); - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.Ban); - - if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)) - { - var newQ = db.PgDbContext.Ban - .Include(p => p.Unban) - .Where(b => b.Address != null - && EF.Functions.ContainsOrEqual(b.Address.Value, address) - && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer)); - - query = query == null ? newQ : query.Union(newQ); - } - - DebugTools.Assert( - query != null, - "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - if (exemptFlags is { } exempt) - { - if (exempt != ServerBanExemptFlags.None) - exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - - query = query.Where(b => (b.ExemptFlags & exempt) == 0); - } - - return query.Distinct(); - } - - private static IQueryable? MakeBanLookupQualityShared( - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbSet set) - where TBan : class, IBanCommon - where TUnban : class, IUnbanCommon - { - IQueryable? query = null; + var selectorQueries = new List>(); if (userId is { } uid) - { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.PlayerUserId == uid.UserId); - - query = query == null ? newQ : query.Union(newQ); - } + selectorQueries.Add(db.DbContext.BanPlayer.Where(b => b.UserId == uid.UserId)); if (hwId != null && hwId.Value.Length > 0) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid.Where(bh => + bh.HWId!.Type == HwidType.Legacy && bh.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()) + )); } if (modernHWIds != null) { foreach (var modernHwid in modernHWIds) { - var newQ = set - .Include(p => p.Unban) - .Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray())); - - query = query == null ? newQ : query.Union(newQ); + selectorQueries.Add(db.DbContext.BanHwid + .Where(b => b.HWId!.Type == HwidType.Modern + && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()))); } } - return query; - } - - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - - NetUserId? aUid = null; - if (ban.BanningAdmin is {} aGuid) + if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None) + .HasFlag(ServerBanExemptFlags.IP)) { - aUid = new NetUserId(aGuid); + selectorQueries.Add(db.PgDbContext.BanAddress + .Where(ba => EF.Functions.ContainsOrEqual(ba.Address, address) + && !(ba.Ban!.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && + !newPlayer))); } - var unbanDef = ConvertUnban(ban.Unban); - - return new ServerBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - ban.BanTime, - ban.ExpirationTime, - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unbanDef, - ban.ExemptFlags); - } - - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is {} aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerUnbanDef( - unban.Id, - aUid, - unban.UnbanTime); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - HWId = serverBan.HWId, - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.PgDbContext.SaveChangesAsync(); - } - - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.PgDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.PgDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var query = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id); - - var ban = await query.SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - - } - - public override async Task> GetServerRoleBansAsync(IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) - { - if (address == null && userId == null && hwId == null) - { - throw new ArgumentException("Address, userId, and hwId cannot all be null"); - } - - await using var db = await GetDbImpl(); - - var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned) - .OrderByDescending(b => b.BanTime); + DebugTools.Assert( + selectorQueries.Count > 0, + "At least one filter item (IP/UserID/HWID) must have been given to make query not null."); - return await QueryRoleBans(query); - } + var selectorQuery = selectorQueries + .Select(q => q.Select(sel => sel.BanId)) + .Aggregate((selectors, queryable) => selectors.Union(queryable)); - private static async Task> QueryRoleBans(IQueryable query) - { - var queryRoleBans = await query.ToArrayAsync(); - var bans = new List(queryRoleBans.Length); + var banQuery = db.DbContext.Ban.Where(b => selectorQuery.Contains(b.Id)); - foreach (var ban in queryRoleBans) + if (!includeUnbanned) { - var banDef = ConvertRoleBan(ban); - - if (banDef != null) - { - bans.Add(banDef); - } + banQuery = banQuery.Where(p => + p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); } - return bans; - } - - private static IQueryable MakeRoleBanLookupQuery( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - DbGuardImpl db, - bool includeUnbanned) - { - var query = MakeBanLookupQualityShared( - userId, - hwId, - modernHWIds, - db.PgDbContext.RoleBan); - - if (address != null) + if (exemptFlags is { } exempt) { - var newQ = db.PgDbContext.RoleBan - .Include(p => p.Unban) - .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address)); + if (exempt != ServerBanExemptFlags.None) + exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange - query = query == null ? newQ : query.Union(newQ); + banQuery = banQuery.Where(b => (b.ExemptFlags & exempt) == 0); } - if (!includeUnbanned) - { - query = query?.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - query = query!.Distinct(); - return query; + return banQuery + .Where(b => b.Type == type) + .ApplyIncludes(GetBanDefIncludes(type)) + .AsSplitQuery(); } [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is {} guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is {} aGuid) { aUid = new NetUserId(aGuid); } - var unbanDef = ConvertRoleUnban(ban.Unban); + var unbanDef = ConvertUnban(ban.Unban); - return new ServerRoleBanDef( + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) + { + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; + } + + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], ban.BanTime, ban.ExpirationTime, - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, unbanDef, - ban.RoleId); + ban.ExemptFlags, + roles); } - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -449,45 +259,54 @@ private static IQueryable MakeRoleBanLookupQuery( aUid = new NetUserId(aGuid); } - return new ServerRoleUnbanDef( + return new UnbanDef( unban.Id, aUid, unban.UnbanTime); } - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - var ban = new ServerRoleBan - { - Address = serverRoleBan.Address.ToNpgsqlInet(), - HWId = serverRoleBan.HWId, - Reason = serverRoleBan.Reason, - Severity = serverRoleBan.Severity, - BanningAdmin = serverRoleBan.BanningAdmin?.UserId, - BanTime = serverRoleBan.BanTime.UtcDateTime, - ExpirationTime = serverRoleBan.ExpirationTime?.UtcDateTime, - RoundId = serverRoleBan.RoundId, - PlaytimeAtNote = serverRoleBan.PlaytimeAtNote, - PlayerUserId = serverRoleBan.UserId?.UserId, - RoleId = serverRoleBan.Role, + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole + { + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.PgDbContext.RoleBan.Add(ban); + db.PgDbContext.Ban.Add(banEntity); await db.PgDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.PgDbContext.RoleUnban.Add(new ServerRoleUnban + db.PgDbContext.Unban.Add(new Unban { - BanId = serverRoleUnban.BanId, - UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId, - UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.PgDbContext.SaveChangesAsync(); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index c3109ec6e66..97e265dbff0 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore; using Robust.Shared.Configuration; using Robust.Shared.Network; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Server.Database @@ -36,8 +37,9 @@ public ServerDbSqlite( bool inMemory, IConfigurationManager cfg, bool synchronous, - ISawmill opsLog) - : base(opsLog) + ISawmill opsLog, + ISerializationManager serialization) + : base(opsLog, serialization) { _options = options; @@ -66,48 +68,52 @@ public ServerDbSqlite( } #region Ban - public override async Task GetServerBanAsync(int id) + public override async Task GetBanAsync(int id) { await using var db = await GetDbImpl(); var ban = await db.SqliteDbContext.Ban - .Include(p => p.Unban) + .ApplyIncludes(GetBanDefIncludes()) .Where(p => p.Id == id) + .AsSplitQuery() .SingleOrDefaultAsync(); return ConvertBan(ban); } - public override async Task GetServerBanAsync( + public override async Task GetBanAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ImmutableArray>? modernHWIds) + ImmutableArray>? modernHWIds, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false, type)).FirstOrDefault(); } - public override async Task> GetServerBansAsync( + public override async Task> GetBansAsync( IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { await using var db = await GetDbImpl(); - return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList(); + return (await GetBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned, type)).ToList(); } - private async Task> GetServerBanQueryAsync( + private async Task> GetBanQueryAsync( DbGuardImpl db, IPAddress? address, NetUserId? userId, ImmutableArray? hwId, ImmutableArray>? modernHWIds, - bool includeUnbanned) + bool includeUnbanned, + BanType type) { var exempt = await GetBanExemptionCore(db, userId); @@ -115,7 +121,7 @@ private async Task> GetServerBanQueryAsync( // SQLite can't do the net masking stuff we need to match IP address ranges. // So just pull down the whole list into memory. - var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt); + var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt, type); var playerInfo = new BanMatcher.PlayerInfo { @@ -132,12 +138,12 @@ private async Task> GetServerBanQueryAsync( .Where(b => BanMatcher.BanMatches(b!, playerInfo))!; } - private static async Task> GetAllBans( - SqliteServerDbContext db, + private static async Task> GetAllBans(SqliteServerDbContext db, bool includeUnbanned, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + BanType type) { - IQueryable query = db.Ban.Include(p => p.Unban); + var query = db.Ban.Where(b => b.Type == type).ApplyIncludes(GetBanDefIncludes(type)); if (!includeUnbanned) { query = query.Where(p => @@ -153,269 +159,99 @@ private static async Task> GetAllBans( query = query.Where(b => (b.ExemptFlags & exempt) == 0); } - return await query.ToListAsync(); - } - - public override async Task AddServerBanAsync(ServerBanDef serverBan) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Ban.Add(new ServerBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - ExemptFlags = serverBan.ExemptFlags - }); - - await db.SqliteDbContext.SaveChangesAsync(); + return await query.AsSplitQuery().ToListAsync(); } - public override async Task AddServerUnbanAsync(ServerUnbanDef serverUnban) - { - await using var db = await GetDbImpl(); - - db.SqliteDbContext.Unban.Add(new ServerUnban - { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime - }); - - await db.SqliteDbContext.SaveChangesAsync(); - } - #endregion - - #region Role Ban - public override async Task GetServerRoleBanAsync(int id) - { - await using var db = await GetDbImpl(); - - var ban = await db.SqliteDbContext.RoleBan - .Include(p => p.Unban) - .Where(p => p.Id == id) - .SingleOrDefaultAsync(); - - return ConvertRoleBan(ban); - } - - public override async Task> GetServerRoleBansAsync( - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds, - bool includeUnbanned) + public override async Task AddBanAsync(BanDef ban) { await using var db = await GetDbImpl(); - // SQLite can't do the net masking stuff we need to match IP address ranges. - // So just pull down the whole list into memory. - var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned); - - return queryBans - .Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds)) - .Select(ConvertRoleBan) - .ToList()!; - } - - private static async Task> GetAllRoleBans( - SqliteServerDbContext db, - bool includeUnbanned) - { - IQueryable query = db.RoleBan.Include(p => p.Unban); - if (!includeUnbanned) - { - query = query.Where(p => - p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow)); - } - - return await query.ToListAsync(); - } - - private static bool RoleBanMatches( - ServerRoleBan ban, - IPAddress? address, - NetUserId? userId, - ImmutableArray? hwId, - ImmutableArray>? modernHWIds) - { - if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) - { - return true; - } - - if (userId is { } id && ban.PlayerUserId == id.UserId) - { - return true; - } - - switch (ban.HWId?.Type) - { - case HwidType.Legacy: - if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - break; - - case HwidType.Modern: - if (modernHWIds != null) - { - foreach (var modernHWId in modernHWIds) + var banEntity = new Ban + { + Type = ban.Type, + Addresses = [..ban.Addresses.Select(ba => new BanAddress { Address = ba.ToNpgsqlInet() })], + Hwids = [..ban.HWIds.Select(bh => new BanHwid { HWId = bh })], + Reason = ban.Reason, + Severity = ban.Severity, + BanningAdmin = ban.BanningAdmin?.UserId, + BanTime = ban.BanTime.UtcDateTime, + ExpirationTime = ban.ExpirationTime?.UtcDateTime, + Rounds = [..ban.RoundIds.Select(bri => new BanRound { RoundId = bri })], + PlaytimeAtNote = ban.PlaytimeAtNote, + Players = [..ban.UserIds.Select(bp => new BanPlayer { UserId = bp.UserId })], + ExemptFlags = ban.ExemptFlags, + Roles = ban.Roles == null + ? [] + : ban.Roles.Value.Select(brd => new BanRole { - if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid)) - return true; - } - } - - break; - } - - return false; - } - - public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverBan) - { - await using var db = await GetDbImpl(); - - var ban = new ServerRoleBan - { - Address = serverBan.Address.ToNpgsqlInet(), - Reason = serverBan.Reason, - Severity = serverBan.Severity, - BanningAdmin = serverBan.BanningAdmin?.UserId, - HWId = serverBan.HWId, - BanTime = serverBan.BanTime.UtcDateTime, - ExpirationTime = serverBan.ExpirationTime?.UtcDateTime, - RoundId = serverBan.RoundId, - PlaytimeAtNote = serverBan.PlaytimeAtNote, - PlayerUserId = serverBan.UserId?.UserId, - RoleId = serverBan.Role, + RoleType = brd.RoleType, + RoleId = brd.RoleId + }) + .ToList(), }; - db.SqliteDbContext.RoleBan.Add(ban); + db.SqliteDbContext.Ban.Add(banEntity); await db.SqliteDbContext.SaveChangesAsync(); - return ConvertRoleBan(ban); + return ConvertBan(banEntity); } - public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnban) + public override async Task AddUnbanAsync(UnbanDef unban) { await using var db = await GetDbImpl(); - db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban + db.SqliteDbContext.Unban.Add(new Unban { - BanId = serverUnban.BanId, - UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId, - UnbanTime = serverUnban.UnbanTime.UtcDateTime + BanId = unban.BanId, + UnbanningAdmin = unban.UnbanningAdmin?.UserId, + UnbanTime = unban.UnbanTime.UtcDateTime }); await db.SqliteDbContext.SaveChangesAsync(); } + #endregion [return: NotNullIfNotNull(nameof(ban))] - private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban) + private static BanDef? ConvertBan(Ban? ban) { if (ban == null) { return null; } - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } - NetUserId? aUid = null; if (ban.BanningAdmin is { } aGuid) { aUid = new NetUserId(aGuid); } - var unban = ConvertRoleUnban(ban.Unban); - - return new ServerRoleBanDef( - ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), - ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, - ban.PlaytimeAtNote, - ban.Reason, - ban.Severity, - aUid, - unban, - ban.RoleId); - } - - private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban) - { - if (unban == null) - { - return null; - } - - NetUserId? aUid = null; - if (unban.UnbanningAdmin is { } aGuid) - { - aUid = new NetUserId(aGuid); - } - - return new ServerRoleUnbanDef( - unban.Id, - aUid, - // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. - DateTime.SpecifyKind(unban.UnbanTime, DateTimeKind.Utc)); - } - #endregion - - [return: NotNullIfNotNull(nameof(ban))] - private static ServerBanDef? ConvertBan(ServerBan? ban) - { - if (ban == null) - { - return null; - } - - NetUserId? uid = null; - if (ban.PlayerUserId is { } guid) - { - uid = new NetUserId(guid); - } + var unban = ConvertUnban(ban.Unban); - NetUserId? aUid = null; - if (ban.BanningAdmin is { } aGuid) + ImmutableArray? roles = null; + if (ban.Type == BanType.Role) { - aUid = new NetUserId(aGuid); + roles = [..ban.Roles!.Select(br => new BanRoleDef(br.RoleType, br.RoleId))]; } - var unban = ConvertUnban(ban.Unban); - - return new ServerBanDef( + return new BanDef( ban.Id, - uid, - ban.Address.ToTuple(), - ban.HWId, + ban.Type, + [..ban.Players!.Select(bp => new NetUserId(bp.UserId))], + [..ban.Addresses!.Select(ba => ba.Address.ToTuple())], + [..ban.Hwids!.Select(bh => bh.HWId)], // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc), ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc), - ban.RoundId, + [..ban.Rounds!.Select(r => r.RoundId)], ban.PlaytimeAtNote, ban.Reason, ban.Severity, aUid, - unban); + unban, + ban.ExemptFlags, + roles); } - private static ServerUnbanDef? ConvertUnban(ServerUnban? unban) + private static UnbanDef? ConvertUnban(Unban? unban) { if (unban == null) { @@ -428,7 +264,7 @@ public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnba aUid = new NetUserId(aGuid); } - return new ServerUnbanDef( + return new UnbanDef( unban.Id, aUid, // SQLite apparently always reads DateTime as unspecified, but we always write as UTC. diff --git a/Content.Server/Database/ServerRoleBanDef.cs b/Content.Server/Database/ServerRoleBanDef.cs deleted file mode 100644 index dda3a822378..00000000000 --- a/Content.Server/Database/ServerRoleBanDef.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Net; -using Content.Shared.Database; -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleBanDef -{ - public int? Id { get; } - public NetUserId? UserId { get; } - public (IPAddress address, int cidrMask)? Address { get; } - public ImmutableTypedHwid? HWId { get; } - - public DateTimeOffset BanTime { get; } - public DateTimeOffset? ExpirationTime { get; } - public int? RoundId { get; } - public TimeSpan PlaytimeAtNote { get; } - public string Reason { get; } - public NoteSeverity Severity { get; set; } - public NetUserId? BanningAdmin { get; } - public ServerRoleUnbanDef? Unban { get; } - public string Role { get; } - - public ServerRoleBanDef( - int? id, - NetUserId? userId, - (IPAddress, int)? address, - ImmutableTypedHwid? hwId, - DateTimeOffset banTime, - DateTimeOffset? expirationTime, - int? roundId, - TimeSpan playtimeAtNote, - string reason, - NoteSeverity severity, - NetUserId? banningAdmin, - ServerRoleUnbanDef? unban, - string role) - { - if (userId == null && address == null && hwId == null) - { - throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID"); - } - - if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6) - { - // Fix IPv6-mapped IPv4 addresses - // So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes. - address = (addr.Item1.MapToIPv4(), addr.Item2 - 96); - } - - Id = id; - UserId = userId; - Address = address; - HWId = hwId; - BanTime = banTime; - ExpirationTime = expirationTime; - RoundId = roundId; - PlaytimeAtNote = playtimeAtNote; - Reason = reason; - Severity = severity; - BanningAdmin = banningAdmin; - Unban = unban; - Role = role; - } -} diff --git a/Content.Server/Database/ServerRoleUnbanDef.cs b/Content.Server/Database/ServerRoleUnbanDef.cs deleted file mode 100644 index 3960a86808f..00000000000 --- a/Content.Server/Database/ServerRoleUnbanDef.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Network; - -namespace Content.Server.Database; - -public sealed class ServerRoleUnbanDef -{ - public int BanId { get; } - - public NetUserId? UnbanningAdmin { get; } - - public DateTimeOffset UnbanTime { get; } - - public ServerRoleUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) - { - BanId = banId; - UnbanningAdmin = unbanningAdmin; - UnbanTime = unbanTime; - } -} diff --git a/Content.Server/Database/ServerUnbanDef.cs b/Content.Server/Database/ServerUnbanDef.cs deleted file mode 100644 index 3d39a6b90c4..00000000000 --- a/Content.Server/Database/ServerUnbanDef.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Robust.Shared.Network; - -namespace Content.Server.Database -{ - public sealed class ServerUnbanDef - { - public int BanId { get; } - - public NetUserId? UnbanningAdmin { get; } - - public DateTimeOffset UnbanTime { get; } - - public ServerUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) - { - BanId = banId; - UnbanningAdmin = unbanningAdmin; - UnbanTime = unbanTime; - } - } -} diff --git a/Content.Server/Database/UnbanDef.cs b/Content.Server/Database/UnbanDef.cs new file mode 100644 index 00000000000..1fa4fc2a6a9 --- /dev/null +++ b/Content.Server/Database/UnbanDef.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Network; + +namespace Content.Server.Database +{ + public sealed class UnbanDef + { + public int BanId { get; } + + public NetUserId? UnbanningAdmin { get; } + + public DateTimeOffset UnbanTime { get; } + + public UnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime) + { + BanId = banId; + UnbanningAdmin = unbanningAdmin; + UnbanTime = unbanTime; + } + } +} diff --git a/Content.Server/Delivery/DeliverySystem.cs b/Content.Server/Delivery/DeliverySystem.cs index 72d9427dffc..496bc61a34d 100644 --- a/Content.Server/Delivery/DeliverySystem.cs +++ b/Content.Server/Delivery/DeliverySystem.cs @@ -4,6 +4,7 @@ using Content.Server.StationRecords.Systems; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using Content.Shared.Chat; using Content.Shared.Delivery; using Content.Shared.FingerprintReader; using Content.Shared.Labels.EntitySystems; diff --git a/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs new file mode 100644 index 00000000000..a00194c9110 --- /dev/null +++ b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs @@ -0,0 +1,35 @@ +using Content.Shared.Damage; + +namespace Content.Server.Destructible; + +public sealed partial class DestructibleSystem +{ + /// + /// Tests all triggers in a DestructibleComponent to see how expensive it is to query them. + /// + public void TestAllTriggers(List> destructibles) + { + foreach (var (uid, damageable, destructible) in destructibles) + { + foreach (var threshold in destructible.Thresholds) + { + // Chances are, none of these triggers will pass! + Triggered(threshold, (uid, damageable)); + } + } + } + + /// + /// Tests all behaviours in a DestructibleComponent to see how expensive it is to query them. + /// + public void TestAllBehaviors(List> destructibles) + { + foreach (var (uid, damageable, destructible) in destructibles) + { + foreach (var threshold in destructible.Thresholds) + { + Execute(threshold, uid); + } + } + } +} diff --git a/Content.Server/Destructible/DestructibleSystem.cs b/Content.Server/Destructible/DestructibleSystem.cs index b4a79c6830b..27e01c00292 100644 --- a/Content.Server/Destructible/DestructibleSystem.cs +++ b/Content.Server/Destructible/DestructibleSystem.cs @@ -2,19 +2,20 @@ using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Systems; using Content.Server.Construction; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Explosion.EntitySystems; using Content.Server.Fluids.EntitySystems; using Content.Server.Stack; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Destructible; +using Content.Shared.Destructible.Thresholds.Triggers; using Content.Shared.FixedPoint; +using Content.Shared.Gibbing; using Content.Shared.Humanoid; using Content.Shared.Trigger.Systems; using JetBrains.Annotations; @@ -26,14 +27,14 @@ namespace Content.Server.Destructible { [UsedImplicitly] - public sealed class DestructibleSystem : SharedDestructibleSystem + public sealed partial class DestructibleSystem : SharedDestructibleSystem { [Dependency] public readonly IRobustRandom Random = default!; public new IEntityManager EntityManager => base.EntityManager; [Dependency] public readonly AtmosphereSystem AtmosphereSystem = default!; [Dependency] public readonly AudioSystem AudioSystem = default!; - [Dependency] public readonly BodySystem BodySystem = default!; + [Dependency] public readonly GibbingSystem Gibbing = default!; [Dependency] public readonly ConstructionSystem ConstructionSystem = default!; [Dependency] public readonly ExplosionSystem ExplosionSystem = default!; [Dependency] public readonly StackSystem StackSystem = default!; @@ -42,24 +43,24 @@ public sealed class DestructibleSystem : SharedDestructibleSystem [Dependency] public readonly PuddleSystem PuddleSystem = default!; [Dependency] public readonly SharedContainerSystem ContainerSystem = default!; [Dependency] public readonly IPrototypeManager PrototypeManager = default!; - [Dependency] public readonly IAdminLogManager _adminLogger = default!; + [Dependency] public readonly IAdminLogManager AdminLogger = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(Execute); + SubscribeLocalEvent(OnDamageChanged); } /// - /// Check if any thresholds were reached. if they were, execute them. + /// Check if any thresholds were reached. if they were, execute them. /// - public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args) + private void OnDamageChanged(EntityUid uid, DestructibleComponent component, DamageChangedEvent args) { component.IsBroken = false; foreach (var threshold in component.Thresholds) { - if (threshold.Reached(args.Damageable, this)) + if (Triggered(threshold, (uid, args.Damageable))) { RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold), true); @@ -77,23 +78,23 @@ public void Execute(EntityUid uid, DestructibleComponent component, DamageChange })); // If it doesn't have a humanoid component, it's probably not particularly notable? - if (logImpact > LogImpact.Medium && !HasComp(uid)) + if (logImpact > LogImpact.Medium && !HasComp(uid)) logImpact = LogImpact.Medium; if (args.Origin != null) { - _adminLogger.Add(LogType.Damaged, + AdminLogger.Add(LogType.Damaged, logImpact, $"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]"); } else { - _adminLogger.Add(LogType.Damaged, + AdminLogger.Add(LogType.Damaged, logImpact, $"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]"); } - threshold.Execute(uid, this, EntityManager, args.Origin); + Execute(threshold, uid, args.Origin); } if (threshold.OldTriggered) @@ -108,6 +109,61 @@ public void Execute(EntityUid uid, DestructibleComponent component, DamageChange } } + /// + /// Check if the given threshold should trigger. + /// + public bool Triggered(DamageThreshold threshold, Entity owner) + { + if (threshold.Trigger == null) + return false; + + if (threshold.Triggered && threshold.TriggersOnce) + return false; + + if (threshold.OldTriggered) + { + threshold.OldTriggered = threshold.Trigger.Reached(owner, this); + return false; + } + + if (!threshold.Trigger.Reached(owner, this)) + return false; + + threshold.OldTriggered = true; + return true; + } + + /// + /// Check if the conditions for the given threshold are currently true. + /// + public bool Reached(DamageThreshold threshold, Entity owner) + { + if (threshold.Trigger == null) + return false; + + return threshold.Trigger.Reached(owner, this); + } + + /// + /// Triggers this threshold. + /// + /// The entity that owns this threshold. + /// The entity that caused this threshold to trigger. + public void Execute(DamageThreshold threshold, EntityUid owner, EntityUid? cause = null) + { + threshold.Triggered = true; + + foreach (var behavior in threshold.Behaviors) + { + // The owner has been deleted. We stop execution of behaviors here. + if (!Exists(owner)) + return; + + // TODO: Replace with EntityEffects. + behavior.Execute(owner, this, cause); + } + } + public bool TryGetDestroyedAt(Entity ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt) { destroyedAt = null; @@ -145,7 +201,7 @@ public FixedPoint2 DestroyedAt(EntityUid uid, DestructibleComponent? destructibl if (behavior is DoActsBehavior actBehavior && actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage)) { - damageNeeded = Math.Min(damageNeeded.Float(), trigger.Damage); + damageNeeded = FixedPoint2.Min(damageNeeded, trigger.Damage); } } } diff --git a/Content.Server/Destructible/Thresholds/ActsFlags.cs b/Content.Server/Destructible/Thresholds/ActsFlags.cs deleted file mode 100644 index 5193499aa90..00000000000 --- a/Content.Server/Destructible/Thresholds/ActsFlags.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Content.Server.Destructible.Thresholds -{ - public sealed class ActsFlags { } -} diff --git a/Content.Server/Destructible/Thresholds/Behaviors/BurnBodyBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/BurnBodyBehavior.cs index 1d3c1993f97..5bc83702c75 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/BurnBodyBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/BurnBodyBehavior.cs @@ -11,6 +11,11 @@ namespace Content.Server.Destructible.Thresholds.Behaviors; [DataDefinition] public sealed partial class BurnBodyBehavior : IThresholdBehavior { + /// + /// The popup displayed upon destruction. + /// + [DataField] + public LocId PopupMessage = "bodyburn-text-others"; public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? cause = null) { @@ -27,7 +32,7 @@ public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? caus } var bodyIdentity = Identity.Entity(bodyId, system.EntityManager); - sharedPopupSystem.PopupCoordinates(Loc.GetString("bodyburn-text-others", ("name", bodyIdentity)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution); + sharedPopupSystem.PopupCoordinates(Loc.GetString(PopupMessage, ("name", bodyIdentity)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution); system.EntityManager.QueueDeleteEntity(bodyId); } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs index 6392da75b0e..c2bf5fc2751 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs @@ -1,4 +1,6 @@ -namespace Content.Server.Destructible.Thresholds.Behaviors +using Content.Shared.Destructible; + +namespace Content.Server.Destructible.Thresholds.Behaviors { [Serializable] [DataDefinition] diff --git a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs index a8448a1b7f2..5646ce6d88b 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs @@ -43,7 +43,7 @@ public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause if (EntityPrototypeHelpers.HasComponent(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory)) { var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset))); - system.StackSystem.SetCount(spawned, toSpawn); + system.StackSystem.SetCount((spawned, null), toSpawn); system.EntityManager.GetComponent(spawned).LocalRotation = system.Random.NextAngle(); } else diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs index 9aa798a3d09..1addf96b62a 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs @@ -14,10 +14,7 @@ public sealed partial class GibBehavior : IThresholdBehavior public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) { - if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body)) - { - system.BodySystem.GibBody(owner, _recursive, body); - } + system.Gibbing.Gib(owner, _recursive); } } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs index 413991515b9..13027a31fcf 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs @@ -58,7 +58,7 @@ public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause var spawned = SpawnInContainer ? system.EntityManager.SpawnNextToOrDrop(entityId, owner) : system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector())); - system.StackSystem.SetCount(spawned, count); + system.StackSystem.SetCount((spawned, null), count); TransferForensics(spawned, system, owner); } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs index ff050ce2cda..ad34f05edd0 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpillBehavior.cs @@ -1,42 +1,38 @@ -using Content.Shared.Chemistry.EntitySystems; using Content.Server.Fluids.EntitySystems; -using Content.Shared.Fluids.Components; +using Content.Shared.Chemistry.EntitySystems; using JetBrains.Annotations; -namespace Content.Server.Destructible.Thresholds.Behaviors -{ - [UsedImplicitly] - [DataDefinition] - public sealed partial class SpillBehavior : IThresholdBehavior - { - [DataField] - public string? Solution; +namespace Content.Server.Destructible.Thresholds.Behaviors; - /// - /// If there is a SpillableComponent on EntityUidowner use it to create a puddle/smear. - /// Or whatever solution is specified in the behavior itself. - /// If none are available do nothing. - /// - /// Entity on which behavior is executed - /// system calling the behavior - /// - public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) - { - var solutionContainerSystem = system.EntityManager.System(); - var spillableSystem = system.EntityManager.System(); +[UsedImplicitly] +[DataDefinition] +public sealed partial class SpillBehavior : IThresholdBehavior +{ + /// + /// Optional fallback solution name if SpillableComponent is not present. + /// + [DataField] + public string? Solution; - var coordinates = system.EntityManager.GetComponent(owner).Coordinates; + /// + /// When triggered, spills the entity's solution onto the ground. + /// Will first try to use the solution from a SpillableComponent if present, + /// otherwise falls back to the solution specified in the behavior's data fields. + /// The solution is properly drained/split before spilling to prevent double-spilling with other behaviors. + /// + /// Entity whose solution will be spilled + /// System calling this behavior + /// Optional entity that caused this behavior to trigger + public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null) + { + var puddleSystem = system.EntityManager.System(); + var solutionContainer = system.EntityManager.System(); + var coordinates = system.EntityManager.GetComponent(owner).Coordinates; - if (system.EntityManager.TryGetComponent(owner, out SpillableComponent? spillableComponent) && - solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName, out _, out var compSolution)) - { - spillableSystem.TrySplashSpillAt(owner, coordinates, compSolution, out _, false, user: cause); - } - else if (Solution != null && - solutionContainerSystem.TryGetSolution(owner, Solution, out _, out var behaviorSolution)) - { - spillableSystem.TrySplashSpillAt(owner, coordinates, behaviorSolution, out _, user: cause); - } - } + // Spill the solution that was drained/split + if (solutionContainer.TryGetSolution(owner, Solution, out _, out var solution)) + puddleSystem.TrySplashSpillAt(owner, coordinates, solution, out _, false, cause); + else + puddleSystem.TrySplashSpillAt(owner, coordinates, out _, out _, false, cause); } } diff --git a/Content.Server/Destructible/Thresholds/Behaviors/VomitBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/VomitBehavior.cs index 067e7d45659..cba85762135 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/VomitBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/VomitBehavior.cs @@ -1,4 +1,4 @@ -using Content.Server.Medical; +using Content.Shared.Medical; namespace Content.Server.Destructible.Thresholds.Behaviors; diff --git a/Content.Server/Destructible/Thresholds/Behaviors/WeightedSpawnEntityBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/WeightedSpawnEntityBehavior.cs index 4a275ac8958..2e04971972c 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/WeightedSpawnEntityBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/WeightedSpawnEntityBehavior.cs @@ -7,6 +7,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Spawners; +using Robust.Shared.Random; namespace Content.Server.Destructible.Thresholds.Behaviors; diff --git a/Content.Server/Destructible/Thresholds/DamageThreshold.cs b/Content.Server/Destructible/Thresholds/DamageThreshold.cs index e180f5c45cb..de4362d6772 100644 --- a/Content.Server/Destructible/Thresholds/DamageThreshold.cs +++ b/Content.Server/Destructible/Thresholds/DamageThreshold.cs @@ -1,96 +1,43 @@ using Content.Server.Destructible.Thresholds.Behaviors; -using Content.Server.Destructible.Thresholds.Triggers; -using Content.Shared.Damage; +using Content.Shared.Destructible.Thresholds.Triggers; -namespace Content.Server.Destructible.Thresholds -{ - [DataDefinition] - public sealed partial class DamageThreshold - { - [DataField("behaviors")] - private List _behaviors = new(); - - /// - /// Whether or not this threshold was triggered in the previous call to - /// . - /// - [ViewVariables] public bool OldTriggered { get; private set; } - - /// - /// Whether or not this threshold has already been triggered. - /// - [DataField("triggered")] - public bool Triggered { get; private set; } - - /// - /// Whether or not this threshold only triggers once. - /// If false, it will trigger again once the entity is healed - /// and then damaged to reach this threshold once again. - /// It will not repeatedly trigger as damage rises beyond that. - /// - [DataField("triggersOnce")] - public bool TriggersOnce { get; set; } - - /// - /// The trigger that decides if this threshold has been reached. - /// - [DataField("trigger")] - public IThresholdTrigger? Trigger { get; set; } - - /// - /// Behaviors to activate once this threshold is triggered. - /// - [ViewVariables] public IReadOnlyList Behaviors => _behaviors; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - if (Trigger == null) - { - return false; - } +namespace Content.Server.Destructible.Thresholds; - if (Triggered && TriggersOnce) - { - return false; - } - - if (OldTriggered) - { - OldTriggered = Trigger.Reached(damageable, system); - return false; - } - - if (!Trigger.Reached(damageable, system)) - { - return false; - } - - OldTriggered = true; - return true; - } - - /// - /// Triggers this threshold. - /// - /// The entity that owns this threshold. - /// - /// An instance of to get dependency and - /// system references from, if relevant. - /// - /// - /// - public void Execute(EntityUid owner, DestructibleSystem system, IEntityManager entityManager, EntityUid? cause) - { - Triggered = true; - - foreach (var behavior in Behaviors) - { - // The owner has been deleted. We stop execution of behaviors here. - if (!entityManager.EntityExists(owner)) - return; - - behavior.Execute(owner, system, cause); - } - } - } +[DataDefinition] +public sealed partial class DamageThreshold +{ + /// + /// Whether or not this threshold was triggered in the previous call to + /// . + /// + [ViewVariables] public bool OldTriggered; + + /// + /// Whether or not this threshold has already been triggered. + /// + [DataField] + public bool Triggered; + + /// + /// Whether or not this threshold only triggers once. + /// If false, it will trigger again once the entity is healed + /// and then damaged to reach this threshold once again. + /// It will not repeatedly trigger as damage rises beyond that. + /// + [DataField] + public bool TriggersOnce; + + /// + /// The condition that decides if this threshold has been reached. + /// Gets evaluated each time the entity's damage changes. + /// + [DataField] + public IThresholdTrigger? Trigger; + + /// + /// Behaviors to activate once this threshold is triggered. + /// TODO: Replace with EntityEffects. + /// + [DataField] + public List Behaviors = new(); } diff --git a/Content.Server/Destructible/Thresholds/ThresholdActs.cs b/Content.Server/Destructible/Thresholds/ThresholdActs.cs deleted file mode 100644 index d2ecd53742f..00000000000 --- a/Content.Server/Destructible/Thresholds/ThresholdActs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Server.Destructible.Thresholds -{ - [Flags, FlagsFor(typeof(ActsFlags))] - [Serializable] - public enum ThresholdActs - { - None = 0, - Breakage, - Destruction - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs deleted file mode 100644 index 70e0c3917c1..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when all of its triggers have activated. - /// - [Serializable] - [DataDefinition] - public sealed partial class AndTrigger : IThresholdTrigger - { - [DataField("triggers")] - public List Triggers { get; set; } = new(); - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - foreach (var trigger in Triggers) - { - if (!trigger.Reached(damageable, system)) - { - return false; - } - } - - return true; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs deleted file mode 100644 index 09083105cae..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Content.Shared.Damage.Prototypes; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// of the specified class is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageGroupTrigger : IThresholdTrigger - { - [DataField("damageGroup", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DamageGroup { get; set; } = default!; - - /// - /// The amount of damage at which this threshold will trigger. - /// - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.DamagePerGroup[DamageGroup] >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs deleted file mode 100644 index ce0106ee2b1..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageTrigger : IThresholdTrigger - { - /// - /// The amount of damage at which this threshold will trigger. - /// - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.TotalDamage >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs deleted file mode 100644 index ed9fd27ab73..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Content.Shared.Damage.Prototypes; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// of the specified type is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public sealed partial class DamageTypeTrigger : IThresholdTrigger - { - [DataField("damageType", required:true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DamageType { get; set; } = default!; - - [DataField("damage", required: true)] - public int Damage { get; set; } = default!; - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - return damageable.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) && - damageReceived >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs deleted file mode 100644 index dfceb31c9b0..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - public interface IThresholdTrigger - { - /// - /// Checks if this trigger has been reached. - /// - /// The damageable component to check with. - /// - /// An instance of to pull - /// dependencies from, if any. - /// - /// true if this trigger has been reached, false otherwise. - bool Reached(DamageableComponent damageable, DestructibleSystem system); - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs deleted file mode 100644 index 179141dbf80..00000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Damage; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when any of its triggers have activated. - /// - [Serializable] - [DataDefinition] - public sealed partial class OrTrigger : IThresholdTrigger - { - [DataField("triggers")] - public List Triggers { get; private set; } = new(); - - public bool Reached(DamageableComponent damageable, DestructibleSystem system) - { - foreach (var trigger in Triggers) - { - if (trigger.Reached(damageable, system)) - { - return true; - } - } - - return false; - } - } -} diff --git a/Content.Server/DeviceLinking/Systems/GunSignalControlSystem.cs b/Content.Server/DeviceLinking/Systems/GunSignalControlSystem.cs index 1f9d338c58e..cf78f5c1140 100644 --- a/Content.Server/DeviceLinking/Systems/GunSignalControlSystem.cs +++ b/Content.Server/DeviceLinking/Systems/GunSignalControlSystem.cs @@ -27,18 +27,18 @@ private void OnSignalReceived(Entity gunControl, ref return; if (args.Port == gunControl.Comp.TriggerPort) - _gun.AttemptShoot(gunControl, gun); + _gun.AttemptShoot((gunControl, gun)); if (!TryComp(gunControl, out var autoShoot)) return; if (args.Port == gunControl.Comp.TogglePort) - _gun.SetEnabled(gunControl, autoShoot, !autoShoot.Enabled); + _gun.SetEnabled((gunControl, autoShoot), !autoShoot.Enabled); if (args.Port == gunControl.Comp.OnPort) - _gun.SetEnabled(gunControl, autoShoot, true); + _gun.SetEnabled((gunControl, autoShoot), true); if (args.Port == gunControl.Comp.OffPort) - _gun.SetEnabled(gunControl, autoShoot, false); + _gun.SetEnabled((gunControl, autoShoot), false); } } diff --git a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs index b03cceda0f3..1cb5091d242 100644 --- a/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs +++ b/Content.Server/DeviceLinking/Systems/PowerSensorSystem.cs @@ -98,28 +98,19 @@ private void UpdateOutputs(EntityUid uid, PowerSensorComponent comp) var nodeContainer = Comp(uid); var deviceNode = (CableDeviceNode) nodeContainer.Nodes[cable.Node]; - var charge = 0f; - var chargingState = false; - var dischargingState = false; - // update state based on the power stats retrieved from the selected power network var xform = _xformQuery.GetComponent(uid); if (!TryComp(xform.GridUid, out MapGridComponent? grid)) return; - var cables = deviceNode.GetReachableNodes(xform, _nodeQuery, _xformQuery, grid, EntityManager); - foreach (var node in cables) - { - if (node.NodeGroup == null) - continue; + if (deviceNode.NodeGroup == null) + return; - var group = (IBasePowerNet) node.NodeGroup; - var stats = _powerNet.GetNetworkStatistics(group.NetworkNode); - charge = comp.Output ? stats.OutStorageCurrent : stats.InStorageCurrent; - chargingState = charge > comp.LastCharge; - dischargingState = charge < comp.LastCharge; - break; - } + var group = (IBasePowerNet) deviceNode.NodeGroup; + var stats = _powerNet.GetNetworkStatistics(group.NetworkNode); + var charge = comp.Output ? stats.OutStorageCurrent : stats.InStorageCurrent; + var chargingState = charge > comp.LastCharge; + var dischargingState = charge < comp.LastCharge; comp.LastCharge = charge; diff --git a/Content.Server/DeviceLinking/Systems/RandomGateSystem.cs b/Content.Server/DeviceLinking/Systems/RandomGateSystem.cs new file mode 100644 index 00000000000..a9610399d12 --- /dev/null +++ b/Content.Server/DeviceLinking/Systems/RandomGateSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.DeviceLinking.Components; +using Content.Shared.DeviceLinking.Events; +using Content.Shared.DeviceLinking.Systems; +using Robust.Shared.Random; + +namespace Content.Server.DeviceLinking.Systems; + +public sealed class RandomGateSystem : SharedRandomGateSystem +{ + [Dependency] private readonly DeviceLinkSystem _deviceLink = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnSignalReceived); + } + + private void OnSignalReceived(Entity ent, ref SignalReceivedEvent args) + { + if (args.Port != ent.Comp.InputPort) + return; + + var output = _random.Prob(ent.Comp.SuccessProbability); + if (output != ent.Comp.LastOutput) + { + ent.Comp.LastOutput = output; + Dirty(ent); + _deviceLink.SendSignal(ent.Owner, ent.Comp.OutputPort, output); + } + } +} diff --git a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs index e34929bd2e2..b31e6a6d3bf 100644 --- a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs @@ -361,14 +361,11 @@ private void DetermineMode(EntityUid configuratorUid, NetworkConfiguratorCompone if (hasLinking && HasComp(target) || hasLinking == configurator.LinkModeActive) return; - if (hasLinking) - { - SetMode(configuratorUid, configurator, userUid, true); - return; - } - - if (HasComp(target)) + var hasNetworking = HasComp(target); + if (hasNetworking) SetMode(configuratorUid, configurator, userUid, false); + else if (hasLinking) + SetMode(configuratorUid, configurator, userUid, true); } #endregion @@ -842,11 +839,5 @@ public void OnDeviceShutdown(Entity conf, Entity< UpdateListUiState(conf, conf.Comp); } - - private void OnUiOpenAttempt(EntityUid uid, NetworkConfiguratorComponent configurator, ActivatableUIOpenAttemptEvent args) - { - if (configurator.LinkModeActive) - args.Cancel(); - } #endregion } diff --git a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs index 358bc4ab3ec..34ddc39f712 100644 --- a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs +++ b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; +using NetCord; using NetCord.Gateway; using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; @@ -24,6 +25,10 @@ public void Initialize() { _discordLink.OnMessageReceived += OnMessageReceived; + #if DEBUG + _discordLink.RegisterCommandCallback(OnDebugCommandRun, "debug"); + #endif + _configurationManager.OnValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged, true); _configurationManager.OnValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged, true); } @@ -36,6 +41,14 @@ public void Shutdown() _configurationManager.UnsubValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged); } + #if DEBUG + private void OnDebugCommandRun(CommandReceivedEventArgs ev) + { + var args = string.Join('\n', ev.Arguments); + _sawmill.Info($"Provided arguments: \n{args}"); + } + #endif + private void OnOocChannelIdChanged(string channelId) { if (string.IsNullOrEmpty(channelId)) diff --git a/Content.Server/Discord/DiscordLink/DiscordLink.cs b/Content.Server/Discord/DiscordLink/DiscordLink.cs index cbfe12f180f..5bfb61d4d10 100644 --- a/Content.Server/Discord/DiscordLink/DiscordLink.cs +++ b/Content.Server/Discord/DiscordLink/DiscordLink.cs @@ -1,9 +1,11 @@ -using System.Threading.Tasks; +using System.Collections.ObjectModel; +using System.Threading.Tasks; using Content.Shared.CCVar; using NetCord; using NetCord.Gateway; using NetCord.Rest; using Robust.Shared.Configuration; +using Robust.Shared.Utility; namespace Content.Server.Discord.DiscordLink; @@ -18,9 +20,16 @@ public sealed class CommandReceivedEventArgs public string Command { get; init; } = string.Empty; /// - /// The arguments to the command. This is everything after the command + /// The raw arguments to the command. This is everything after the command /// - public string Arguments { get; init; } = string.Empty; + public string RawArguments { get; init; } = string.Empty; + + /// + /// A list of arguments to the command. + /// This uses mostly for maintainability. + /// + public List Arguments { get; init; } = []; + /// /// Information about the message that the command was received from. This includes the message content, author, etc. /// Use this to reply to the message, delete it, etc. @@ -66,6 +75,7 @@ public sealed class DiscordLink : IPostInjectInit /// public event Action? OnMessageReceived; + // TODO: consider implementing this in a way where we can unregister it in a similar way public void RegisterCommandCallback(Action callback, string command) { OnCommandReceived += args => @@ -180,24 +190,28 @@ private ValueTask OnCommandReceivedInternal(Message message) var trimmedInput = content[BotPrefix.Length..].Trim(); var firstSpaceIndex = trimmedInput.IndexOf(' '); - string command, arguments; + string command, rawArguments; if (firstSpaceIndex == -1) { command = trimmedInput; - arguments = string.Empty; + rawArguments = string.Empty; } else { command = trimmedInput[..firstSpaceIndex]; - arguments = trimmedInput[(firstSpaceIndex + 1)..].Trim(); + rawArguments = trimmedInput[(firstSpaceIndex + 1)..].Trim(); } + var argumentList = new List(); + CommandParsing.ParseArguments(rawArguments, argumentList); + // Raise the event! OnCommandReceived?.Invoke(new CommandReceivedEventArgs { Command = command, - Arguments = arguments, + Arguments = argumentList, + RawArguments = rawArguments, Message = message, }); return ValueTask.CompletedTask; diff --git a/Content.Server/Disposal/Unit/BeingDisposedSystem.cs b/Content.Server/Disposal/Unit/BeingDisposedSystem.cs index fcff4ba3b55..b2fa7239fb3 100644 --- a/Content.Server/Disposal/Unit/BeingDisposedSystem.cs +++ b/Content.Server/Disposal/Unit/BeingDisposedSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; +using Content.Shared.Atmos; namespace Content.Server.Disposal.Unit; diff --git a/Content.Server/Disposal/Unit/DisposableSystem.cs b/Content.Server/Disposal/Unit/DisposableSystem.cs index d307488110e..993545b9dca 100644 --- a/Content.Server/Disposal/Unit/DisposableSystem.cs +++ b/Content.Server/Disposal/Unit/DisposableSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Disposal.Tube; -using Content.Shared.Body.Components; -using Content.Shared.Damage; +using Content.Shared.Body; +using Content.Shared.Damage.Systems; using Content.Shared.Disposal.Components; using Content.Shared.Item; using Content.Shared.Throwing; diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs index 7dfdebf3678..40ecdc21be8 100644 --- a/Content.Server/Doors/Systems/AirlockSystem.cs +++ b/Content.Server/Doors/Systems/AirlockSystem.cs @@ -1,81 +1,5 @@ -using Content.Server.Power.Components; -using Content.Server.Wires; -using Content.Shared.DeviceLinking.Events; -using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; -using Content.Shared.Interaction; -using Content.Shared.Power; -using Content.Shared.Wires; -using Robust.Shared.Player; namespace Content.Server.Doors.Systems; -public sealed class AirlockSystem : SharedAirlockSystem -{ - [Dependency] private readonly WiresSystem _wiresSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnSignalReceived); - - SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnActivate, before: new[] { typeof(DoorSystem) }); - } - - private void OnSignalReceived(EntityUid uid, AirlockComponent component, ref SignalReceivedEvent args) - { - if (args.Port == component.AutoClosePort && component.AutoClose) - { - component.AutoClose = false; - Dirty(uid, component); - } - } - - private void OnPowerChanged(EntityUid uid, AirlockComponent component, ref PowerChangedEvent args) - { - component.Powered = args.Powered; - Dirty(uid, component); - - if (!TryComp(uid, out DoorComponent? door)) - return; - - if (!args.Powered) - { - // stop any scheduled auto-closing - if (door.State == DoorState.Open) - DoorSystem.SetNextStateChange(uid, null); - } - else - { - UpdateAutoClose(uid, door: door); - } - } - - private void OnActivate(EntityUid uid, AirlockComponent component, ActivateInWorldEvent args) - { - if (args.Handled || !args.Complex) - return; - - if (TryComp(uid, out var panel) && - panel.Open && - TryComp(args.User, out var actor)) - { - if (TryComp(uid, out var wiresPanelSecurity) && - !wiresPanelSecurity.WiresAccessible) - return; - - _wiresSystem.OpenUserInterface(uid, actor.PlayerSession); - args.Handled = true; - return; - } - - if (component.KeepOpenIfClicked && component.AutoClose) - { - // Disable auto close - component.AutoClose = false; - Dirty(uid, component); - } - } -} +public sealed class AirlockSystem : SharedAirlockSystem; diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index 5fd0bf7e97a..91cba14bd66 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -1,10 +1,12 @@ using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Monitor.Components; using Content.Server.Atmos.Monitor.Systems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; @@ -22,6 +24,8 @@ public sealed class FirelockSystem : SharedFirelockSystem [Dependency] private readonly SharedMapSystem _mapping = default!; [Dependency] private readonly PointLightSystem _pointLight = default!; + private EntityQuery _atmosAlarmQuery; + private const int UpdateInterval = 30; private int _accumulatedTicks; @@ -30,9 +34,9 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnAtmosAlarm); - SubscribeLocalEvent(PowerChanged); + _atmosAlarmQuery = GetEntityQuery(); } private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args) @@ -57,6 +61,14 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var firelock, out var door)) { + if (_atmosAlarmQuery.TryComp(uid, out var alarmable) + && alarmable.LastAlarmState == AtmosAlarmType.Danger + && this.IsPowered(uid, EntityManager) + && door.State == DoorState.Open) + { + EmergencyPressureStop(uid, firelock, door); + } + // only bother to check pressure on doors that are some variation of closed. if (door.State != DoorState.Closed && door.State != DoorState.Welded diff --git a/Content.Server/Dragon/Components/DragonComponent.cs b/Content.Server/Dragon/Components/DragonComponent.cs index 80461e156af..c634836b5af 100644 --- a/Content.Server/Dragon/Components/DragonComponent.cs +++ b/Content.Server/Dragon/Components/DragonComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Chemistry.Components; using Content.Shared.NPC.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -65,5 +66,17 @@ public sealed partial class DragonComponent : Component /// [DataField] public ProtoId Faction = "Dragon"; + + /// + /// The smoke to spawn upon rift timeout death. + /// + [DataField] + public EntProtoId SmokePrototype = "BloodSmoke"; + + /// + /// The solution to place into the smoke (mostly just needed for color) + /// + [DataField] + public Solution SmokeSolution = new ([new("Blood", 1)]); } } diff --git a/Content.Server/Dragon/DragonRiftSystem.cs b/Content.Server/Dragon/DragonRiftSystem.cs index 9cab018fd79..842b27ec862 100644 --- a/Content.Server/Dragon/DragonRiftSystem.cs +++ b/Content.Server/Dragon/DragonRiftSystem.cs @@ -2,16 +2,14 @@ using Content.Server.NPC; using Content.Server.NPC.Systems; using Content.Server.Pinpointer; -using Content.Shared.Damage; using Content.Shared.Dragon; using Content.Shared.Examine; using Content.Shared.Sprite; -using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Serialization.Manager; using System.Numerics; -using Robust.Shared.Audio; +using Content.Shared.Damage.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Utility; diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index 960365a7ca1..8bc5fd83b14 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -1,9 +1,11 @@ +using Content.Server.Fluids.EntitySystems; using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; using Content.Server.Popups; -using Content.Server.Roles; using Content.Shared.Actions; +using Content.Shared.Chemistry.Components; using Content.Shared.Dragon; +using Content.Shared.Gibbing; using Content.Shared.Maps; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -21,6 +23,7 @@ namespace Content.Server.Dragon; public sealed partial class DragonSystem : EntitySystem { [Dependency] private readonly CarpRiftsConditionSystem _carpRifts = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly NpcFactionSystem _faction = default!; [Dependency] private readonly PopupSystem _popup = default!; @@ -30,6 +33,8 @@ public sealed partial class DragonSystem : EntitySystem [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; + [Dependency] private readonly SmokeSystem _smoke = default!; private EntityQuery _objQuery; @@ -97,11 +102,14 @@ public override void Update(float frameTime) if (!_mobState.IsDead(uid)) comp.RiftAccumulator += frameTime; - // Delete it, naughty dragon! + // Gib it, naughty dragon! if (comp.RiftAccumulator >= comp.RiftMaxAccumulator) { - Roar(uid, comp); - QueueDel(uid); + Roar(uid, comp, Transform(uid).Coordinates); + var smoke = Spawn(comp.SmokePrototype, Transform(uid).Coordinates); + if (TryComp(smoke, out var smokeComp)) + _smoke.StartSmoke(smoke, comp.SmokeSolution, smokeComp.Duration, smokeComp.SpreadAmount, smokeComp); + _gibbing.Gib(uid); } } } @@ -167,6 +175,8 @@ private void OnSpawnRift(EntityUid uid, DragonComponent component, DragonSpawnRi } var carpUid = Spawn(component.RiftPrototype, _transform.GetMapCoordinates(uid, xform: xform)); + Transform(carpUid).LocalRotation = Angle.Zero; + component.Rifts.Add(carpUid); Comp(carpUid).Dragon = uid; } @@ -199,10 +209,15 @@ private void OnZombified(Entity ent, ref EntityZombifiedEvent a _faction.AddFaction(ent.Owner, ent.Comp.Faction); } - private void Roar(EntityUid uid, DragonComponent comp) + private void Roar(EntityUid uid, DragonComponent comp, EntityCoordinates? coords = null) { if (comp.SoundRoar != null) - _audio.PlayPvs(comp.SoundRoar, uid); + { + if (coords != null) + _audio.PlayPvs(comp.SoundRoar, coords.Value); + else + _audio.PlayPvs(comp.SoundRoar, uid); + } } /// @@ -224,10 +239,9 @@ public void DeleteRifts(EntityUid uid, bool resetRole, DragonComponent? comp = n comp.Rifts.Clear(); // stop here if not trying to reset the objective's rift count - if (!resetRole || !TryComp(uid, out var mindContainer) || !mindContainer.HasMind) + if (!resetRole || !_mind.TryGetMind(uid, out _, out var mind)) return; - var mind = Comp(mindContainer.Mind.Value); foreach (var objId in mind.Objectives) { if (_objQuery.TryGetComponent(objId, out var obj)) @@ -246,10 +260,9 @@ public void RiftCharged(EntityUid uid, DragonComponent? comp = null) if (!Resolve(uid, ref comp)) return; - if (!TryComp(uid, out var mindContainer) || !mindContainer.HasMind) + if (!_mind.TryGetMind(uid, out _, out var mind)) return; - var mind = Comp(mindContainer.Mind.Value); foreach (var objId in mind.Objectives) { if (_objQuery.TryGetComponent(objId, out var obj)) @@ -268,9 +281,6 @@ public void RiftDestroyed(EntityUid uid, DragonComponent? comp = null) if (!Resolve(uid, ref comp)) return; - // do reset the rift count since crew destroyed the rift, not deleted by the dragon dying. - DeleteRifts(uid, true, comp); - // We can't predict the rift being destroyed anyway so no point adding weakened to shared. comp.WeakenedAccumulator = comp.WeakenedDuration; _movement.RefreshMovementSpeedModifiers(uid); diff --git a/Content.Server/Electrocution/ElectrocutionNode.cs b/Content.Server/Electrocution/ElectrocutionNode.cs index ddf09fcce74..3f53e7718fc 100644 --- a/Content.Server/Electrocution/ElectrocutionNode.cs +++ b/Content.Server/Electrocution/ElectrocutionNode.cs @@ -14,10 +14,11 @@ public sealed partial class ElectrocutionNode : Node [DataField("node")] public string? NodeName; - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { if (CableEntity == null || NodeName == null) diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index eb943b9e4ed..533a4a8d0cb 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -1,14 +1,12 @@ using Content.Server.Administration.Logs; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; -using Content.Server.NodeContainer.NodeGroups; -using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Power.NodeGroups; using Content.Server.Weapons.Melee; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Electrocution; using Content.Shared.IdentityManagement; @@ -27,7 +25,6 @@ using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; using Robust.Shared.Physics.Events; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -415,19 +412,15 @@ private bool DoCommonElectrocution(EntityUid uid, EntityUid? sourceUid, ? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier) : _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier); } - // TODO: Sparks here. if (shockDamage is { } dmg) { - var actual = _damageable.TryChangeDamage(uid, - new DamageSpecifier(_prototypeManager.Index(DamageType), dmg), origin: sourceUid); - - if (actual != null) + if (_damageable.TryChangeDamage(uid, new DamageSpecifier(_prototypeManager.Index(DamageType), dmg), out var damage, origin: sourceUid)) { _adminLogger.Add(LogType.Electrocution, - $"{ToPrettyString(uid):entity} received {actual.GetTotal():damage} powered electrocution damage{(sourceUid != null ? " from " + ToPrettyString(sourceUid.Value) : ""):source}"); + $"{ToPrettyString(uid):entity} received {damage:damage} powered electrocution damage{(sourceUid != null ? " from " + ToPrettyString(sourceUid.Value) : ""):source}"); } } diff --git a/Content.Server/Emoting/Systems/BodyEmotesSystem.cs b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs index aef79f1419b..39217a81963 100644 --- a/Content.Server/Emoting/Systems/BodyEmotesSystem.cs +++ b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Emoting.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Hands.Components; using Robust.Shared.Prototypes; diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs index 67f9cabd425..38ab09e1cfc 100644 --- a/Content.Server/Emp/EmpSystem.cs +++ b/Content.Server/Emp/EmpSystem.cs @@ -2,16 +2,11 @@ using Content.Server.Radio; using Content.Server.SurveillanceCamera; using Content.Shared.Emp; -using Robust.Shared.Map; namespace Content.Server.Emp; public sealed class EmpSystem : SharedEmpSystem { - [Dependency] private readonly EntityLookupSystem _lookup = default!; - - public const string EmpPulseEffectPrototype = "EffectEmpPulse"; - public override void Initialize() { base.Initialize(); @@ -22,84 +17,6 @@ public override void Initialize() SubscribeLocalEvent(OnCameraSetActive); } - public override void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration) - { - foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range)) - { - TryEmpEffects(uid, energyConsumption, duration); - } - Spawn(EmpPulseEffectPrototype, coordinates); - } - - /// - /// Triggers an EMP pulse at the given location, by first raising an , then a raising on all entities in range. - /// - /// The location to trigger the EMP pulse at. - /// The range of the EMP pulse. - /// The amount of energy consumed by the EMP pulse. - /// The duration of the EMP effects. - public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, float duration) - { - foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range)) - { - TryEmpEffects(uid, energyConsumption, duration); - } - Spawn(EmpPulseEffectPrototype, coordinates); - } - - /// - /// Attempts to apply the effects of an EMP pulse onto an entity by first raising an , followed by raising a on it. - /// - /// The entity to apply the EMP effects on. - /// The amount of energy consumed by the EMP. - /// The duration of the EMP effects. - public void TryEmpEffects(EntityUid uid, float energyConsumption, float duration) - { - var attemptEv = new EmpAttemptEvent(); - RaiseLocalEvent(uid, attemptEv); - if (attemptEv.Cancelled) - return; - - DoEmpEffects(uid, energyConsumption, duration); - } - - /// - /// Applies the effects of an EMP pulse onto an entity by raising a on it. - /// - /// The entity to apply the EMP effects on. - /// The amount of energy consumed by the EMP. - /// The duration of the EMP effects. - public void DoEmpEffects(EntityUid uid, float energyConsumption, float duration) - { - var ev = new EmpPulseEvent(energyConsumption, false, false, TimeSpan.FromSeconds(duration)); - RaiseLocalEvent(uid, ref ev); - - if (ev.Affected) - Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates); - - if (!ev.Disabled) - return; - - var disabled = EnsureComp(uid); - disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp)) - { - if (comp.DisabledUntil < Timing.CurTime) - { - RemComp(uid); - var ev = new EmpDisabledRemoved(); - RaiseLocalEvent(uid, ref ev); - } - } - } - private void OnRadioSendAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioSendAttemptEvent args) { args.Cancelled = true; @@ -120,14 +37,3 @@ private void OnCameraSetActive(EntityUid uid, EmpDisabledComponent component, re args.Cancelled = true; } } - -/// -/// Raised on an entity before . Cancel this to prevent the emp event being raised. -/// -public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs; - -[ByRefEvent] -public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration); - -[ByRefEvent] -public record struct EmpDisabledRemoved(); diff --git a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs index fbf281201bb..297deefed00 100644 --- a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs +++ b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs @@ -72,8 +72,8 @@ bool IsTileClear() if (component.Deleted || !IsTileClear()) return; - if (TryComp(uid, out StackComponent? stackComp) - && component.RemoveOnInteract && !_stackSystem.Use(uid, 1, stackComp)) + if (TryComp(uid, out var stackComp) + && component.RemoveOnInteract && !_stackSystem.TryUse((uid, stackComp), 1)) { return; } diff --git a/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs new file mode 100644 index 00000000000..e7b8aaf22b1 --- /dev/null +++ b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityConditions; +using Content.Shared.EntityConditions.Conditions.Body; + +namespace Content.Server.EntityConditions.Conditions; + +/// +/// Returns true if this entity is both able to breathe and is currently breathing. +/// +/// +public sealed partial class IsBreathingEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly RespiratorSystem _respirator = default!; + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _respirator.IsBreathing(entity.AsNullable()); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs new file mode 100644 index 00000000000..033704ffcdc --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs @@ -0,0 +1,22 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// This effect adjusts a gas at the tile this entity is currently on. +/// The amount changed is modified by scale. +/// +/// +public sealed partial class CreateGasEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var tileMix = _atmosphere.GetContainingMixture(entity.AsNullable(), false, true); + + tileMix?.AdjustMoles(args.Effect.Gas, args.Scale * args.Effect.Moles); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs new file mode 100644 index 00000000000..65c818f1430 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// Adds a number of FireStacks modified by scale to this entity. +/// The amount of FireStacks added is modified by scale. +/// +/// +public sealed partial class FlammableEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly FlammableSystem _flammable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // The multiplier is determined by if the entity is already on fire, and if the multiplier for existing FireStacks has a value. + // If both of these are true, we use the MultiplierOnExisting value, otherwise we use the standard Multiplier. + var multiplier = entity.Comp.FireStacks == 0f || args.Effect.MultiplierOnExisting == null ? args.Effect.Multiplier : args.Effect.MultiplierOnExisting.Value; + + _flammable.AdjustFireStacks(entity, args.Scale * multiplier, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs new file mode 100644 index 00000000000..de90656c666 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs @@ -0,0 +1,23 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Atmos; + +namespace Content.Server.EntityEffects.Effects.Atmos; + +/// +/// Sets this entity on fire. +/// +/// +public sealed partial class IngiteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly FlammableSystem _flammable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // TODO: Proper BodySystem Metabolism Effect relay... + // TODO: If this fucks over downstream shitmed, I give you full approval to use whatever shitcode method you need to fix it. Metabolism is awful. + _flammable.Ignite(entity, entity, flammable: entity.Comp); + } +} + diff --git a/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs new file mode 100644 index 00000000000..0cbf0b38649 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Body; + +namespace Content.Server.EntityEffects.Effects.Body; + +/// +/// This effect adjusts a respirator's saturation value. +/// The saturation adjustment is modified by scale. +/// +/// +public sealed partial class OxygenateEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly RespiratorSystem _respirator = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _respirator.UpdateSaturation(entity, args.Scale * args.Effect.Factor, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs new file mode 100644 index 00000000000..ab656b8aa45 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustHealthEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.Health += args.Effect.Amount; + _plantHolder.CheckHealth(entity, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs new file mode 100644 index 00000000000..4732465098c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationLevelEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.MutationLevel += args.Effect.Amount * entity.Comp.MutationMod; + _plantHolder.CheckHealth(entity, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs new file mode 100644 index 00000000000..3163ee374c4 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationModEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.MutationMod += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs new file mode 100644 index 00000000000..56c016700dd --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustNutritionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _plantHolder.AdjustNutrient(entity, args.Effect.Amount, entity); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs new file mode 100644 index 00000000000..0495034b38d --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustPestsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.PestLevel += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs new file mode 100644 index 00000000000..f63d49f209a --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs @@ -0,0 +1,17 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.Seed.Potency = Math.Max(entity.Comp.Seed.Potency + args.Effect.Amount, 1); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs new file mode 100644 index 00000000000..31dc3289777 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustToxinsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.Toxins += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs new file mode 100644 index 00000000000..706eeb2ffe0 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWaterEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _plantHolder.AdjustWater(entity, args.Effect.Amount, entity.Comp); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs new file mode 100644 index 00000000000..34aa51e4ff4 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWeedsEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + entity.Comp.WeedLevel += args.Effect.Amount; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs new file mode 100644 index 00000000000..b0faa6255e1 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAffectGrowthEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + _plantHolder.AffectGrowth(entity, (int)args.Effect.Amount, entity); + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs new file mode 100644 index 00000000000..3d82f74b116 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs @@ -0,0 +1,122 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// This system mutates an inputted stat for a PlantHolder, only works for floats, integers, and bools. +/// +/// +public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSystem +{ + // TODO: This is awful. I do not have the strength to refactor this. I want it gone. + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + var effect = args.Effect; + var member = entity.Comp.Seed.GetType().GetField(args.Effect.TargetValue); + + if (member == null) + { + Log.Error($"{ effect.GetType().Name } Error: Member { args.Effect.TargetValue} not found on { entity.Comp.Seed.GetType().Name }. Did you misspell it?"); + return; + } + + var currentValObj = member.GetValue(entity.Comp.Seed); + if (currentValObj == null) + return; + + if (member.FieldType == typeof(float)) + { + var floatVal = (float)currentValObj; + MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); + member.SetValue(entity.Comp.Seed, floatVal); + } + else if (member.FieldType == typeof(int)) + { + var intVal = (int)currentValObj; + MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); + member.SetValue(entity.Comp.Seed, intVal); + } + else if (member.FieldType == typeof(bool)) + { + var boolVal = (bool)currentValObj; + boolVal = !boolVal; + member.SetValue(entity.Comp.Seed, boolVal); + } + } + + // Mutate reference 'val' between 'min' and 'max' by pretending the value + // is representable by a thermometer code with 'bits' number of bits and + // randomly flipping some of them. + private void MutateFloat(ref float val, float min, float max, int bits) + { + if (min == max) + { + val = min; + return; + } + + // Starting number of bits that are high, between 0 and bits. + // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. + int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + // val may be outside the range of min/max due to starting prototype values, so clamp. + valInt = Math.Clamp(valInt, 0, bits); + + // Probability that the bit flip increases n. + // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it. + // In other words, it tends to go to the middle. + float probIncrease = 1 - (float)valInt / bits; + int valIntMutated; + if (_random.Prob(probIncrease)) + { + valIntMutated = valInt + 1; + } + else + { + valIntMutated = valInt - 1; + } + + // Set value based on mutated thermometer code. + float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); + val = valMutated; + } + + private void MutateInt(ref int val, int min, int max, int bits) + { + if (min == max) + { + val = min; + return; + } + + // Starting number of bits that are high, between 0 and bits. + // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. + int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + // val may be outside the range of min/max due to starting prototype values, so clamp. + valInt = Math.Clamp(valInt, 0, bits); + + // Probability that the bit flip increases n. + // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it. + // In other words, it tends to go to the middle. + float probIncrease = 1 - (float)valInt / bits; + int valMutated; + if (_random.Prob(probIncrease)) + { + valMutated = val + 1; + } + else + { + valMutated = val - 1; + } + + valMutated = Math.Clamp(valMutated, min, max); + val = valMutated; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs new file mode 100644 index 00000000000..710bce24dde --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs @@ -0,0 +1,30 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + var deviation = 0; + var seed = entity.Comp.Seed; + if (seed == null) + return; + if (entity.Comp.Age > seed.Maturation) + deviation = (int) Math.Max(seed.Maturation - 1, entity.Comp.Age - _random.Next(7, 10)); + else + deviation = (int) (seed.Maturation / seed.GrowthStages); + entity.Comp.Age -= deviation; + entity.Comp.LastProduce = entity.Comp.Age; + entity.Comp.SkipAging++; + entity.Comp.ForceUpdate = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs new file mode 100644 index 00000000000..5139c43c0fc --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs @@ -0,0 +1,29 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Server.Popups; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Content.Shared.Popups; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (entity.Comp.Seed.Seedless) + return; + + _popup.PopupEntity( + Loc.GetString("botany-plant-seedsdestroyed"), + entity, + PopupType.SmallCaution + ); + entity.Comp.Seed.Seedless = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs new file mode 100644 index 00000000000..4da542df931 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (_random.Prob(0.1f)) + { + entity.Comp.Seed!.Lifespan++; + } + + if (_random.Prob(0.1f)) + { + entity.Comp.Seed!.Endurance++; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs new file mode 100644 index 00000000000..8a073392e1c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + entity.Comp.Seed.Viable = true; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzuEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzuEntityEffectSystem.cs new file mode 100644 index 00000000000..f681afda5eb --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzuEntityEffectSystem.cs @@ -0,0 +1,16 @@ +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantRemoveKudzuEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + entity.Comp.Seed.TurnIntoKudzu = false; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs new file mode 100644 index 00000000000..a120a1eccde --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs @@ -0,0 +1,24 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Server.Popups; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) + return; + + if (!entity.Comp.Seed.Seedless) + return; + + _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), entity); + entity.Comp.Seed.Seedless = false; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs new file mode 100644 index 00000000000..a957de82b4d --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs @@ -0,0 +1,38 @@ +using Content.Server.Botany.Components; +using Content.Server.Botany.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// This effect directly increases the potency of a PlantHolder's plant provided it exists and isn't dead. +/// Potency directly correlates to the size of the plant's produce. +/// +/// +public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Dead) + return; + + if (entity.Comp.Seed.Potency < args.Effect.PotencyLimit) + { + entity.Comp.Seed.Potency = Math.Min(entity.Comp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); + + if (entity.Comp.Seed.Potency > args.Effect.PotencySeedlessThreshold) + { + entity.Comp.Seed.Seedless = true; + } + } + else if (entity.Comp.Seed.Yield > 1 && _random.Prob(0.1f)) + { + // Too much of a good thing reduces yield + entity.Comp.Seed.Yield--; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs new file mode 100644 index 00000000000..b21bc0b7b34 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs @@ -0,0 +1,45 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Content.Shared.FixedPoint; +using Content.Shared.Random.Helpers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var chemicals = entity.Comp.Seed.Chemicals; + var randomChems = _proto.Index(args.Effect.RandomPickBotanyReagent); + + // Add a random amount of a random chemical to this set of chemicals + var (chemicalId, quantity) = randomChems.Pick(_random); + var amount = FixedPoint2.Max(_random.NextFloat(0f, 1f) * quantity, FixedPoint2.Epsilon); + var seedChemQuantity = new SeedChemQuantity(); + if (chemicals.ContainsKey(chemicalId)) + { + seedChemQuantity.Min = chemicals[chemicalId].Min; + seedChemQuantity.Max = chemicals[chemicalId].Max + amount; + } + else + { + //Set the minimum to a fifth of the quantity to give some level of bad luck protection + seedChemQuantity.Min = FixedPoint2.Clamp(quantity / 5f, FixedPoint2.Epsilon, 1f); + seedChemQuantity.Max = seedChemQuantity.Min + amount; + seedChemQuantity.Inherent = false; + } + var potencyDivisor = 100f / seedChemQuantity.Max; + seedChemQuantity.PotencyDivisor = (float) potencyDivisor; + chemicals[chemicalId] = seedChemQuantity; + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs new file mode 100644 index 00000000000..e2376ba186c --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Content.Server.Botany.Components; +using Content.Shared.Atmos; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var gasses = entity.Comp.Seed.ExudeGasses; + + // Add a random amount of a random gas to this gas dictionary + float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); + var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + + if (!gasses.TryAdd(gas, amount)) + { + gasses[gas] += amount; + } + } +} + +public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + var gasses = entity.Comp.Seed.ConsumeGasses; + + // Add a random amount of a random gas to this gas dictionary + var amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); + var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + + if (!gasses.TryAdd(gas, amount)) + { + gasses[gas] += amount; + } + } +} + diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs new file mode 100644 index 00000000000..95d7f97bbe3 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null) + return; + + switch (entity.Comp.Seed.HarvestRepeat) + { + case HarvestType.NoRepeat: + entity.Comp.Seed.HarvestRepeat = HarvestType.Repeat; + break; + case HarvestType.Repeat: + entity.Comp.Seed.HarvestRepeat = HarvestType.SelfHarvest; + break; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs new file mode 100644 index 00000000000..c26e1e08cf7 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs @@ -0,0 +1,31 @@ +using Content.Server.Botany; +using Content.Server.Botany.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Botany; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.EntityEffects.Effects.Botany; + +public sealed partial class PlantMutateSpeciesChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (entity.Comp.Seed == null || entity.Comp.Seed.MutationPrototypes.Count == 0) + return; + + var targetProto = _random.Pick(entity.Comp.Seed.MutationPrototypes); + _proto.TryIndex(targetProto, out SeedPrototype? protoSeed); + + if (protoSeed == null) + { + Log.Error($"Seed prototype could not be found: {targetProto}!"); + return; + } + + entity.Comp.Seed = entity.Comp.Seed.SpeciesChange(protoSeed); + } +} diff --git a/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs new file mode 100644 index 00000000000..2e2fadff323 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/ExplodeEntityEffectSystem.cs @@ -0,0 +1,20 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects; +using Content.Shared.Explosion.Components; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Makes this entity explode using its . +/// +/// +public sealed partial class ExplodeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly ExplosionSystem _explosion = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _explosion.TriggerExplosive(entity, entity, args.Effect.Delete, args.Effect.Intensity, args.Effect.Radius, args.User); + } +} diff --git a/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs new file mode 100644 index 00000000000..c623b258576 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs @@ -0,0 +1,42 @@ +using Content.Server.Ghost.Roles.Components; +using Content.Server.Speech.Components; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects; +using Content.Shared.Mind.Components; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Makes this entity sentient. Allows ghost to take it over if it's not already occupied. +/// Optionally also allows this entity to speak. +/// +/// +public sealed partial class MakeSentientEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + // Let affected entities speak normally to make this effect different from, say, the "random sentience" event + // This also works on entities that already have a mind + // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect + if (args.Effect.AllowSpeech) + { + RemComp(entity); + // TODO: Make MonkeyAccent a replacement accent and remove MonkeyAccent code-smell. + RemComp(entity); + } + + // Stops from adding a ghost role to things like people who already have a mind + if (TryComp(entity, out var mindContainer) && mindContainer.HasMind) + return; + + // Don't add a ghost role to things that already have ghost roles + if (TryComp(entity, out GhostRoleComponent? ghostRole)) + return; + + ghostRole = AddComp(entity); + EnsureComp(entity); + + ghostRole.RoleName = entity.Comp.EntityName; + ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description"); + } +} diff --git a/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs new file mode 100644 index 00000000000..5f19bcc50b1 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Polymorph.Components; +using Content.Server.Polymorph.Systems; +using Content.Shared.EntityEffects; + +namespace Content.Server.EntityEffects.Effects; + +/// +/// Polymorphs this entity into another entity. +/// +/// +public sealed partial class PolymorphEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly PolymorphSystem _polymorph = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _polymorph.PolymorphEntity(entity, args.Effect.Prototype); + } +} diff --git a/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs new file mode 100644 index 00000000000..e5ef488de81 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs @@ -0,0 +1,51 @@ +using Content.Server.Fluids.EntitySystems; +using Content.Server.Spreader; +using Content.Shared.Chemistry.Components; +using Content.Shared.Coordinates.Helpers; +using Content.Shared.EntityEffects; +using Content.Shared.EntityEffects.Effects.Solution; +using Content.Shared.Maps; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; + +namespace Content.Server.EntityEffects.Effects.Solution; + +/// +/// This effect creates smoke at this solution's position. +/// The amount of smoke created is modified by scale. +/// +/// +public sealed partial class AreaReactionEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly SmokeSystem _smoke = default!; + [Dependency] private readonly SpreaderSystem _spreader = default!; + [Dependency] private readonly TurfSystem _turf = default!; + + // TODO: A sane way to make Smoke without a solution. + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var xform = Transform(entity); + var mapCoords = _xform.GetMapCoordinates(entity); + var spreadAmount = (int) Math.Max(0, Math.Ceiling(args.Scale / args.Effect.OverflowThreshold)); + var effect = args.Effect; + + if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) || + !_map.TryGetTileRef(gridUid, grid, xform.Coordinates, out var tileRef)) + return; + + if (_spreader.RequiresFloorToSpread(effect.PrototypeId.ToString()) && _turf.IsSpace(tileRef)) + return; + + var coords = _map.MapToGrid(gridUid, mapCoords); + var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid()); + + _smoke.StartSmoke(ent, entity.Comp.Solution, args.Effect.Duration, spreadAmount); + + _audio.PlayPvs(args.Effect.Sound, entity, AudioParams.Default.WithVariation(0.25f)); + } +} diff --git a/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs new file mode 100644 index 00000000000..fec56aa2d35 --- /dev/null +++ b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.EntityEffects; +using ExplosionEffect = Content.Shared.EntityEffects.Effects.Transform.Explosion; + +namespace Content.Server.EntityEffects.Effects.Transform; + +/// +/// Creates an explosion at this entity's position. +/// Intensity is modified by scale. +/// +/// +public sealed partial class ExplosionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly ExplosionSystem _explosion = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var intensity = MathF.Min(args.Effect.IntensityPerUnit * args.Scale, args.Effect.MaxTotalIntensity); + + _explosion.QueueExplosion( + entity, + args.Effect.ExplosionType, + intensity, + args.Effect.IntensitySlope, + args.Effect.MaxIntensity, + args.Effect.TileBreakScale); + } +} diff --git a/Content.Server/EntityEffects/EntityEffectSystem.cs b/Content.Server/EntityEffects/EntityEffectSystem.cs deleted file mode 100644 index efa6bb65219..00000000000 --- a/Content.Server/EntityEffects/EntityEffectSystem.cs +++ /dev/null @@ -1,977 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; -using Content.Server.Botany; -using Content.Server.Chat.Systems; -using Content.Server.Emp; -using Content.Server.Explosion.EntitySystems; -using Content.Server.Fluids.EntitySystems; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Medical; -using Content.Server.Polymorph.Components; -using Content.Server.Polymorph.Systems; -using Content.Server.Speech.Components; -using Content.Server.Spreader; -using Content.Server.Temperature.Components; -using Content.Server.Temperature.Systems; -using Content.Server.Zombies; -using Content.Shared.Atmos; -using Content.Shared.Atmos.Components; -using Content.Shared.Body.Components; -using Content.Shared.Coordinates.Helpers; -using Content.Shared.EntityEffects.EffectConditions; -using Content.Shared.EntityEffects.Effects.PlantMetabolism; -using Content.Shared.EntityEffects.Effects; -using Content.Shared.EntityEffects; -using Content.Shared.Flash; -using Content.Shared.Maps; -using Content.Shared.Mind.Components; -using Content.Shared.Popups; -using Content.Shared.Random; -using Content.Shared.Traits.Assorted; -using Content.Shared.Zombies; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -using TemperatureCondition = Content.Shared.EntityEffects.EffectConditions.Temperature; // disambiguate the namespace -using PolymorphEffect = Content.Shared.EntityEffects.Effects.Polymorph; -using Content.Shared.Chat; - -namespace Content.Server.EntityEffects; - -public sealed class EntityEffectSystem : EntitySystem -{ - private static readonly ProtoId RandomPickBotanyReagent = "RandomPickBotanyReagent"; - - [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly BloodstreamSystem _bloodstream = default!; - [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly EmpSystem _emp = default!; - [Dependency] private readonly ExplosionSystem _explosion = default!; - [Dependency] private readonly FlammableSystem _flammable = default!; - [Dependency] private readonly SharedFlashSystem _flash = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly MutationSystem _mutation = default!; - [Dependency] private readonly NarcolepsySystem _narcolepsy = default!; - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; - [Dependency] private readonly PolymorphSystem _polymorph = default!; - [Dependency] private readonly RespiratorSystem _respirator = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPointLightSystem _pointLight = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SmokeSystem _smoke = default!; - [Dependency] private readonly SpreaderSystem _spreader = default!; - [Dependency] private readonly TemperatureSystem _temperature = default!; - [Dependency] private readonly SharedTransformSystem _xform = default!; - [Dependency] private readonly VomitSystem _vomit = default!; - [Dependency] private readonly TurfSystem _turf = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnCheckTemperature); - SubscribeLocalEvent>(OnCheckBreathing); - SubscribeLocalEvent>(OnCheckOrganType); - SubscribeLocalEvent>(OnExecutePlantAdjustHealth); - SubscribeLocalEvent>(OnExecutePlantAdjustMutationLevel); - SubscribeLocalEvent>(OnExecutePlantAdjustMutationMod); - SubscribeLocalEvent>(OnExecutePlantAdjustNutrition); - SubscribeLocalEvent>(OnExecutePlantAdjustPests); - SubscribeLocalEvent>(OnExecutePlantAdjustPotency); - SubscribeLocalEvent>(OnExecutePlantAdjustToxins); - SubscribeLocalEvent>(OnExecutePlantAdjustWater); - SubscribeLocalEvent>(OnExecutePlantAdjustWeeds); - SubscribeLocalEvent>(OnExecutePlantAffectGrowth); - SubscribeLocalEvent>(OnExecutePlantChangeStat); - SubscribeLocalEvent>(OnExecutePlantCryoxadone); - SubscribeLocalEvent>(OnExecutePlantDestroySeeds); - SubscribeLocalEvent>(OnExecutePlantDiethylamine); - SubscribeLocalEvent>(OnExecutePlantPhalanximine); - SubscribeLocalEvent>(OnExecutePlantRestoreSeeds); - SubscribeLocalEvent>(OnExecuteRobustHarvest); - SubscribeLocalEvent>(OnExecuteAdjustTemperature); - SubscribeLocalEvent>(OnExecuteAreaReactionEffect); - SubscribeLocalEvent>(OnExecuteCauseZombieInfection); - SubscribeLocalEvent>(OnExecuteChemCleanBloodstream); - SubscribeLocalEvent>(OnExecuteChemVomit); - SubscribeLocalEvent>(OnExecuteCreateEntityReactionEffect); - SubscribeLocalEvent>(OnExecuteCreateGas); - SubscribeLocalEvent>(OnExecuteCureZombieInfection); - SubscribeLocalEvent>(OnExecuteEmote); - SubscribeLocalEvent>(OnExecuteEmpReactionEffect); - SubscribeLocalEvent>(OnExecuteExplosionReactionEffect); - SubscribeLocalEvent>(OnExecuteFlammableReaction); - SubscribeLocalEvent>(OnExecuteFlashReactionEffect); - SubscribeLocalEvent>(OnExecuteIgnite); - SubscribeLocalEvent>(OnExecuteMakeSentient); - SubscribeLocalEvent>(OnExecuteModifyBleedAmount); - SubscribeLocalEvent>(OnExecuteModifyBloodLevel); - SubscribeLocalEvent>(OnExecuteModifyLungGas); - SubscribeLocalEvent>(OnExecuteOxygenate); - SubscribeLocalEvent>(OnExecutePlantMutateChemicals); - SubscribeLocalEvent>(OnExecutePlantMutateConsumeGasses); - SubscribeLocalEvent>(OnExecutePlantMutateExudeGasses); - SubscribeLocalEvent>(OnExecutePlantMutateHarvest); - SubscribeLocalEvent>(OnExecutePlantSpeciesChange); - SubscribeLocalEvent>(OnExecutePolymorph); - SubscribeLocalEvent>(OnExecuteResetNarcolepsy); - } - - private void OnCheckTemperature(ref CheckEntityEffectConditionEvent args) - { - args.Result = false; - if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp)) - { - if (temp.CurrentTemperature >= args.Condition.Min && temp.CurrentTemperature <= args.Condition.Max) - args.Result = true; - } - } - - private void OnCheckBreathing(ref CheckEntityEffectConditionEvent args) - { - if (!TryComp(args.Args.TargetEntity, out RespiratorComponent? respiratorComp)) - { - args.Result = !args.Condition.IsBreathing; - return; - } - - var breathingState = _respirator.IsBreathing((args.Args.TargetEntity, respiratorComp)); - args.Result = args.Condition.IsBreathing == breathingState; - } - - private void OnCheckOrganType(ref CheckEntityEffectConditionEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.OrganEntity == null) - { - args.Result = false; - return; - } - - args.Result = OrganCondition(args.Condition, reagentArgs.OrganEntity.Value); - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public bool OrganCondition(OrganType condition, Entity metabolizer) - { - metabolizer.Comp ??= EntityManager.GetComponentOrNull(metabolizer.Owner); - if (metabolizer.Comp != null - && metabolizer.Comp.MetabolizerTypes != null - && metabolizer.Comp.MetabolizerTypes.Contains(condition.Type)) - return condition.ShouldHave; - return !condition.ShouldHave; - } - - /// - /// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default. - /// - /// The entity holding the plant - /// The plant holder component - /// The entity manager - /// Whether to check if it has an alive plant or not - /// - private bool CanMetabolizePlant(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent, - bool mustHaveAlivePlant = true, bool mustHaveMutableSeed = false) - { - plantHolderComponent = null; - - if (!TryComp(plantHolder, out plantHolderComponent)) - return false; - - if (mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead)) - return false; - - if (mustHaveMutableSeed && (plantHolderComponent.Seed == null || plantHolderComponent.Seed.Immutable)) - return false; - - return true; - } - - private void OnExecutePlantAdjustHealth(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.Health += args.Effect.Amount; - _plantHolder.CheckHealth(args.Args.TargetEntity, plantHolderComp); - } - - private void OnExecutePlantAdjustMutationLevel(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.MutationLevel += args.Effect.Amount * plantHolderComp.MutationMod; - } - - private void OnExecutePlantAdjustMutationMod(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.MutationMod += args.Effect.Amount; - } - - private void OnExecutePlantAdjustNutrition(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false)) - return; - - _plantHolder.AdjustNutrient(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp); - } - - private void OnExecutePlantAdjustPests(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.PestLevel += args.Effect.Amount; - } - - private void OnExecutePlantAdjustPotency(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Potency = Math.Max(plantHolderComp.Seed.Potency + args.Effect.Amount, 1); - } - - private void OnExecutePlantAdjustToxins(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.Toxins += args.Effect.Amount; - } - - private void OnExecutePlantAdjustWater(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false)) - return; - - _plantHolder.AdjustWater(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp); - } - - private void OnExecutePlantAdjustWeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - plantHolderComp.WeedLevel += args.Effect.Amount; - } - - private void OnExecutePlantAffectGrowth(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - _plantHolder.AffectGrowth(args.Args.TargetEntity, (int) args.Effect.Amount, plantHolderComp); - } - - // Mutate reference 'val' between 'min' and 'max' by pretending the value - // is representable by a thermometer code with 'bits' number of bits and - // randomly flipping some of them. - private void MutateFloat(ref float val, float min, float max, int bits) - { - if (min == max) - { - val = min; - return; - } - - // Starting number of bits that are high, between 0 and bits. - // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); - // val may be outside the range of min/max due to starting prototype values, so clamp. - valInt = Math.Clamp(valInt, 0, bits); - - // Probability that the bit flip increases n. - // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it. - // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; - int valIntMutated; - if (_random.Prob(probIncrease)) - { - valIntMutated = valInt + 1; - } - else - { - valIntMutated = valInt - 1; - } - - // Set value based on mutated thermometer code. - float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); - val = valMutated; - } - - private void MutateInt(ref int val, int min, int max, int bits) - { - if (min == max) - { - val = min; - return; - } - - // Starting number of bits that are high, between 0 and bits. - // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); - // val may be outside the range of min/max due to starting prototype values, so clamp. - valInt = Math.Clamp(valInt, 0, bits); - - // Probability that the bit flip increases n. - // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it. - // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; - int valMutated; - if (_random.Prob(probIncrease)) - { - valMutated = val + 1; - } - else - { - valMutated = val - 1; - } - - valMutated = Math.Clamp(valMutated, min, max); - val = valMutated; - } - - private void OnExecutePlantChangeStat(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - var member = plantHolderComp.Seed.GetType().GetField(args.Effect.TargetValue); - - if (member == null) - { - _mutation.Log.Error(args.Effect.GetType().Name + " Error: Member " + args.Effect.TargetValue + " not found on " + plantHolderComp.Seed.GetType().Name + ". Did you misspell it?"); - return; - } - - var currentValObj = member.GetValue(plantHolderComp.Seed); - if (currentValObj == null) - return; - - if (member.FieldType == typeof(float)) - { - var floatVal = (float)currentValObj; - MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(plantHolderComp.Seed, floatVal); - } - else if (member.FieldType == typeof(int)) - { - var intVal = (int)currentValObj; - MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(plantHolderComp.Seed, intVal); - } - else if (member.FieldType == typeof(bool)) - { - var boolVal = (bool)currentValObj; - boolVal = !boolVal; - member.SetValue(plantHolderComp.Seed, boolVal); - } - } - - private void OnExecutePlantCryoxadone(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - var deviation = 0; - var seed = plantHolderComp.Seed; - if (seed == null) - return; - if (plantHolderComp.Age > seed.Maturation) - deviation = (int) Math.Max(seed.Maturation - 1, plantHolderComp.Age - _random.Next(7, 10)); - else - deviation = (int) (seed.Maturation / seed.GrowthStages); - plantHolderComp.Age -= deviation; - plantHolderComp.LastProduce = plantHolderComp.Age; - plantHolderComp.SkipAging++; - plantHolderComp.ForceUpdate = true; - } - - private void OnExecutePlantDestroySeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (plantHolderComp.Seed!.Seedless == false) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - _popup.PopupEntity( - Loc.GetString("botany-plant-seedsdestroyed"), - args.Args.TargetEntity, - PopupType.SmallCaution - ); - plantHolderComp.Seed.Seedless = true; - } - } - - private void OnExecutePlantDiethylamine(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed!.Lifespan++; - } - - if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed!.Endurance++; - } - } - - private void OnExecutePlantPhalanximine(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - plantHolderComp.Seed!.Viable = true; - } - - private void OnExecutePlantRestoreSeeds(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true)) - return; - - if (plantHolderComp.Seed!.Seedless) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), args.Args.TargetEntity); - plantHolderComp.Seed.Seedless = false; - } - } - - private void OnExecuteRobustHarvest(ref ExecuteEntityEffectEvent args) - { - if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp)) - return; - - if (plantHolderComp.Seed == null) - return; - - if (plantHolderComp.Seed.Potency < args.Effect.PotencyLimit) - { - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); - - if (plantHolderComp.Seed.Potency > args.Effect.PotencySeedlessThreshold) - { - plantHolderComp.Seed.Seedless = true; - } - } - else if (plantHolderComp.Seed.Yield > 1 && _random.Prob(0.1f)) - { - // Too much of a good thing reduces yield - _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp); - plantHolderComp.Seed.Yield--; - } - } - - private void OnExecuteAdjustTemperature(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp)) - { - var amount = args.Effect.Amount; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - amount *= reagentArgs.Scale.Float(); - } - - _temperature.ChangeHeat(args.Args.TargetEntity, amount, true, temp); - } - } - - private void OnExecuteAreaReactionEffect(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null) - return; - - var spreadAmount = (int) Math.Max(0, Math.Ceiling((reagentArgs.Quantity / args.Effect.OverflowThreshold).Float())); - var splitSolution = reagentArgs.Source.SplitSolution(reagentArgs.Source.Volume); - var transform = Comp(reagentArgs.TargetEntity); - var mapCoords = _xform.GetMapCoordinates(reagentArgs.TargetEntity, xform: transform); - - if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) || - !_map.TryGetTileRef(gridUid, grid, transform.Coordinates, out var tileRef)) - { - return; - } - - if (_spreader.RequiresFloorToSpread(args.Effect.PrototypeId) && _turf.IsSpace(tileRef)) - return; - - var coords = _map.MapToGrid(gridUid, mapCoords); - var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid()); - - _smoke.StartSmoke(ent, splitSolution, args.Effect.Duration, spreadAmount); - - _audio.PlayPvs(args.Effect.Sound, reagentArgs.TargetEntity, AudioParams.Default.WithVariation(0.25f)); - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - private void OnExecuteCauseZombieInfection(ref ExecuteEntityEffectEvent args) - { - EnsureComp(args.Args.TargetEntity); - EnsureComp(args.Args.TargetEntity); - } - - private void OnExecuteChemCleanBloodstream(ref ExecuteEntityEffectEvent args) - { - var cleanseRate = args.Effect.CleanseRate; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null || reagentArgs.Reagent == null) - return; - - cleanseRate *= reagentArgs.Scale.Float(); - _bloodstream.FlushChemicals(args.Args.TargetEntity, reagentArgs.Reagent, cleanseRate); - } - else - { - _bloodstream.FlushChemicals(args.Args.TargetEntity, null, cleanseRate); - } - } - - private void OnExecuteChemVomit(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) - return; - - _vomit.Vomit(args.Args.TargetEntity, args.Effect.ThirstAmount, args.Effect.HungerAmount); - } - - private void OnExecuteCreateEntityReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - var quantity = (int)args.Effect.Number; - if (args.Args is EntityEffectReagentArgs reagentArgs) - quantity *= reagentArgs.Quantity.Int(); - - for (var i = 0; i < quantity; i++) - { - var uid = Spawn(args.Effect.Entity, _xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform)); - _xform.AttachToGridOrMap(uid); - - // TODO figure out how to properly spawn inside of containers - // e.g. cheese: - // if the user is holding a bowl milk & enzyme, should drop to floor, not attached to the user. - // if reaction happens in a backpack, should insert cheese into backpack. - // --> if it doesn't fit, iterate through parent storage until it attaches to the grid (again, DON'T attach to players). - // if the reaction happens INSIDE a stomach? the bloodstream? I have no idea how to handle that. - // presumably having cheese materialize inside of your blood would have "disadvantages". - } - } - - private void OnExecuteCreateGas(ref ExecuteEntityEffectEvent args) - { - var tileMix = _atmosphere.GetContainingMixture(args.Args.TargetEntity, false, true); - - if (tileMix != null) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - tileMix.AdjustMoles(args.Effect.Gas, reagentArgs.Quantity.Float() * args.Effect.Multiplier); - } - else - { - tileMix.AdjustMoles(args.Effect.Gas, args.Effect.Multiplier); - } - } - } - - private void OnExecuteCureZombieInfection(ref ExecuteEntityEffectEvent args) - { - if (HasComp(args.Args.TargetEntity)) - return; - - RemComp(args.Args.TargetEntity); - RemComp(args.Args.TargetEntity); - - if (args.Effect.Innoculate) - { - EnsureComp(args.Args.TargetEntity); - } - } - - private void OnExecuteEmote(ref ExecuteEntityEffectEvent args) - { - if (args.Effect.EmoteId == null) - return; - - if (args.Effect.ShowInChat) - _chat.TryEmoteWithChat(args.Args.TargetEntity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force); - else - _chat.TryEmoteWithoutChat(args.Args.TargetEntity, args.Effect.EmoteId); - } - - private void OnExecuteEmpReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - - var range = args.Effect.EmpRangePerUnit; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - range = MathF.Min((float) (reagentArgs.Quantity * args.Effect.EmpRangePerUnit), args.Effect.EmpMaxRange); - } - - _emp.EmpPulse(_xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform), - range, - args.Effect.EnergyConsumption, - args.Effect.DisableDuration); - } - - private void OnExecuteExplosionReactionEffect(ref ExecuteEntityEffectEvent args) - { - var intensity = args.Effect.IntensityPerUnit; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - intensity = MathF.Min((float) reagentArgs.Quantity * args.Effect.IntensityPerUnit, args.Effect.MaxTotalIntensity); - } - - _explosion.QueueExplosion( - args.Args.TargetEntity, - args.Effect.ExplosionType, - intensity, - args.Effect.IntensitySlope, - args.Effect.MaxIntensity, - args.Effect.TileBreakScale); - } - - private void OnExecuteFlammableReaction(ref ExecuteEntityEffectEvent args) - { - if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable)) - return; - - // Sets the multiplier for FireStacks to MultiplierOnExisting is 0 or greater and target already has FireStacks - var multiplier = flammable.FireStacks != 0f && args.Effect.MultiplierOnExisting >= 0 ? args.Effect.MultiplierOnExisting : args.Effect.Multiplier; - var quantity = 1f; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - quantity = reagentArgs.Quantity.Float(); - _flammable.AdjustFireStacks(args.Args.TargetEntity, quantity * multiplier, flammable); - if (reagentArgs.Reagent != null) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); - } - else - { - _flammable.AdjustFireStacks(args.Args.TargetEntity, multiplier, flammable); - } - } - - private void OnExecuteFlashReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - - var range = 1f; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - range = MathF.Min((float)(reagentArgs.Quantity * args.Effect.RangePerUnit), args.Effect.MaxRange); - - _flash.FlashArea( - args.Args.TargetEntity, - null, - range, - args.Effect.Duration, - slowTo: args.Effect.SlowTo, - sound: args.Effect.Sound); - - if (args.Effect.FlashEffectPrototype == null) - return; - - var uid = EntityManager.SpawnEntity(args.Effect.FlashEffectPrototype, _xform.GetMapCoordinates(transform)); - _xform.AttachToGridOrMap(uid); - - if (!TryComp(uid, out var pointLightComp)) - return; - - _pointLight.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp); - } - - private void OnExecuteIgnite(ref ExecuteEntityEffectEvent args) - { - if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable)) - return; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - _flammable.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity, flammable: flammable); - } - else - { - _flammable.Ignite(args.Args.TargetEntity, args.Args.TargetEntity, flammable: flammable); - } - } - - private void OnExecuteMakeSentient(ref ExecuteEntityEffectEvent args) - { - var uid = args.Args.TargetEntity; - - // Let affected entities speak normally to make this effect different from, say, the "random sentience" event - // This also works on entities that already have a mind - // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect - RemComp(uid); - RemComp(uid); - - // Stops from adding a ghost role to things like people who already have a mind - if (TryComp(uid, out var mindContainer) && mindContainer.HasMind) - { - return; - } - - // Don't add a ghost role to things that already have ghost roles - if (TryComp(uid, out GhostRoleComponent? ghostRole)) - { - return; - } - - ghostRole = AddComp(uid); - EnsureComp(uid); - - var entityData = Comp(uid); - ghostRole.RoleName = entityData.EntityName; - ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description"); - } - - private void OnExecuteModifyBleedAmount(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out var blood)) - { - var amt = args.Effect.Amount; - if (args.Args is EntityEffectReagentArgs reagentArgs) { - if (args.Effect.Scaled) - amt *= reagentArgs.Quantity.Float(); - amt *= reagentArgs.Scale.Float(); - } - - _bloodstream.TryModifyBleedAmount((args.Args.TargetEntity, blood), amt); - } - } - - private void OnExecuteModifyBloodLevel(ref ExecuteEntityEffectEvent args) - { - if (TryComp(args.Args.TargetEntity, out var blood)) - { - var amt = args.Effect.Amount; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (args.Effect.Scaled) - amt *= reagentArgs.Quantity; - amt *= reagentArgs.Scale; - } - - _bloodstream.TryModifyBloodLevel((args.Args.TargetEntity, blood), amt); - } - } - - private void OnExecuteModifyLungGas(ref ExecuteEntityEffectEvent args) - { - LungComponent? lung; - float amount = 1f; - - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - if (!TryComp(reagentArgs.OrganEntity, out var organLung)) - return; - lung = organLung; - amount = reagentArgs.Quantity.Float(); - } - else - { - if (!TryComp(args.Args.TargetEntity, out var organLung)) //Likely needs to be modified to ensure it works correctly - return; - lung = organLung; - } - - if (lung != null) - { - foreach (var (gas, ratio) in args.Effect.Ratios) - { - var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier; - if (quantity < 0) - quantity = Math.Max(quantity, -lung.Air[(int) gas]); - lung.Air.AdjustMoles(gas, quantity); - } - } - } - - private void OnExecuteOxygenate(ref ExecuteEntityEffectEvent args) - { - var multiplier = 1f; - if (args.Args is EntityEffectReagentArgs reagentArgs) - { - multiplier = reagentArgs.Quantity.Float(); - } - - if (TryComp(args.Args.TargetEntity, out var resp)) - { - _respirator.UpdateSaturation(args.Args.TargetEntity, multiplier * args.Effect.Factor, resp); - } - } - - private void OnExecutePlantMutateChemicals(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var chemicals = plantholder.Seed.Chemicals; - var randomChems = _protoManager.Index(RandomPickBotanyReagent).Fills; - - // Add a random amount of a random chemical to this set of chemicals - if (randomChems != null) - { - var pick = _random.Pick(randomChems); - var chemicalId = _random.Pick(pick.Reagents); - var amount = _random.Next(1, (int)pick.Quantity); - var seedChemQuantity = new SeedChemQuantity(); - if (chemicals.ContainsKey(chemicalId)) - { - seedChemQuantity.Min = chemicals[chemicalId].Min; - seedChemQuantity.Max = chemicals[chemicalId].Max + amount; - } - else - { - seedChemQuantity.Min = 1; - seedChemQuantity.Max = 1 + amount; - seedChemQuantity.Inherent = false; - } - var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max); - seedChemQuantity.PotencyDivisor = potencyDivisor; - chemicals[chemicalId] = seedChemQuantity; - } - } - - private void OnExecutePlantMutateConsumeGasses(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var gasses = plantholder.Seed.ConsumeGasses; - - // Add a random amount of a random gas to this gas dictionary - float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); - if (gasses.ContainsKey(gas)) - { - gasses[gas] += amount; - } - else - { - gasses.Add(gas, amount); - } - } - - private void OnExecutePlantMutateExudeGasses(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - var gasses = plantholder.Seed.ExudeGasses; - - // Add a random amount of a random gas to this gas dictionary - float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); - if (gasses.ContainsKey(gas)) - { - gasses[gas] += amount; - } - else - { - gasses.Add(gas, amount); - } - } - - private void OnExecutePlantMutateHarvest(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - - if (plantholder.Seed == null) - return; - - if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat) - plantholder.Seed.HarvestRepeat = HarvestType.Repeat; - else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat) - plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest; - } - - private void OnExecutePlantSpeciesChange(ref ExecuteEntityEffectEvent args) - { - var plantholder = Comp(args.Args.TargetEntity); - if (plantholder.Seed == null) - return; - - if (plantholder.Seed.MutationPrototypes.Count == 0) - return; - - var targetProto = _random.Pick(plantholder.Seed.MutationPrototypes); - if (!_protoManager.TryIndex(targetProto, out SeedPrototype? protoSeed)) - { - Log.Error($"Seed prototype could not be found: {targetProto}!"); - return; - } - - plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed); - } - - private void OnExecutePolymorph(ref ExecuteEntityEffectEvent args) - { - // Make it into a prototype - EnsureComp(args.Args.TargetEntity); - _polymorph.PolymorphEntity(args.Args.TargetEntity, args.Effect.PolymorphPrototype); - } - - private void OnExecuteResetNarcolepsy(ref ExecuteEntityEffectEvent args) - { - if (args.Args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) - return; - - _narcolepsy.AdjustNarcolepsyTimer(args.Args.TargetEntity, args.Effect.TimerReset); - } -} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index fb87167f3c6..7e041163856 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -1,4 +1,4 @@ -using Content.Server.Imperial.Medieval.Achievements.Jobs; +using System.Threading.Tasks; using Content.Server.Acz; using Content.Server.Administration; using Content.Server.Administration.Logs; @@ -9,10 +9,10 @@ using Content.Server.Database; using Content.Server.Discord.DiscordLink; using Content.Server.EUI; +using Content.Server.FeedbackSystem; using Content.Server.GameTicking; using Content.Server.GhostKick; using Content.Server.GuideGenerator; -using Content.Server.Imperial.Medieval.JoinQueue; using Content.Server.Imperial.Entry; using Content.Server.Info; using Content.Server.IoC; @@ -26,6 +26,7 @@ using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; using Content.Shared.CCVar; +using Content.Shared.FeedbackSystem; using Content.Shared.Kitchen; using Content.Shared.Localizations; using Robust.Server; @@ -33,6 +34,7 @@ using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -43,134 +45,141 @@ public sealed class EntryPoint : GameServer internal const string ConfigPresetsDir = "/ConfigPresets/"; private const string ConfigPresetsDirBuild = $"{ConfigPresetsDir}Build/"; - private EuiManager _euiManager = default!; - private IVoteManager _voteManager = default!; - private ServerUpdateManager _updateManager = default!; - private PlayTimeTrackingManager? _playTimeTracking; - private IEntitySystemManager? _sysMan; - private IServerDbManager? _dbManager; - private IWatchlistWebhookManager _watchlistWebhookManager = default!; - private IConnectionManager? _connectionManager; + [Dependency] private readonly CVarControlManager _cvarCtrl = default!; + [Dependency] private readonly ContentLocalizationManager _loc = default!; + [Dependency] private readonly ContentNetworkResourceManager _netResMan = default!; + [Dependency] private readonly DiscordChatLink _discordChatLink = default!; + [Dependency] private readonly DiscordLink _discordLink = default!; + [Dependency] private readonly EuiManager _euiManager = default!; + [Dependency] private readonly GhostKickManager _ghostKick = default!; + [Dependency] private readonly IAdminManager _admin = default!; + [Dependency] private readonly IAdminLogManager _adminLog = default!; + [Dependency] private readonly IAfkManager _afk = default!; + [Dependency] private readonly IBanManager _ban = default!; + [Dependency] private readonly IChatManager _chatSan = default!; + [Dependency] private readonly IChatSanitizationManager _chat = default!; + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IConnectionManager _connection = default!; + [Dependency] private readonly IEntitySystemManager _entSys = default!; + [Dependency] private readonly IGameMapManager _gameMap = default!; + [Dependency] private readonly ILogManager _log = default!; + [Dependency] private readonly INodeGroupFactory _nodeFactory = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IResourceManager _res = default!; + [Dependency] private readonly IServerDbManager _dbManager = default!; + [Dependency] private readonly IServerPreferencesManager _preferences = default!; + [Dependency] private readonly IStatusHost _host = default!; + [Dependency] private readonly IVoteManager _voteManager = default!; + [Dependency] private readonly IWatchlistWebhookManager _watchlistWebhookManager = default!; + [Dependency] private readonly JobWhitelistManager _job = default!; + [Dependency] private readonly MultiServerKickManager _multiServerKick = default!; + [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; + [Dependency] private readonly PlayerRateLimitManager _rateLimit = default!; + [Dependency] private readonly RecipeManager _recipe = default!; + [Dependency] private readonly RulesManager _rules = default!; + [Dependency] private readonly ServerApi _serverApi = default!; + [Dependency] private readonly ServerInfoManager _serverInfo = default!; + [Dependency] private readonly ServerUpdateManager _updateManager = default!; + [Dependency] private readonly ServerFeedbackManager _feedbackManager = null!; + + public override void PreInit() + { + ServerContentIoC.Register(Dependencies); + foreach (var callback in TestingCallbacks) + { + var cast = (ServerModuleTestingCallbacks)callback; + cast.ServerBeforeIoC?.Invoke(); + } + + Dependencies.Resolve().FloatFlags = SerializerFloatFlags.RemoveReadNan; + } /// public override void Init() { base.Init(); + Dependencies.BuildGraph(); + Dependencies.InjectDependencies(this); - var cfg = IoCManager.Resolve(); - var res = IoCManager.Resolve(); - var logManager = IoCManager.Resolve(); + LoadConfigPresets(_cfg, _res, _log.GetSawmill("configpreset")); - LoadConfigPresets(cfg, res, logManager.GetSawmill("configpreset")); + var aczProvider = new ContentMagicAczProvider(Dependencies); + _host.SetMagicAczProvider(aczProvider); - var aczProvider = new ContentMagicAczProvider(IoCManager.Resolve()); - IoCManager.Resolve().SetMagicAczProvider(aczProvider); + _factory.DoAutoRegistrations(); + _factory.IgnoreMissingComponents("Visuals"); + _factory.RegisterIgnore(IgnoredComponents.List); + _factory.GenerateNetIds(); - var factory = IoCManager.Resolve(); - var prototypes = IoCManager.Resolve(); + _proto.RegisterIgnore("parallax"); - factory.DoAutoRegistrations(); - factory.IgnoreMissingComponents("Visuals"); - - factory.RegisterIgnore(IgnoredComponents.List); - - prototypes.RegisterIgnore("parallax"); - - ServerContentIoC.Register(); - - foreach (var callback in TestingCallbacks) - { - var cast = (ServerModuleTestingCallbacks) callback; - cast.ServerBeforeIoC?.Invoke(); - } + _loc.Initialize(); - IoCManager.BuildGraph(); - factory.GenerateNetIds(); - var configManager = IoCManager.Resolve(); - var dest = configManager.GetCVar(CCVars.DestinationFile); - IoCManager.Resolve().Initialize(); - if (string.IsNullOrEmpty(dest)) //hacky but it keeps load times for the generator down. - { - _euiManager = IoCManager.Resolve(); - _voteManager = IoCManager.Resolve(); - _updateManager = IoCManager.Resolve(); - _playTimeTracking = IoCManager.Resolve(); - _connectionManager = IoCManager.Resolve(); - _sysMan = IoCManager.Resolve(); - _dbManager = IoCManager.Resolve(); - _watchlistWebhookManager = IoCManager.Resolve(); - - logManager.GetSawmill("Storage").Level = LogLevel.Info; - logManager.GetSawmill("db.ef").Level = LogLevel.Info; - - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - _dbManager.Init(); - IoCManager.Resolve().Init(); // Imperial Medieval Flavor Images - IoCManager.Resolve().Init(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); // Imperial-Medieval-JoinQueue - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - - _voteManager.Initialize(); - _updateManager.Initialize(); - _playTimeTracking.Initialize(); - _watchlistWebhookManager.Initialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - - // Imperial EntryPoint Start - ImperialEntry.Init(); // Imperial Space - IoCManager.Resolve().Initialize(); // Imperial AlwaysPvs - IoCManager.Resolve().Initialize(); - // Imperial EntryPoint End - } + var dest = _cfg.GetCVar(CCVars.DestinationFile); + if (!string.IsNullOrEmpty(dest)) + return; //hacky but it keeps load times for the generator down. + + _log.GetSawmill("Storage").Level = LogLevel.Info; + _log.GetSawmill("db.ef").Level = LogLevel.Info; + + _adminLog.Initialize(); + _connection.Initialize(); + _dbManager.Init(); + _preferences.Init(); + _nodeFactory.Initialize(); + _netResMan.Initialize(); + _ghostKick.Initialize(); + _serverInfo.Initialize(); + _serverApi.Initialize(); + _voteManager.Initialize(); + _updateManager.Initialize(); + _playTimeTracking.Initialize(); + _watchlistWebhookManager.Initialize(); + _job.Initialize(); + _rateLimit.Initialize(); + + // Imperial EntryPoint Start + ImperialEntry.Init(); // Imperial Space / Medieval managers + // Imperial EntryPoint End } public override void PostInit() { base.PostInit(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - var configManager = IoCManager.Resolve(); - var resourceManager = IoCManager.Resolve(); - var dest = configManager.GetCVar(CCVars.DestinationFile); + _chatSan.Initialize(); + _chat.Initialize(); + var dest = _cfg.GetCVar(CCVars.DestinationFile); if (!string.IsNullOrEmpty(dest)) { var resPath = new ResPath(dest).ToRootedPath(); - var file = resourceManager.UserData.OpenWriteText(resPath.WithName("chem_" + dest)); + var file = _res.UserData.OpenWriteText(resPath.WithName("chem_" + dest)); ChemistryJsonGenerator.PublishJson(file); file.Flush(); - file = resourceManager.UserData.OpenWriteText(resPath.WithName("react_" + dest)); + file = _res.UserData.OpenWriteText(resPath.WithName("react_" + dest)); ReactionJsonGenerator.PublishJson(file); file.Flush(); - IoCManager.Resolve().Shutdown("Data generation done"); + Dependencies.Resolve().Shutdown("Data generation done"); + return; } - else - { - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - _euiManager.Initialize(); - - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().GetEntitySystem().PostInitialize(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().PostInit(); - IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); - - ImperialEntry.PostInit(); - } + _recipe.Initialize(); + _admin.Initialize(); + _afk.Initialize(); + _rules.Initialize(); + _discordLink.Initialize(); + _discordChatLink.Initialize(); + _euiManager.Initialize(); + _gameMap.Initialize(); + _entSys.GetEntitySystem().PostInitialize(); + _ban.Initialize(); + _connection.PostInit(); + _multiServerKick.Initialize(); + _cvarCtrl.Initialize(); + _feedbackManager.Initialize(); + + ImperialEntry.PostInit(); // Imperial Space / Medieval } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) @@ -188,21 +197,27 @@ public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) case ModUpdateLevel.FramePostEngine: _updateManager.Update(); - _playTimeTracking?.Update(); + _playTimeTracking.Update(); _watchlistWebhookManager.Update(); - _connectionManager?.Update(); + _connection.Update(); break; } } protected override void Dispose(bool disposing) { - _playTimeTracking?.Shutdown(); - _dbManager?.Shutdown(); - IoCManager.Resolve().Shutdown(); + var dest = _cfg.GetCVar(CCVars.DestinationFile); + if (!string.IsNullOrEmpty(dest)) + { + _playTimeTracking.Shutdown(); + _dbManager.Shutdown(); + } + + _serverApi.Shutdown(); - IoCManager.Resolve().Shutdown(); - IoCManager.Resolve().Shutdown(); + // We don't care when or how this finishes, just spin the task off into the void. + _ = _discordLink.Shutdown(); + _discordChatLink.Shutdown(); } private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill) diff --git a/Content.Server/ExCable/ExCableNodeGroup.cs b/Content.Server/ExCable/ExCableNodeGroup.cs new file mode 100644 index 00000000000..b505fbad944 --- /dev/null +++ b/Content.Server/ExCable/ExCableNodeGroup.cs @@ -0,0 +1,14 @@ +using Content.Server.NodeContainer.NodeGroups; +using Content.Shared.NodeContainer.NodeGroups; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; + +namespace Content.Server.ExCable; + +/// +/// Dummy Node group class for handling the explosive cables. +/// +[NodeGroup(NodeGroupID.ExCable)] +public sealed class ExCableNodeGroup : BaseNodeGroup +{ +} diff --git a/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs b/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs new file mode 100644 index 00000000000..68f576dcb9e --- /dev/null +++ b/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs @@ -0,0 +1,100 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.FixedPoint; +using Robust.Shared.Map.Components; +using Robust.Shared.Utility; + +namespace Content.Server.Explosion.Components; + +/// +/// Stores data for airtight explosion traversal on a entity. +/// +/// +[RegisterComponent] +[Access(typeof(ExplosionSystem), Other = AccessPermissions.None)] +public sealed partial class ExplosionAirtightGridComponent : Component +{ + /// + /// Data for every tile on the current grid. + /// + /// + /// Intentionally not saved. + /// + [ViewVariables] + public readonly Dictionary Tiles = new(); + + /// + /// Data struct that describes the explosion-blocking airtight entities on a tile. + /// + public struct TileData + { + /// + /// Which index into the tolerance cache of this tile is using. + /// + public required int ToleranceCacheIndex; + + /// + /// Which directions this tile is blocking explosions in. Bitflag field. + /// + public required AtmosDirection BlockedDirections; + } + + /// + /// A set of tolerance values + /// + public struct ToleranceValues : IEquatable + { + /// + /// Special value that indicates the entity is "invulnerable" against a specific explosion type. + /// + /// + /// Here to deal with the limited range of over typical floats. + /// + public static readonly FixedPoint2 Invulnerable = FixedPoint2.MaxValue; + + /// + /// The intensities at which explosions of each type can instantly break through an entity. + /// + /// + /// + /// This is an array, with the index of each value corresponding to the "explosion type ID" cached by + /// . + /// + /// + /// Values are stored as to avoid possible precision issues resulting in + /// different-but-almost-identical tolerance values wasting memory. + /// + /// + /// If a value is , that indicates the tile is invulnerable. + /// + /// + public required FixedPoint2[] Values; + + public bool Equals(ToleranceValues other) + { + return Values.AsSpan().SequenceEqual(other.Values); + } + + public override bool Equals(object? obj) + { + return obj is ToleranceValues other && Equals(other); + } + + public override int GetHashCode() + { + var hc = new HashCode(); + hc.AddArray(Values); + return hc.ToHashCode(); + } + + public static bool operator ==(ToleranceValues left, ToleranceValues right) + { + return left.Equals(right); + } + + public static bool operator !=(ToleranceValues left, ToleranceValues right) + { + return !left.Equals(right); + } + } +} diff --git a/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs b/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs index da3ce635af0..0274979c556 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs @@ -1,7 +1,8 @@ using System.Numerics; using Content.Shared.Atmos; -using Robust.Shared.Map; +using Content.Shared.FixedPoint; using Robust.Shared.Map.Components; +using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent; using static Content.Server.Explosion.EntitySystems.ExplosionSystem; namespace Content.Server.Explosion.EntitySystems; @@ -11,6 +12,8 @@ namespace Content.Server.Explosion.EntitySystems; /// public sealed class ExplosionGridTileFlood : ExplosionTileFlood { + private readonly ExplosionSystem _explosionSystem; + public Entity Grid; private bool _needToTransform = false; @@ -45,7 +48,8 @@ public ExplosionGridTileFlood( Dictionary edgeTiles, EntityUid? referenceGrid, Matrix3x2 spaceMatrix, - Angle spaceAngle) + Angle spaceAngle, + ExplosionSystem explosionSystem) { Grid = grid; _airtightMap = airtightMap; @@ -53,6 +57,7 @@ public ExplosionGridTileFlood( _intensityStepSize = intensityStepSize; _typeIndex = typeIndex; _edgeTiles = edgeTiles; + _explosionSystem = explosionSystem; // initialise SpaceTiles foreach (var (tile, spaceNeighbors) in _edgeTiles) @@ -193,11 +198,11 @@ protected override void ProcessNewTile(int iteration, Vector2i tile, AtmosDirect NewBlockedTiles.Add(tile); // At what explosion iteration would this blocker be destroyed? - var required = tileData.ExplosionTolerance[_typeIndex]; + var required = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex]; if (required > _maxIntensity) return; // blocker is never destroyed. - var clearIteration = iteration + (int) MathF.Ceiling(required / _intensityStepSize); + var clearIteration = iteration + (int) MathF.Ceiling((float)required / _intensityStepSize); if (FreedTileLists.TryGetValue(clearIteration, out var list)) list.Add(tile); else @@ -261,13 +266,13 @@ private void AddNewAdjacentTiles(int iteration, IEnumerable tiles, boo foreach (var tile in tiles) { var blockedDirections = AtmosDirection.Invalid; - float sealIntegrity = 0; + FixedPoint2 sealIntegrity = 0; // Note that if (grid, tile) is not a valid key, then airtight.BlockedDirections will default to 0 (no blocked directions) if (_airtightMap.TryGetValue(tile, out var tileData)) { blockedDirections = tileData.BlockedDirections; - sealIntegrity = tileData.ExplosionTolerance[_typeIndex]; + sealIntegrity = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex]; } // First, yield any neighboring tiles that are not blocked by airtight entities on this tile @@ -290,7 +295,7 @@ private void AddNewAdjacentTiles(int iteration, IEnumerable tiles, boo continue; // At what explosion iteration would this blocker be destroyed? - var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize); + var clearIteration = iteration + (int) MathF.Ceiling((float) sealIntegrity / _intensityStepSize); // Get the delayed neighbours list if (!_delayedNeighbors.TryGetValue(clearIteration, out var list)) diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs index af004e112e9..4428c8eb0eb 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs @@ -1,28 +1,43 @@ +using System.Linq; +using System.Runtime.InteropServices; using Content.Server.Atmos.Components; -using Content.Server.Destructible; +using Content.Server.Explosion.Components; using Content.Shared.Atmos; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Explosion; -using Content.Shared.Explosion.EntitySystems; using Content.Shared.FixedPoint; +using Robust.Shared.Collections; using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent; namespace Content.Server.Explosion.EntitySystems; public sealed partial class ExplosionSystem { - [Dependency] private readonly DestructibleSystem _destructibleSystem = default!; + // We keep track of which tiles are airtight, and how much damage from explosions those airtight blockers can take. + // This is quite complicated, as the data effectively needs to be tracked *per tile*, *per explosion type*. + // To avoid wasting significant memory, we calculate the values and share the actual backing storage of it. + // Stored values are reference counted so they can be evicted when no longer needed. + // At the time of writing, this compacts the storage for Box Station from ~5500 tolerance value sets to 13, + // at round start. - private readonly Dictionary _explosionTypes = new(); + // Use integers instead of prototype IDs for storage of explosion data. + // This allows us to replace a Dictionary with just a FixedPoint2[]. + private readonly Dictionary, int> _explosionTypes = new(); + // Index to look up if we already have an existing set of tolerance values stored, so the data can be shared. + private readonly Dictionary _toleranceIndex = new(); + // Storage for tolerance values. Entries form a free linked list when not occupied by a set of real values. + private ValueList _toleranceData; + // First free position in _toleranceData. + // -1 indicates there are no free slots left and the storage must be expanded. + private int _freeListHead = -1; private void InitAirtightMap() { - // Currently explosion prototype hot-reload isn't supported, as it would involve completely re-computing the - // airtight map. Could be done, just not yet implemented. + _explosionTypes.Clear(); - // for storing airtight entity damage thresholds for all anchored airtight entities, we will use integers in - // place of id-strings. This initializes the string <--> id association. - // This allows us to replace a Dictionary with just a float[]. int index = 0; foreach (var prototype in _prototypeManager.EnumeratePrototypes()) { @@ -31,21 +46,25 @@ private void InitAirtightMap() } } - // The explosion intensity required to break an entity depends on the explosion type. So it is stored in a - // Dictionary - // - // Hence, each tile has a tuple (Dictionary, AtmosDirection). This specifies what directions are - // blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that - // tile. This is the TileData struct. - // - // We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid - // indices to this tile-data struct. - private Dictionary> _airtightMap = new(); - - public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? grid = null, EntityQuery? query = null) + private void ReloadExplosionPrototypes(PrototypesReloadedEventArgs prototypesReloadedEventArgs) + { + if (!prototypesReloadedEventArgs.Modified.Contains(typeof(ExplosionPrototype))) + return; + + InitAirtightMap(); + ReloadMap(); + } + + public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? grid = null) { if (Resolve(gridId, ref grid, false)) - UpdateAirtightMap(gridId, grid, tile, query); + UpdateAirtightMap(gridId, grid, tile); + } + + [Access(typeof(ExplosionGridTileFlood))] + public ToleranceValues GetToleranceValues(int idx) + { + return _toleranceData[idx].Values; } /// @@ -58,36 +77,114 @@ public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? /// something like a normal and a reinforced windoor on the same tile. But given that this is a pretty rare /// occurrence, I am fine with this. /// - public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile, EntityQuery? query = null) + public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile) { - var tolerance = new float[_explosionTypes.Count]; - var blockedDirections = AtmosDirection.Invalid; + var airtightGrid = EnsureComp(gridId); + + // Calculate tile new airtight state. - if (!_airtightMap.ContainsKey(gridId)) - _airtightMap[gridId] = new(); + var tolerance = new FixedPoint2[_explosionTypes.Count]; + var blockedDirections = AtmosDirection.Invalid; - query ??= GetEntityQuery(); - var damageQuery = GetEntityQuery(); - var destructibleQuery = GetEntityQuery(); - var anchoredEnumerator = _mapSystem.GetAnchoredEntitiesEnumerator(gridId, grid, tile); + var anchoredEnumerator = _map.GetAnchoredEntitiesEnumerator(gridId, grid, tile); while (anchoredEnumerator.MoveNext(out var uid)) { - if (!query.Value.TryGetComponent(uid, out var airtight) || !airtight.AirBlocked) + if (!_airtightQuery.TryGetComponent(uid, out var airtight) || !airtight.AirBlocked) continue; blockedDirections |= airtight.AirBlockedDirection; - var entityTolerances = GetExplosionTolerance(uid.Value, damageQuery, destructibleQuery); - for (var i = 0; i < tolerance.Length; i++) + GetExplosionTolerance(uid.Value, tolerance); + } + + // Log.Info($"UPDATE {gridId}/{tile}: {blockedDirections}"); + + if (blockedDirections == AtmosDirection.Invalid) + { + // No longer airtight + + if (!airtightGrid.Tiles.Remove(tile, out var tileData)) { - tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]); + // Did not have this tile before and after, nothing to do. + return; } + + // Removing tile data. + DecrementRefCount(tileData.ToleranceCacheIndex); + return; + } + + ref var tileEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(airtightGrid.Tiles, tile, out var existed); + var cacheKey = new ToleranceValues { Values = tolerance }; + + // Remove previous tolerance reference if necessary. + if (existed) + { + ref var prevEntry = ref _toleranceData[tileEntry.ToleranceCacheIndex]; + if (prevEntry.Values == cacheKey) + { + // No change. + return; + } + + DecrementRefCount(tileEntry.ToleranceCacheIndex); } - if (blockedDirections != AtmosDirection.Invalid) - _airtightMap[gridId][tile] = new(tolerance, blockedDirections); + ref var newCacheIndex = ref CollectionsMarshal.GetValueRefOrAddDefault(_toleranceIndex, cacheKey, out existed); + if (existed) + { + _toleranceData[newCacheIndex].RefCount += 1; + } else - _airtightMap[gridId].Remove(tile); + { + if (_freeListHead < 0) + ExpandCache(); + + newCacheIndex = _freeListHead; + ref var newCacheEntry = ref _toleranceData[newCacheIndex]; + _freeListHead = newCacheEntry.RefCount; + + newCacheEntry.Values = cacheKey; + newCacheEntry.RefCount = 1; + } + + tileEntry = new TileData + { + BlockedDirections = blockedDirections, + ToleranceCacheIndex = newCacheIndex, + }; + } + + private void ExpandCache() + { + var newCacheSize = Math.Max(8, _toleranceData.Count * 2); + var curSize = _toleranceData.Count; + + _toleranceData.EnsureLength(newCacheSize); + for (var i = curSize; i < newCacheSize; i++) + { + _toleranceData[i].RefCount = _freeListHead; + _freeListHead = i; + } + } + + private void DecrementRefCount(int index) + { + ref var cacheEntry = ref _toleranceData[index]; + + DebugTools.Assert(cacheEntry.RefCount > 0); + cacheEntry.RefCount -= 1; + + if (cacheEntry.RefCount == 0) + { + var prevValue = cacheEntry.Values; + cacheEntry.Values = default; + cacheEntry.RefCount = _freeListHead; + _freeListHead = index; + + var result = _toleranceIndex.Remove(prevValue); + DebugTools.Assert(result, "Failed to removed 0 refcounted index!"); + } } /// @@ -105,78 +202,100 @@ private void OnAirtightDamaged(EntityUid uid, AirtightComponent airtight, Damage if (!TryComp(transform.GridUid, out var grid)) return; - UpdateAirtightMap(transform.GridUid.Value, grid, _mapSystem.CoordinatesToTile(transform.GridUid.Value, grid, transform.Coordinates)); + UpdateAirtightMap(transform.GridUid.Value, grid, _map.CoordinatesToTile(transform.GridUid.Value, grid, transform.Coordinates)); } /// /// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity. /// - public float[] GetExplosionTolerance( - EntityUid uid, - EntityQuery damageQuery, - EntityQuery destructibleQuery) + private void GetExplosionTolerance(EntityUid uid, Span explosionTolerance) { // How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES // that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes // are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously. var totalDamageTarget = FixedPoint2.MaxValue; - if (destructibleQuery.TryGetComponent(uid, out var destructible)) + if (_destructibleQuery.TryGetComponent(uid, out var destructible)) { totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible); } - var explosionTolerance = new float[_explosionTypes.Count]; - if (totalDamageTarget == FixedPoint2.MaxValue || !damageQuery.TryGetComponent(uid, out var damageable)) + if (totalDamageTarget == FixedPoint2.MaxValue || !_damageableQuery.TryGetComponent(uid, out var damageable)) { for (var i = 0; i < explosionTolerance.Length; i++) { - explosionTolerance[i] = float.MaxValue; + explosionTolerance[i] = ToleranceValues.Invulnerable; } - return explosionTolerance; + + return; } // What multiple of each explosion type damage set will result in the damage exceeding the required amount? This // does not support entities dynamically changing explosive resistances (e.g. via clothing). But these probably // shouldn't be airtight structures anyways.... + var mod = _damageableSystem.UniversalAllDamageModifier * _damageableSystem.UniversalExplosionDamageModifier; foreach (var (id, index) in _explosionTypes) { - if (!_prototypeManager.TryIndex(id, out var explosionType)) + // TODO EXPLOSION SYSTEM + // cache explosion type damage. + if (!_prototypeManager.Resolve(id, out ExplosionPrototype? explosionType)) continue; // evaluate the damage that this damage type would do to this entity var damagePerIntensity = FixedPoint2.Zero; foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict) { - if (!damageable.Damage.DamageDict.ContainsKey(type)) + if (!_damageableSystem.CanBeDamagedBy((uid, damageable), type)) continue; + // TODO EXPLOSION SYSTEM + // add a variant of the event that gets raised once, instead of once per prototype. + // Or better yet, just calculate this manually w/o the event. + // The event mainly exists for indirect resistances via things like inventory & clothing + // But this shouldn't matter for airtight entities. var ev = new GetExplosionResistanceEvent(explosionType.ID); RaiseLocalEvent(uid, ref ev); - damagePerIntensity += value * Math.Max(0, ev.DamageCoefficient); + damagePerIntensity += value * mod * Math.Max(0, ev.DamageCoefficient); } - explosionTolerance[index] = damagePerIntensity > 0 - ? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity) - : float.MaxValue; + var toleranceValue = damagePerIntensity > 0 + ? (float) ((totalDamageTarget - _damageableSystem.GetTotalDamage((uid, damageable))) / damagePerIntensity) + : ToleranceValues.Invulnerable; + + explosionTolerance[index] = toleranceValue; + } + } + + private void OnAirtightGridRemoved(EntityUid entity) + { + if (!TryComp(entity, out ExplosionAirtightGridComponent? airtightGrid)) + return; + + foreach (var tile in airtightGrid.Tiles.Values) + { + DecrementRefCount(tile.ToleranceCacheIndex); } - return explosionTolerance; + RemComp(entity); } - /// - /// Data struct that describes the explosion-blocking airtight entities on a tile. - /// - public struct TileData + public override void ReloadMap() { - public TileData(float[] explosionTolerance, AtmosDirection blockedDirections) + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var airtightComp, out var mapGrid)) { - ExplosionTolerance = explosionTolerance; - BlockedDirections = blockedDirections; + foreach (var pos in airtightComp.Tiles.Keys) + { + UpdateAirtightMap(uid, pos, mapGrid); + } } + } - public float[] ExplosionTolerance; - public AtmosDirection BlockedDirections = AtmosDirection.Invalid; + private struct CacheEntry + { + public ToleranceValues Values; + public int RefCount; // Doubles as freelist chain } + } diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs index 8515e1af42c..3767d0c2380 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs @@ -38,7 +38,7 @@ private void OnGridStartup(GridStartupEvent ev) private void OnGridRemoved(GridRemovalEvent ev) { - _airtightMap.Remove(ev.EntityUid); + OnAirtightGridRemoved(ev.EntityUid); _gridEdges.Remove(ev.EntityUid); // this should be a small enough set that iterating all of them is fine @@ -258,7 +258,7 @@ private void OnTileChanged(ref TileChangedEvent ev) { var neighbourIndex = change.GridIndices + NeighbourVectors[i]; - if (_mapSystem.TryGetTileRef(ev.Entity, grid, neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty) + if (_map.TryGetTileRef(ev.Entity, grid, neighbourIndex, out var neighbourTile) && !neighbourTile.Tile.IsEmpty) { var oppositeDirection = (NeighborFlag)(1 << ((i + 4) % 8)); edges[neighbourIndex] = edges.GetValueOrDefault(neighbourIndex) | oppositeDirection; @@ -307,7 +307,7 @@ private bool IsEdge(Entity grid, Vector2i index, out NeighborF spaceDirections = NeighborFlag.Invalid; for (var i = 0; i < NeighbourVectors.Length; i++) { - if (!_mapSystem.TryGetTileRef(grid, grid.Comp, index + NeighbourVectors[i], out var neighborTile) || neighborTile.Tile.IsEmpty) + if (!_map.TryGetTileRef(grid, grid.Comp, index + NeighbourVectors[i], out var neighborTile) || neighborTile.Tile.IsEmpty) spaceDirections |= (NeighborFlag) (1 << i); } diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs index d424ce51c60..fd474605b77 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs @@ -1,13 +1,9 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Explosion.Components; using Content.Shared.CCVar; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Database; using Content.Shared.Explosion; using Content.Shared.Explosion.Components; -using Content.Shared.Explosion.EntitySystems; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Projectiles; @@ -17,18 +13,18 @@ using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System.Numerics; +using Content.Shared.Damage.Systems; +using Robust.Shared.Prototypes; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Explosion.EntitySystems; public sealed partial class ExplosionSystem { - [Dependency] private readonly FlammableSystem _flammableSystem = default!; - /// /// Used to limit explosion processing time. See . /// @@ -209,6 +205,8 @@ internal bool ExplodeTile(BroadphaseComponent lookup, HashSet processed, string id, float? fireStacks, + float? temperature, + float currentIntensity, EntityUid? cause) { var size = grid.Comp.TileSize; @@ -241,6 +239,12 @@ internal bool ExplodeTile(BroadphaseComponent lookup, ProcessEntity(entity, epicenter, damage, intensity, throwForce, id, null, fireStacks, cause); } + // heat the atmosphere + if (temperature != null) + { + _atmosphere.HotspotExpose(grid.Owner, tile, temperature.Value, currentIntensity, cause, true); + } + // Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for // the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return // information about the entities that were spawned as a result, but without that information we just have to @@ -444,32 +448,29 @@ private void ProcessEntity( float? fireStacksOnIgnite, EntityUid? cause) { - if (originalDamage != null) + if (originalDamage is not null) { GetEntitiesToDamage(uid, originalDamage, id, intensity); foreach (var (entity, damage) in _toDamage) { - if (damage.GetTotal() > 0 && TryComp(entity, out var actorComponent)) + if (!_damageableQuery.TryComp(entity, out var damageable)) + continue; + + // TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin. + _damageableSystem.TryChangeDamage((entity, damageable), damage, ignoreResistances: true, ignoreGlobalModifiers: true); + + if (_actorQuery.HasComp(entity)) { - // Log damage to player entities only, cause this will create a massive amount of log spam otherwise. - if (cause != null) - { + // Log damage to player entities only; this will create a massive amount of log spam otherwise. + if (cause is not null) _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}"); - } else - { _adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}"); - } - } - - // TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin. - _damageableSystem.TryChangeDamage(entity, damage * _damageableSystem.UniversalExplosionDamageModifier, ignoreResistances: true); - } } - // ignite + // ignite entities with the flammable component if (fireStacksOnIgnite != null) { if (_flammableQuery.TryGetComponent(uid, out var flammable)) @@ -496,7 +497,6 @@ private void ProcessEntity( dir, physics, xform, - _projectileQuery, throwForce); } } @@ -510,10 +510,11 @@ public void DamageFloorTile(TileRef tileRef, int maxTileBreak, bool canCreateVacuum, List<(Vector2i GridIndices, Tile Tile)> damagedTiles, - ExplosionPrototype type) + ExplosionPrototype type, + TileHistoryComponent? history, + ref (TileHistoryChunk? Chunk, Vector2i Indices)? chunk) { - if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef - || tileDef.Indestructible) + if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef || tileDef.Indestructible) return; if (!CanCreateVacuum) @@ -521,23 +522,25 @@ public void DamageFloorTile(TileRef tileRef, else if (tileDef.MapAtmosphere) canCreateVacuum = true; // is already a vacuum. + // break the tile into its underlying parts int tileBreakages = 0; while (maxTileBreak > tileBreakages && _robustRandom.Prob(type.TileBreakChance(effectiveIntensity))) { tileBreakages++; effectiveIntensity -= type.TileBreakRerollReduction; - // does this have a base-turf that we can break it down to? - if (string.IsNullOrEmpty(tileDef.BaseTurf)) + if (GetNextTile((tileDef, tileRef.GridIndices), history, ref chunk) is not { } newId) break; - if (_tileDefinitionManager[tileDef.BaseTurf] is not ContentTileDefinition newDef) - break; + var newDef = (ContentTileDefinition) _tileDefinitionManager[newId]; if (newDef.MapAtmosphere && !canCreateVacuum) break; tileDef = newDef; + + if (newDef.Indestructible) + break; } if (tileDef.TileId == tileRef.Tile.TypeId) @@ -545,6 +548,44 @@ public void DamageFloorTile(TileRef tileRef, damagedTiles.Add((tileRef.GridIndices, new Tile(tileDef.TileId))); } + + private ProtoId? GetNextTile((ContentTileDefinition tileDef, Vector2i gridIndices) tile, + TileHistoryComponent? history, + ref (TileHistoryChunk? Chunk, Vector2i Indices)? chunk) + { + if (chunk?.Chunk == null || !chunk.Value.Chunk.History.TryGetValue(tile.gridIndices, out var stack)) + return tile.tileDef.BaseTurf; // No tile stack means we return BaseTurf if it exists! + + // last entry in the stack + if (stack.Count > 1) + { + var newId = stack[^1]; + stack.RemoveAt(stack.Count - 1); + chunk.Value.Chunk.LastModified = _timing.CurTick; + return newId; + } + + chunk.Value.Chunk.History.Remove(tile.gridIndices); + if (chunk.Value.Chunk.History.Count == 0) + { + history?.ChunkHistory.Remove(chunk.Value.Indices); + chunk = null; + } + else + { + chunk.Value.Chunk.LastModified = _timing.CurTick; + } + + return stack[0]; // If the stack is somehow empty, this will throw, but we will have at least removed it from dict first! + } + + public void DirtyHistory(EntityUid grid) + { + if (!_tileHistoryQuery.TryComp(grid, out var history)) + return; + + Dirty(grid, history); + } } /// @@ -577,11 +618,14 @@ struct ExplosionData /// /// The actual grid that this corresponds to. If null, this implies space. /// - public Entity? MapGrid; + public Entity? MapGrid; } private readonly List _explosionData = new(); + private Entity? _currentGrid; + private (TileHistoryChunk? Chunk, Vector2i Indices)? _currentChunk; + /// /// The explosion intensity associated with each tile iteration. /// @@ -629,7 +673,6 @@ struct ExplosionData private DamageSpecifier? _expectedDamage; #endif private Entity _currentLookup = default!; - private Entity? _currentGrid; private float _currentIntensity; private float _currentThrowForce; private List.Enumerator _currentEnumerator; @@ -671,6 +714,7 @@ struct ExplosionData private readonly IEntityManager _entMan; private readonly ExplosionSystem _system; private readonly SharedMapSystem _mapSystem; + private readonly Shared.Damage.Systems.DamageableSystem _damageable; public readonly EntityUid VisualEnt; @@ -691,10 +735,11 @@ public Explosion(ExplosionSystem system, int maxTileBreak, bool canCreateVacuum, IEntityManager entMan, - IMapManager mapMan, EntityUid visualEnt, EntityUid? cause, - SharedMapSystem mapSystem) + SharedMapSystem mapSystem, + DamageableSystem damageable, + EntityQuery historyQuery) { VisualEnt = visualEnt; Cause = cause; @@ -709,6 +754,7 @@ public Explosion(ExplosionSystem system, _maxTileBreak = maxTileBreak; _canCreateVacuum = canCreateVacuum; _entMan = entMan; + _damageable = damageable; _xformQuery = entMan.GetEntityQuery(); _physicsQuery = entMan.GetEntityQuery(); @@ -724,7 +770,7 @@ public Explosion(ExplosionSystem system, { TileLists = spaceData.TileLists, Lookup = (mapUid, entMan.GetComponent(mapUid)), - MapGrid = null + MapGrid = null, }); _spaceMatrix = spaceMatrix; @@ -733,11 +779,12 @@ public Explosion(ExplosionSystem system, foreach (var grid in gridData) { + var history = historyQuery.CompOrNull(grid.Grid); _explosionData.Add(new ExplosionData { TileLists = grid.TileLists, Lookup = (grid.Grid, entMan.GetComponent(grid.Grid)), - MapGrid = grid.Grid, + MapGrid = (grid.Grid, entMan.GetComponent(grid.Grid), history), }); } @@ -763,8 +810,10 @@ private bool TryGetNextTileEnumerator() _expectedDamage = ExplosionType.DamagePerIntensity * _currentIntensity; } #endif - - _currentDamage = ExplosionType.DamagePerIntensity * _currentIntensity; + var modifier = _currentIntensity + * _damageable.UniversalExplosionDamageModifier + * _damageable.UniversalAllDamageModifier; + _currentDamage = ExplosionType.DamagePerIntensity * modifier; // only throw if either the explosion is small, or if this is the outer ring of a large explosion. var doThrow = Area < _system.ThrowLimit || CurrentIteration > _tileSetIntensity.Count - 6; @@ -784,10 +833,11 @@ private bool TryGetNextTileEnumerator() _currentEnumerator = tileList.GetEnumerator(); _currentLookup = _explosionData[_currentDataIndex].Lookup; _currentGrid = _explosionData[_currentDataIndex].MapGrid; + _currentChunk = null; _currentDataIndex++; // sanity checks, in case something changed while the explosion was being processed over several ticks. - if (_currentLookup.Comp.Deleted || _currentGrid != null && !_entMan.EntityExists(_currentGrid.Value)) + if (_currentLookup.Comp.Deleted || (_currentGrid is { } grid && !_entMan.EntityExists(grid.Owner))) continue; return true; @@ -843,19 +893,19 @@ public int Process(int processingTarget) // Is the current tile on a grid (instead of in space)? if (_currentGrid is { } currentGrid && - _mapSystem.TryGetTileRef(currentGrid, currentGrid.Comp, _currentEnumerator.Current, out var tileRef) && + _mapSystem.TryGetTileRef(currentGrid.Owner, currentGrid.Comp1, _currentEnumerator.Current, out var tileRef) && !tileRef.Tile.IsEmpty) { - if (!_tileUpdateDict.TryGetValue(currentGrid, out var tileUpdateList)) + if (!_tileUpdateDict.TryGetValue((currentGrid.Owner, currentGrid.Comp1), out var tileUpdateList)) { tileUpdateList = new(); - _tileUpdateDict[currentGrid] = tileUpdateList; + _tileUpdateDict[(currentGrid.Owner, currentGrid.Comp1)] = tileUpdateList; } // damage entities on the tile. Also figures out whether there are any solid entities blocking the floor // from being destroyed. var canDamageFloor = _system.ExplodeTile(_currentLookup, - currentGrid, + (currentGrid.Owner, currentGrid.Comp1), _currentEnumerator.Current, _currentThrowForce, _currentDamage, @@ -864,11 +914,30 @@ public int Process(int processingTarget) ProcessedEntities, ExplosionType.ID, ExplosionType.FireStacks, + ExplosionType.Temperature, + _currentIntensity, Cause); // If the floor is not blocked by some dense object, damage the floor tiles. if (canDamageFloor) - _system.DamageFloorTile(tileRef, _currentIntensity * _tileBreakScale, _maxTileBreak, _canCreateVacuum, tileUpdateList, ExplosionType); + { + var tileIndices = _currentEnumerator.Current; + var chunkIndices = SharedMapSystem.GetChunkIndices(tileIndices, TileSystem.ChunkSize); + if (_currentChunk?.Indices != chunkIndices) + { + var chunk = currentGrid.Comp2?.ChunkHistory.GetValueOrDefault(chunkIndices); + _currentChunk = (chunk, chunkIndices); + } + + _system.DamageFloorTile(tileRef, + _currentIntensity * _tileBreakScale, + _maxTileBreak, + _canCreateVacuum, + tileUpdateList, + ExplosionType, + currentGrid.Comp2, + ref _currentChunk); + } } else { @@ -909,6 +978,8 @@ private void SetTiles() if (list.Count > 0 && _entMan.EntityExists(grid.Owner)) { _mapSystem.SetTiles(grid.Owner, grid, list); + + _system.DirtyHistory(grid.Owner); } } _tileUpdateDict.Clear(); diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs index cee694886ba..a274fa86603 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Numerics; +using Content.Server.Explosion.Components; using Content.Shared.Administration; using Content.Shared.Explosion.Components; using Robust.Shared.Map; @@ -40,11 +41,7 @@ public sealed partial class ExplosionSystem if (totalIntensity <= 0 || slope <= 0) return null; - if (!_explosionTypes.TryGetValue(typeID, out var typeIndex)) - { - Log.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported."); - return null; - } + var typeIndex = _explosionTypes[typeID]; Vector2i initialTile; EntityUid? epicentreGrid = null; @@ -52,7 +49,7 @@ public sealed partial class ExplosionSystem // get the epicenter tile indices if (_mapManager.TryFindGridAt(epicenter, out var gridUid, out var candidateGrid) && - _mapSystem.TryGetTileRef(gridUid, candidateGrid, _mapSystem.WorldToTile(gridUid, candidateGrid, epicenter.Position), out var tileRef) && + _map.TryGetTileRef(gridUid, candidateGrid, _map.WorldToTile(gridUid, candidateGrid, epicenter.Position), out var tileRef) && !tileRef.Tile.IsEmpty) { epicentreGrid = gridUid; @@ -62,7 +59,7 @@ public sealed partial class ExplosionSystem { // reference grid defines coordinate system that the explosion in space will use var gridComp = Comp(referenceGrid.Value); - initialTile = _mapSystem.WorldToTile(referenceGrid.Value, gridComp, epicenter.Position); + initialTile = _map.WorldToTile(referenceGrid.Value, gridComp, epicenter.Position); } else { @@ -103,8 +100,7 @@ public sealed partial class ExplosionSystem // set up the initial `gridData` instance encounteredGrids.Add(epicentreGrid.Value); - if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap)) - airtightMap = new(); + var airtightMap = CompOrNull(epicentreGrid)?.Tiles ?? new(); var initialGridData = new ExplosionGridTileFlood( (epicentreGrid.Value, Comp(epicentreGrid.Value)), @@ -115,7 +111,8 @@ public sealed partial class ExplosionSystem _gridEdges[epicentreGrid.Value], referenceGrid, spaceMatrix, - spaceAngle); + spaceAngle, + this); gridData[epicentreGrid.Value] = initialGridData; @@ -192,8 +189,7 @@ public sealed partial class ExplosionSystem // is this a new grid, for which we must create a new explosion data set if (!gridData.TryGetValue(grid, out var data)) { - if (!_airtightMap.TryGetValue(grid, out var airtightMap)) - airtightMap = new(); + var airtightMap = CompOrNull(grid)?.Tiles ?? new(); data = new ExplosionGridTileFlood( (grid, Comp(grid)), @@ -204,7 +200,8 @@ public sealed partial class ExplosionSystem _gridEdges[grid], referenceGrid, spaceMatrix, - spaceAngle); + spaceAngle, + this); gridData[grid] = data; } diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs index b459f5c70f5..400f555db47 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs @@ -2,21 +2,24 @@ using System.Numerics; using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Destructible; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NPC.Pathfinding; using Content.Shared.Atmos.Components; using Content.Shared.Camera; using Content.Shared.CCVar; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Explosion; using Content.Shared.Explosion.Components; using Content.Shared.Explosion.EntitySystems; using Content.Shared.GameTicking; using Content.Shared.Inventory; +using Content.Shared.Maps; using Content.Shared.Projectiles; using Content.Shared.Throwing; -using Robust.Server.GameObjects; using Robust.Server.GameStates; using Robust.Server.Player; using Robust.Shared.Audio.Systems; @@ -26,6 +29,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Explosion.EntitySystems; @@ -38,23 +42,31 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!; [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!; [Dependency] private readonly SharedCameraRecoilSystem _recoilSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly ThrowingSystem _throwingSystem = default!; [Dependency] private readonly PvsOverrideSystem _pvsSys = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly FlammableSystem _flammableSystem = default!; + [Dependency] private readonly DestructibleSystem _destructibleSystem = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; private EntityQuery _flammableQuery; private EntityQuery _physicsQuery; private EntityQuery _projectileQuery; + private EntityQuery _actorQuery; + private EntityQuery _destructibleQuery; + private EntityQuery _damageableQuery; + private EntityQuery _airtightQuery; + private EntityQuery _tileHistoryQuery; /// /// "Tile-size" for space when there are no nearby grids to use as a reference. @@ -93,6 +105,13 @@ public override void Initialize() _flammableQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _projectileQuery = GetEntityQuery(); + _actorQuery = GetEntityQuery(); + _destructibleQuery = GetEntityQuery(); + _damageableQuery = GetEntityQuery(); + _airtightQuery = GetEntityQuery(); + _tileHistoryQuery = GetEntityQuery(); + + _prototypeManager.PrototypesReloaded += ReloadExplosionPrototypes; } private void OnReset(RoundRestartCleanupEvent ev) @@ -111,6 +130,7 @@ public override void Shutdown() base.Shutdown(); _nodeGroupSystem.PauseUpdating = false; _pathfindingSystem.PauseUpdating = false; + _prototypeManager.PrototypesReloaded -= ReloadExplosionPrototypes; } private void RelayedResistance(EntityUid uid, ExplosionResistanceComponent component, @@ -246,11 +266,14 @@ public override void QueueExplosion(EntityUid uid, var logImpact = (alertMinExplosionIntensity > -1 && totalIntensity >= alertMinExplosionIntensity) ? LogImpact.Extreme : LogImpact.High; - _adminLogger.Add(LogType.Explosion, logImpact, - $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not found]")} with intensity {totalIntensity} slope {slope}"); + if (posFound) + _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:{gridPos:coordinates} with intensity {totalIntensity} slope {slope}"); + else + _adminLogger.Add(LogType.Explosion, logImpact, $"{ToPrettyString(user.Value):user} caused {ToPrettyString(uid):entity} to explode ({typeId}) at Pos:[Grid or Map not found] with intensity {totalIntensity} slope {slope}"); } } + /// /// Queue an explosion, with a specified epicenter and set of starting tiles. /// @@ -317,7 +340,7 @@ public void QueueExplosion(MapCoordinates epicenter, private Explosion? SpawnExplosion(QueuedExplosion queued) { var pos = queued.Epicenter; - if (!_mapSystem.MapExists(pos.MapId)) + if (!_map.MapExists(pos.MapId)) return null; var results = GetExplosionTiles(pos, queued.Proto.ID, queued.TotalIntensity, queued.Slope, queued.MaxTileIntensity); @@ -333,7 +356,7 @@ public void QueueExplosion(MapCoordinates epicenter, CameraShake(iterationIntensity.Count * 4f, pos, queued.TotalIntensity); //For whatever bloody reason, sound system requires ENTITY coordinates. - var mapEntityCoords = _transformSystem.ToCoordinates(_mapSystem.GetMap(pos.MapId), pos); + var mapEntityCoords = _transformSystem.ToCoordinates(_map.GetMap(pos.MapId), pos); // play sound. // for the normal audio, we want everyone in pvs range @@ -376,10 +399,11 @@ public void QueueExplosion(MapCoordinates epicenter, queued.MaxTileBreak, queued.CanCreateVacuum, EntityManager, - _mapManager, visualEnt, queued.Cause, - _map); + _map, + _damageableSystem, + _tileHistoryQuery); } private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity) diff --git a/Content.Server/Fax/FaxConstants.cs b/Content.Server/Fax/FaxConstants.cs index 4b0ff7853eb..8578172381a 100644 --- a/Content.Server/Fax/FaxConstants.cs +++ b/Content.Server/Fax/FaxConstants.cs @@ -30,4 +30,5 @@ public static class FaxConstants public const string FaxPaperStampedByData = "fax_data_stamped_by"; public const string FaxSyndicateData = "fax_data_i_am_syndicate"; public const string FaxPaperLockedData = "fax_data_locked"; + public const string FaxPaperSenderFaxNameData = "fax_data_sender_fax_name"; } diff --git a/Content.Server/Fax/FaxSystem.cs b/Content.Server/Fax/FaxSystem.cs index 1815b526b4a..6f4a6d06b5a 100644 --- a/Content.Server/Fax/FaxSystem.cs +++ b/Content.Server/Fax/FaxSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Fax; using Content.Shared.Fax.Components; using Content.Shared.Fax.Systems; +using Content.Shared.GameTicking; using Content.Shared.Interaction; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; @@ -30,6 +31,7 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; namespace Content.Server.Fax; @@ -39,6 +41,7 @@ public sealed class FaxSystem : EntitySystem [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly SharedGameTicker _gameTicker = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly PaperSystem _paperSystem = default!; @@ -300,8 +303,9 @@ private void OnPacketReceived(EntityUid uid, FaxMachineComponent component, Devi args.Data.TryGetValue(FaxConstants.FaxPaperStampedByData, out List? stampedBy); args.Data.TryGetValue(FaxConstants.FaxPaperPrototypeData, out string? prototypeId); args.Data.TryGetValue(FaxConstants.FaxPaperLockedData, out bool? locked); + args.Data.TryGetValue(FaxConstants.FaxPaperSenderFaxNameData, out string? senderFaxName); - var printout = new FaxPrintout(content, name, label, prototypeId, stampState, stampedBy, locked ?? false); + var printout = new FaxPrintout(content, name, label, prototypeId, stampState, stampedBy, locked ?? false, senderFaxName); Receive(uid, printout, args.SenderAddress); break; @@ -392,6 +396,7 @@ public void SetDestination(EntityUid uid, string destAddress, FaxMachineComponen return; component.DestinationFaxAddress = destAddress; + component.DestinationFaxName = component.KnownFaxes[destAddress]; UpdateUserInterface(uid, component); } @@ -521,13 +526,35 @@ public void Send(EntityUid uid, FaxMachineComponent? component, FaxSendMessage a TryComp(sendEntity, out var labelComponent); + var content = paper.Content; + + if (component.AddSenderInfo) + { + var faxMachineAddress = TryComp(uid, out var deviceNetworkComponent) + ? deviceNetworkComponent.Address + : Loc.GetString("device-address-unknown"); + + var time = _gameTicker.RoundDuration(); + var timeString = TimeSpan.FromSeconds(Math.Truncate(time.TotalSeconds)).ToString(); + + content += "\n"; + content += Loc.GetString(component.SenderInfo, + ("sender_name", component.FaxName), + ("sender_addr", faxMachineAddress), + ("recipient_name", component.DestinationFaxName ?? Loc.GetString("fax-machine-popup-source-unknown")), + ("recipient_addr", component.DestinationFaxAddress), + ("time", timeString) + ); + } + var payload = new NetworkPayload() { { DeviceNetworkConstants.Command, FaxConstants.FaxPrintCommand }, { FaxConstants.FaxPaperNameData, nameMod?.BaseName ?? metadata.EntityName }, { FaxConstants.FaxPaperLabelData, labelComponent?.CurrentLabel }, - { FaxConstants.FaxPaperContentData, paper.Content }, + { FaxConstants.FaxPaperContentData, content }, { FaxConstants.FaxPaperLockedData, paper.EditingDisabled }, + { FaxConstants.FaxPaperSenderFaxNameData, component.FaxName ?? Loc.GetString("fax-machine-popup-source-unknown") } }; if (metadata.EntityPrototype != null) { @@ -569,9 +596,7 @@ public void Receive(EntityUid uid, FaxPrintout printout, string? fromAddress = n if (!Resolve(uid, ref component)) return; - var faxName = Loc.GetString("fax-machine-popup-source-unknown"); - if (fromAddress != null && component.KnownFaxes.TryGetValue(fromAddress, out var fax)) // If message received from unknown fax address - faxName = fax; + var faxName = printout.SenderFaxName ?? Loc.GetString("fax-machine-popup-source-unknown"); _popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-received", ("from", faxName)), uid); _appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Printing); diff --git a/Content.Server/FeedbackSystem/FeedbackCommand.cs b/Content.Server/FeedbackSystem/FeedbackCommand.cs new file mode 100644 index 00000000000..68caed7df06 --- /dev/null +++ b/Content.Server/FeedbackSystem/FeedbackCommand.cs @@ -0,0 +1,63 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.FeedbackSystem; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.FeedbackSystem; + +/// +/// Adds, removes, and displays feedback for specified sessions. +/// +[ToolshedCommand] +[AdminCommand(AdminFlags.Debug)] +public sealed class FeedbackCommand : ToolshedCommand +{ + [Dependency] private readonly ISharedFeedbackManager _feedback = null!; + + [CommandImplementation("show")] + public void ExecuteShow([CommandArgument] ICommonSession session) + { + _feedback.OpenForSession(session); + } + + [CommandImplementation("show")] + public void ExecuteShow([PipedArgument] IEnumerable sessions) + { + foreach (var session in sessions) + { + _feedback.OpenForSession(session); + } + } + + [CommandImplementation("add")] + public void ExecuteAdd([CommandArgument] ICommonSession session, ProtoId protoId) + { + _feedback.SendToSession(session, [protoId]); + } + + [CommandImplementation("add")] + public void ExecuteAdd([PipedArgument] IEnumerable sessions, ProtoId protoId) + { + foreach (var session in sessions) + { + _feedback.SendToSession(session, [protoId]); + } + } + + [CommandImplementation("remove")] + public void ExecuteRemove([CommandArgument] ICommonSession session, ProtoId protoId) + { + _feedback.SendToSession(session, [protoId], true); + } + + [CommandImplementation("remove")] + public void ExecuteRemove([PipedArgument] IEnumerable sessions, ProtoId protoId) + { + foreach (var session in sessions) + { + _feedback.SendToSession(session, [protoId], true); + } + } +} diff --git a/Content.Server/FeedbackSystem/FeedbackSystem.cs b/Content.Server/FeedbackSystem/FeedbackSystem.cs new file mode 100644 index 00000000000..39e1d55a297 --- /dev/null +++ b/Content.Server/FeedbackSystem/FeedbackSystem.cs @@ -0,0 +1,39 @@ +using Content.Server.GameTicking; +using Content.Shared.FeedbackSystem; +using Content.Shared.GameTicking; +using Robust.Shared.Prototypes; + +namespace Content.Server.FeedbackSystem; + +public sealed partial class FeedbackSystem : EntitySystem +{ + [Dependency] private readonly ServerFeedbackManager _feedbackManager = null!; + [Dependency] private readonly GameTicker _gameTicker = null!; + [Dependency] private readonly IPrototypeManager _prototypeManager = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRoundEnd); + } + + private void OnRoundEnd(RoundEndMessageEvent args) + { + var validPopups = new List>(); + var notValidPopups = new List>(); + + foreach (var feedback in _feedbackManager.GetOriginFeedbackPrototypes(true, true)) + { + if (_gameTicker.IsGameRuleAdded(_prototypeManager.Index(feedback).RuleWhitelist)) + validPopups.Add(feedback); + else + notValidPopups.Add(feedback); + } + + if (validPopups.Count > 0) + _feedbackManager.SendToAllSessions(validPopups); + + if (notValidPopups.Count > 0) + _feedbackManager.SendToAllSessions(notValidPopups, true); + } +} diff --git a/Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs b/Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs new file mode 100644 index 00000000000..7816f7de212 --- /dev/null +++ b/Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs @@ -0,0 +1,24 @@ +using Content.Shared.Administration; +using Content.Shared.FeedbackSystem; +using Robust.Shared.Toolshed; + +namespace Content.Server.FeedbackSystem; + +/// +/// Opens the feedback popup window for the executing session +/// +[AnyCommand] +[ToolshedCommand] +public sealed class OpenFeedbackPopupCommand : ToolshedCommand +{ + [Dependency] private readonly ISharedFeedbackManager _feedback = null!; + + [CommandImplementation] + public void Execute(IInvocationContext context) + { + if (context.Session == null) + return; + + _feedback.OpenForSession(context.Session); + } +} diff --git a/Content.Server/FeedbackSystem/ServerFeedbackManager.cs b/Content.Server/FeedbackSystem/ServerFeedbackManager.cs new file mode 100644 index 00000000000..4ecb22bcc23 --- /dev/null +++ b/Content.Server/FeedbackSystem/ServerFeedbackManager.cs @@ -0,0 +1,66 @@ +using Content.Shared.FeedbackSystem; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.FeedbackSystem; + +/// +public sealed class ServerFeedbackManager : SharedFeedbackManager +{ + [Dependency] private readonly ISharedPlayerManager _player = null!; + + public override void Initialize() + { + base.Initialize(); + NetManager.RegisterNetMessage(); + NetManager.RegisterNetMessage(); + } + + /// + public override bool Send(EntityUid uid, List> popupPrototypes) + { + if (!_player.TryGetSessionByEntity(uid, out var session)) + return false; + + SendToSession(session, popupPrototypes); + return true; + } + + /// + public override void SendToSession(ICommonSession session, List> popupPrototypes, bool remove = false) + { + var msg = new FeedbackPopupMessage + { + FeedbackPrototypes = popupPrototypes, + Remove = remove, + }; + + NetManager.ServerSendMessage(msg, session.Channel); + } + + /// + public override void SendToAllSessions(List> popupPrototypes, bool remove = false) + { + var msg = new FeedbackPopupMessage + { + FeedbackPrototypes = popupPrototypes, + Remove = remove, + }; + + NetManager.ServerSendToAll(msg); + } + + /// + public override void OpenForSession(ICommonSession session) + { + var msg = new OpenFeedbackPopupMessage(); + NetManager.ServerSendMessage(msg, session.Channel); + } + + /// + public override void OpenForAllSessions() + { + var msg = new OpenFeedbackPopupMessage(); + NetManager.ServerSendToAll(msg); + } +} diff --git a/Content.Server/Fluids/Components/SprayComponent.cs b/Content.Server/Fluids/Components/SprayComponent.cs deleted file mode 100644 index 128fdecfa7f..00000000000 --- a/Content.Server/Fluids/Components/SprayComponent.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Server.Fluids.EntitySystems; -using Content.Shared.FixedPoint; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Server.Fluids.Components; - -[RegisterComponent] -[Access(typeof(SpraySystem))] -public sealed partial class SprayComponent : Component -{ - public const string SolutionName = "spray"; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public FixedPoint2 TransferAmount = 10; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public float SprayDistance = 3.5f; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public float SprayVelocity = 3.5f; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public EntProtoId SprayedPrototype = "Vapor"; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public int VaporAmount = 1; - - [ViewVariables(VVAccess.ReadWrite), DataField] - public float VaporSpread = 90f; - - /// - /// How much the player is pushed back for each spray. - /// - [DataField] - public float PushbackAmount = 5f; - - [DataField(required: true)] - [Access(typeof(SpraySystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends - public SoundSpecifier SpraySound { get; private set; } = default!; -} diff --git a/Content.Server/Fluids/EntitySystems/DrainSystem.cs b/Content.Server/Fluids/EntitySystems/DrainSystem.cs deleted file mode 100644 index 84718add5fe..00000000000 --- a/Content.Server/Fluids/EntitySystems/DrainSystem.cs +++ /dev/null @@ -1,266 +0,0 @@ -using Content.Server.DoAfter; -using Content.Server.Popups; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Audio; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.Examine; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids; -using Content.Shared.Fluids.Components; -using Content.Shared.Interaction; -using Content.Shared.Tag; -using Content.Shared.Verbs; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Fluids.EntitySystems; - -public sealed class DrainSystem : SharedDrainSystem -{ - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - private readonly HashSet> _puddles = new(); - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnDrainMapInit); - SubscribeLocalEvent>(AddEmptyVerb); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnInteract); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnDrainMapInit(Entity ent, ref MapInitEvent args) - { - // Randomise puddle drains so roundstart ones don't all dump at the same time. - ent.Comp.Accumulator = _random.NextFloat(ent.Comp.DrainFrequency); - } - - private void AddEmptyVerb(Entity entity, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Using == null) - return; - - if (!TryComp(args.Using, out SpillableComponent? spillable) || - !TryComp(args.Target, out DrainComponent? drain)) - return; - - var used = args.Using.Value; - var target = args.Target; - Verb verb = new() - { - Text = Loc.GetString("drain-component-empty-verb-inhand", ("object", Name(used))), - Act = () => - { - Empty(used, spillable, target, drain); - }, - Impact = LogImpact.Low, - Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")) - - }; - args.Verbs.Add(verb); - } - - private void Empty(EntityUid container, SpillableComponent spillable, EntityUid target, DrainComponent drain) - { - // Find the solution in the container that is emptied - if (!_solutionContainerSystem.TryGetDrainableSolution(container, out var containerSoln, out var containerSolution) || containerSolution.Volume == FixedPoint2.Zero) - { - _popupSystem.PopupEntity( - Loc.GetString("drain-component-empty-verb-using-is-empty-message", ("object", container)), - container); - return; - } - - // try to find the drain's solution - if (!_solutionContainerSystem.ResolveSolution(target, DrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) - { - return; - } - - // Try to transfer as much solution as possible to the drain - - var amountToPutInDrain = drainSolution.AvailableVolume; - var amountToSpillOnGround = containerSolution.Volume - drainSolution.AvailableVolume; - - if (amountToPutInDrain > 0) - { - var solutionToPutInDrain = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToPutInDrain); - _solutionContainerSystem.TryAddSolution(drain.Solution.Value, solutionToPutInDrain); - - _audioSystem.PlayPvs(drain.ManualDrainSound, target); - _ambientSoundSystem.SetAmbience(target, true); - } - - - // Spill the remainder. - - if (amountToSpillOnGround > 0) - { - var solutionToSpill = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToSpillOnGround); - _puddleSystem.TrySpillAt(Transform(target).Coordinates, solutionToSpill, out _); - _popupSystem.PopupEntity( - Loc.GetString("drain-component-empty-verb-target-is-full-message", ("object", target)), - container); - } - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - var managerQuery = GetEntityQuery(); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var drain)) - { - drain.Accumulator += frameTime; - if (drain.Accumulator < drain.DrainFrequency) - { - continue; - } - drain.Accumulator -= drain.DrainFrequency; - - if (!managerQuery.TryGetComponent(uid, out var manager)) - continue; - - // Best to do this one every second rather than once every tick... - if (!_solutionContainerSystem.ResolveSolution((uid, manager), DrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) - continue; - - if (drainSolution.Volume <= 0 && !drain.AutoDrain) - { - _ambientSoundSystem.SetAmbience(uid, false); - continue; - } - - // Remove a bit from the buffer - _solutionContainerSystem.SplitSolution(drain.Solution.Value, (drain.UnitsDestroyedPerSecond * drain.DrainFrequency)); - - // This will ensure that UnitsPerSecond is per second... - var amount = drain.UnitsPerSecond * drain.DrainFrequency; - - if (drain.AutoDrain) - { - _puddles.Clear(); - _lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles); - - if (_puddles.Count == 0 && drainSolution.Volume <= 0) - { - _ambientSoundSystem.SetAmbience(uid, false); - continue; - } - - _ambientSoundSystem.SetAmbience(uid, true); - - amount /= _puddles.Count; - - foreach (var puddle in _puddles) - { - // Queue the solution deletion if it's empty. EvaporationSystem might also do this - // but queuedelete should be pretty safe. - if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution)) - { - QueueDel(puddle); - continue; - } - - // Removes the lowest of: - // the drain component's units per second adjusted for # of puddles - // the puddle's remaining volume (making it cleanly zero) - // the drain's remaining volume in its buffer. - var transferSolution = _solutionContainerSystem.SplitSolution(puddle.Comp.Solution.Value, - FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume)); - - drainSolution.AddSolution(transferSolution, _prototypeManager); - - if (puddleSolution.Volume <= 0) - { - QueueDel(puddle); - } - } - } - - _solutionContainerSystem.UpdateChemicals(drain.Solution.Value); - } - } - - private void OnExamined(Entity entity, ref ExaminedEvent args) - { - if (!args.IsInDetailsRange || - !HasComp(entity) || - !_solutionContainerSystem.ResolveSolution(entity.Owner, DrainComponent.SolutionName, ref entity.Comp.Solution, out var drainSolution)) - { - return; - } - - var text = drainSolution.AvailableVolume != 0 - ? Loc.GetString("drain-component-examine-volume", ("volume", drainSolution.AvailableVolume)) - : Loc.GetString("drain-component-examine-hint-full"); - args.PushMarkup(text); - } - - private void OnInteract(Entity entity, ref AfterInteractUsingEvent args) - { - if (!args.CanReach || args.Target == null || - !_tagSystem.HasTag(args.Used, DrainComponent.PlungerTag) || - !_solutionContainerSystem.ResolveSolution(args.Target.Value, DrainComponent.SolutionName, ref entity.Comp.Solution, out var drainSolution)) - { - return; - } - - if (drainSolution.AvailableVolume > 0) - { - _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-notapplicable", ("object", args.Target.Value)), args.Target.Value); - return; - } - - _audioSystem.PlayPvs(entity.Comp.PlungerSound, entity); - - - var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.UnclogDuration, new DrainDoAfterEvent(), entity, args.Target, args.Used) - { - BreakOnDamage = true, - BreakOnMove = true, - BreakOnHandChange = true - }; - - _doAfterSystem.TryStartDoAfter(doAfterArgs); - } - - private void OnDoAfter(Entity entity, ref DrainDoAfterEvent args) - { - if (args.Target == null) - return; - - if (!_random.Prob(entity.Comp.UnclogProbability)) - { - _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-fail", ("object", args.Target.Value)), args.Target.Value); - return; - } - - - if (!_solutionContainerSystem.ResolveSolution(args.Target.Value, DrainComponent.SolutionName, ref entity.Comp.Solution)) - { - return; - } - - - _solutionContainerSystem.RemoveAllSolution(entity.Comp.Solution.Value); - _audioSystem.PlayPvs(entity.Comp.UnclogSound, args.Target.Value); - _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-success", ("object", args.Target.Value)), args.Target.Value); - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs deleted file mode 100644 index 245ab8308f9..00000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids.Components; - -namespace Content.Server.Fluids.EntitySystems; - -public sealed partial class PuddleSystem -{ - private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1); - - private void OnEvaporationMapInit(Entity entity, ref MapInitEvent args) - { - entity.Comp.NextTick = _timing.CurTime + EvaporationCooldown; - } - - private void UpdateEvaporation(EntityUid uid, Solution solution) - { - if (HasComp(uid)) - { - return; - } - - if (solution.GetTotalPrototypeQuantity(GetEvaporatingReagents(solution)) > FixedPoint2.Zero) - { - var evaporation = AddComp(uid); - evaporation.NextTick = _timing.CurTime + EvaporationCooldown; - return; - } - - RemComp(uid); - } - - private void TickEvaporation() - { - var query = EntityQueryEnumerator(); - var xformQuery = GetEntityQuery(); - var curTime = _timing.CurTime; - while (query.MoveNext(out var uid, out var evaporation, out var puddle)) - { - if (evaporation.NextTick > curTime) - continue; - - evaporation.NextTick += EvaporationCooldown; - - if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var puddleSolution)) - continue; - - // Yes, this means that 50u water + 50u holy water evaporates twice as fast as 100u water. - foreach ((string evaporatingReagent, FixedPoint2 evaporatingSpeed) in GetEvaporationSpeeds(puddleSolution)) - { - var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds * evaporatingSpeed; - puddleSolution.SplitSolutionWithOnly(reagentTick, evaporatingReagent); - } - - // Despawn if we're done - if (puddleSolution.Volume == FixedPoint2.Zero) - { - // Spawn a *sparkle* - Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates); - QueueDel(uid); - } - - _solutionContainerSystem.UpdateChemicals(puddle.Solution.Value); - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index 880a4395b4c..e632d6efb32 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -1,20 +1,8 @@ -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.Chemistry; -using Content.Shared.Clothing; -using Content.Shared.CombatMode.Pacification; using Content.Shared.Database; -using Content.Shared.FixedPoint; using Content.Shared.Fluids.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Popups; using Content.Shared.Spillable; using Content.Shared.Throwing; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Player; namespace Content.Server.Fluids.EntitySystems; @@ -26,10 +14,8 @@ protected override void InitializeSpillable() SubscribeLocalEvent(SpillOnLand); // Openable handles the event if it's closed - SubscribeLocalEvent(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); SubscribeLocalEvent(OnOverflow); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnAttemptPacifiedThrow); } private void OnOverflow(Entity entity, ref SolutionContainerOverflowEvent args) @@ -41,101 +27,16 @@ private void OnOverflow(Entity entity, ref SolutionContainer args.Handled = true; } - private void SplashOnMeleeHit(Entity entity, ref MeleeHitEvent args) - { - if (args.Handled) - return; - - // When attacking someone reactive with a spillable entity, - // splash a little on them (touch react) - // If this also has solution transfer, then assume the transfer amount is how much we want to spill. - // Otherwise let's say they want to spill a quarter of its max volume. - - if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out var soln, out var solution)) - return; - - var hitCount = args.HitEntities.Count; - - var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume); - if (TryComp(entity, out var transfer)) - { - totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume); - } - - // a little lame, but reagent quantity is not very balanced and we don't want people - // spilling like 100u of reagent on someone at once! - totalSplit = FixedPoint2.Min(totalSplit, entity.Comp.MaxMeleeSpillAmount); - - if (totalSplit == 0) - return; - - // Optionally allow further melee handling occur - args.Handled = entity.Comp.PreventMelee; - - // First update the hit count so anything that is not reactive wont count towards the total! - foreach (var hit in args.HitEntities) - { - if (!HasComp(hit)) - hitCount -= 1; - } - - foreach (var hit in args.HitEntities) - { - if (!HasComp(hit)) - continue; - - var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount); - - _adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}"); - _reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch); - - _popups.PopupEntity( - Loc.GetString("spill-melee-hit-attacker", ("amount", totalSplit / hitCount), ("spillable", entity.Owner), - ("target", Identity.Entity(hit, EntityManager))), - hit, args.User); - - _popups.PopupEntity( - Loc.GetString("spill-melee-hit-others", ("attacker", args.User), ("spillable", entity.Owner), - ("target", Identity.Entity(hit, EntityManager))), - hit, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); - } - } - private void SpillOnLand(Entity entity, ref LandEvent args) { - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) + if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner)) return; - if (Openable.IsClosed(entity.Owner)) - return; - - if (!entity.Comp.SpillWhenThrown) - return; - - if (args.User != null) + if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null) { - _adminLogger.Add(LogType.Landed, + AdminLogger.Add(LogType.Landed, $"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing"); } - - var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume); - TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _); - } - - /// - /// Prevent Pacified entities from throwing items that can spill liquids. - /// - private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) - { - // Don’t care about closed containers. - if (Openable.IsClosed(ent)) - return; - - // Don’t care about empty containers. - if (!_solutionContainerSystem.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution) || solution.Volume <= 0) - return; - - args.Cancel("pacified-cannot-throw-spill"); } private void OnDoAfter(Entity entity, ref SpillDoAfterEvent args) diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs deleted file mode 100644 index e850f058a8a..00000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.DragDrop; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids; -using Content.Shared.Nutrition.EntitySystems; - -namespace Content.Server.Fluids.EntitySystems; - -public sealed partial class PuddleSystem -{ - [Dependency] private readonly OpenableSystem _openable = default!; - - private void InitializeTransfers() - { - SubscribeLocalEvent(OnRefillableDragged); - } - - private void OnRefillableDragged(Entity entity, ref DragDropDraggedEvent args) - { - if (!_actionBlocker.CanComplexInteract(args.User)) - { - _popups.PopupEntity(Loc.GetString("mopping-system-no-hands"), args.User, args.User); - return; - } - - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution) || solution.Volume == FixedPoint2.Zero) - { - _popups.PopupEntity(Loc.GetString("mopping-system-empty", ("used", entity.Owner)), entity, args.User); - return; - } - - // Dump reagents into DumpableSolution - if (TryComp(args.Target, out var dump)) - { - if (!_solutionContainerSystem.TryGetDumpableSolution((args.Target, dump, null), out var dumpableSoln, out var dumpableSolution)) - return; - - if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out _, out _)) - return; - - if (_openable.IsClosed(entity)) - return; - - bool success = true; - if (dump.Unlimited) - { - var split = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume); - dumpableSolution.AddSolution(split, _prototypeManager); - } - else - { - var split = _solutionContainerSystem.SplitSolution(soln.Value, dumpableSolution.AvailableVolume); - success = _solutionContainerSystem.TryAddSolution(dumpableSoln.Value, split); - } - - if (success) - { - _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, args.Target); - } - else - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", args.Target)), args.Target, args.User); - } - - return; - } - - // Take reagents from target - if (!TryComp(args.Target, out var drainable)) - { - if (!_solutionContainerSystem.TryGetDrainableSolution((args.Target, drainable, null), out var drainableSolution, out _)) - return; - - var split = _solutionContainerSystem.SplitSolution(drainableSolution.Value, solution.AvailableVolume); - - if (_solutionContainerSystem.TryAddSolution(soln.Value, split)) - { - _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, entity); - } - else - { - _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", entity.Owner)), entity, args.User); - } - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index ef5c4a3f884..aba3c8c878a 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -5,36 +5,27 @@ using Content.Server.DoAfter; using Content.Server.Fluids.Components; using Content.Server.Spreader; -using Content.Shared.ActionBlocker; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; -using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.Effects; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Fluids.Components; -using Content.Shared.Friction; using Content.Shared.IdentityManagement; using Content.Shared.Imperial.Medieval.ChemistryRandomization; using Content.Shared.Maps; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Slippery; -using Content.Shared.StepTrigger.Components; -using Content.Shared.StepTrigger.Systems; -using Robust.Server.Audio; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Fluids.EntitySystems; @@ -43,28 +34,15 @@ namespace Content.Server.Fluids.EntitySystems; /// public sealed partial class PuddleSystem : SharedPuddleSystem { - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly ReactiveSystem _reactive = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; - [Dependency] private readonly SharedPopupSystem _popups = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly StepTriggerSystem _stepTrigger = default!; - [Dependency] private readonly SpeedModifierContactsSystem _speedModContacts = default!; - [Dependency] private readonly TileFrictionController _tile = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TurfSystem _turf = default!; - // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle - // loses & then gains reagents in a single tick. - private HashSet _deletionQueue = []; - private EntityQuery _puddleQuery; /* @@ -79,16 +57,11 @@ public override void Initialize() _puddleQuery = GetEntityQuery(); - // Shouldn't need re-anchoring. - SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnPuddleSpread); SubscribeLocalEvent(OnPuddleSlip); - - SubscribeLocalEvent(OnEvaporationMapInit); - - InitializeTransfers(); } + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged private void OnPuddleSpread(Entity entity, ref SpreadNeighborsEvent args) { // Overflow is the source of the overflowing liquid. This contains the excess fluid above overflow limit (20u) @@ -275,6 +248,7 @@ private void OnPuddleSpread(Entity entity, ref SpreadNeighborsE } } + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged private void OnPuddleSlip(Entity entity, ref SlipEvent args) { // Reactive entities have a chance to get a touch reaction from slipping on a puddle @@ -291,168 +265,12 @@ private void OnPuddleSlip(Entity entity, ref SlipEvent args) out var solution)) return; - _popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)), + Popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)), args.Slipped, args.Slipped, PopupType.SmallCaution); // Take 15% of the puddle solution var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f); - _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); - } - - /// - public override void Update(float frameTime) - { - base.Update(frameTime); - foreach (var ent in _deletionQueue) - { - Del(ent); - } - - _deletionQueue.Clear(); - - TickEvaporation(); - } - - protected override void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) - { - if (args.SolutionId != entity.Comp.SolutionName) - return; - - base.OnSolutionUpdate(entity, ref args); - - if (args.Solution.Volume <= 0) - { - _deletionQueue.Add(entity); - return; - } - - _deletionQueue.Remove(entity); - UpdateSlip((entity, entity.Comp), args.Solution); - UpdateSlow(entity, args.Solution); - UpdateEvaporation(entity, args.Solution); - } - - private void UpdateSlip(Entity entity, Solution solution) - { - if (!TryComp(entity, out var comp)) - return; - - // Ensure we actually have the component - EnsureComp(entity); - - EnsureComp(entity, out var slipComp); - - // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on - // the sprite threshold for a puddle larger than 5 pixels. - var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold); - - // Stores how many units of slippery reagents a puddle has - var slipperyUnits = FixedPoint2.Zero; - // Stores how many units of super slippery reagents a puddle has - var superSlipperyUnits = FixedPoint2.Zero; - - // These three values will be averaged later and all start at zero so the calculations work - // A cumulative weighted amount of minimum speed to slip values - var puddleFriction = FixedPoint2.Zero; - // A cumulative weighted amount of minimum speed to slip values - var slipStepTrigger = FixedPoint2.Zero; - // A cumulative weighted amount of launch multipliers from slippery reagents - var launchMult = FixedPoint2.Zero; - // A cumulative weighted amount of stun times from slippery reagents - var stunTimer = TimeSpan.Zero; - // A cumulative weighted amount of knockdown times from slippery reagents - var knockdownTimer = TimeSpan.Zero; - - // Check if the puddle is big enough to slip in to avoid doing unnecessary logic - if (solution.Volume <= smallPuddleThreshold) - { - _stepTrigger.SetActive(entity, false, comp); - _tile.SetModifier(entity, 1f); - slipComp.SlipData.SlipFriction = 1f; - slipComp.AffectsSliding = false; - Dirty(entity, slipComp); - return; - } - - slipComp.AffectsSliding = true; - - foreach (var (reagent, quantity) in solution.Contents) - { - var reagentProto = _prototypeManager.Index(reagent.Prototype); - - // Calculate the minimum speed needed to slip in the puddle. Average the overall slip thresholds for all reagents - var deltaSlipTrigger = reagentProto.SlipData?.RequiredSlipSpeed ?? entity.Comp.DefaultSlippery; - slipStepTrigger += quantity * deltaSlipTrigger; - - // Aggregate Friction based on quantity - puddleFriction += reagentProto.Friction * quantity; - - if (reagentProto.SlipData == null) - continue; - - slipperyUnits += quantity; - // Aggregate launch speed based on quantity - launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity; - // Aggregate stun times based on quantity - stunTimer += reagentProto.SlipData.StunTime * (float)quantity; - knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity; - - if (reagentProto.SlipData.SuperSlippery) - superSlipperyUnits += quantity; - } - - // Turn on the step trigger if it's slippery - _stepTrigger.SetActive(entity, slipperyUnits > smallPuddleThreshold, comp); - - // This is based of the total volume and not just the slippery volume because there is a default - // slippery for all reagents even if they aren't technically slippery. - slipComp.SlipData.RequiredSlipSpeed = (float)(slipStepTrigger / solution.Volume); - _stepTrigger.SetRequiredTriggerSpeed(entity, slipComp.SlipData.RequiredSlipSpeed); - - // Divide these both by only total amount of slippery reagents. - // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount. - if (slipperyUnits > 0) - { - slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits); - slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits); - slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits); - } - - // Only make it super slippery if there is enough super slippery units for its own puddle - slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold; - - // Lower tile friction based on how slippery it is, lets items slide across a puddle of lube - slipComp.SlipData.SlipFriction = (float)(puddleFriction / solution.Volume); - _tile.SetModifier(entity, slipComp.SlipData.SlipFriction); - - Dirty(entity, slipComp); - } - - private void UpdateSlow(EntityUid uid, Solution solution) - { - var maxViscosity = 0f; - foreach (var (reagent, _) in solution.Contents) - { - var reagentProto = _prototypeManager.Index(reagent.Prototype); - maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity); - } - - if (maxViscosity > 0) - { - var comp = EnsureComp(uid); - var speed = 1 - maxViscosity; - _speedModContacts.ChangeSpeedModifiers(uid, speed, comp); - } - else - { - RemComp(uid); - } - } - - private void OnAnchorChanged(Entity entity, ref AnchorStateChangedEvent args) - { - if (!args.Anchored) - QueueDel(entity); + Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); } /// @@ -509,7 +327,7 @@ public bool TryAddSolution(EntityUid puddleUid, return true; } - _audio.PlayPvs(puddleComponent.SpillSound, puddleUid); + Audio.PlayPvs(puddleComponent.SpillSound, puddleUid); return true; } @@ -555,8 +373,42 @@ public Solution GetOverflowSolution(EntityUid uid, PuddleComponent? puddle = nul #region Spill + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged /// - public override bool TrySplashSpillAt(EntityUid uid, + public override bool TrySplashSpillAt(Entity entity, + EntityCoordinates coordinates, + out EntityUid puddleUid, + out Solution spilled, + bool sound = true, + EntityUid? user = null) + { + puddleUid = EntityUid.Invalid; + spilled = new Solution(); + + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var solution)) + return false; + + spilled = solution.Value.Comp.Solution; + + return TrySplashSpillAt(entity, coordinates, solution.Value, out puddleUid, sound, user); + } + + private bool TrySplashSpillAt(EntityUid entity, + EntityCoordinates coordinates, + Entity solution, + out EntityUid puddleUid, + bool sound = true, + EntityUid? user = null) + { + var result = TrySplashSpillAt(entity, coordinates, solution.Comp.Solution, out puddleUid, sound, user); + _solutionContainerSystem.UpdateChemicals(solution); + return result; + } + + public override bool TrySplashSpillAt(EntityUid entity, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, @@ -568,6 +420,7 @@ public override bool TrySplashSpillAt(EntityUid uid, if (solution.Volume == 0) return false; + var spilled = solution.SplitSolution(solution.Volume); var targets = new List(); var reactive = new HashSet>(); _lookup.GetEntitiesInRange(coordinates, 1.0f, reactive); @@ -579,26 +432,28 @@ public override bool TrySplashSpillAt(EntityUid uid, var owner = ent.Owner; // between 5 and 30% - var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f); - var splitSolution = solution.SplitSolution(splitAmount); + var splitAmount = spilled.Volume * _random.NextFloat(0.05f, 0.30f); + var splitSolution = spilled.SplitSolution(splitAmount); if (user != null) { - _adminLogger.Add(LogType.Landed, - $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}"); + AdminLogger.Add(LogType.Landed, + $"{ToPrettyString(user.Value):user} threw {ToPrettyString(entity):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(spilled):solution} onto {ToPrettyString(owner):target}"); } targets.Add(owner); - _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); - _popups.PopupEntity( - Loc.GetString("spill-land-spilled-on-other", ("spillable", uid), - ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution); + Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); + Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", + ("spillable", entity), + ("target", Identity.Entity(owner, EntityManager))), + owner, + PopupType.SmallCaution); } - _color.RaiseEffect(solution.GetColor(_prototypeManager), targets, - Filter.Pvs(uid, entityManager: EntityManager)); + _color.RaiseEffect(spilled.GetColor(_prototypeManager), targets, + Filter.Pvs(entity, entityManager: EntityManager)); - return TrySpillAt(coordinates, solution, out puddleUid, sound); + return TrySpillAt(coordinates, spilled, out puddleUid, sound); } /// diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs index 13695caff14..c787132504f 100644 --- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -21,7 +21,7 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using System.Linq; - +using Content.Shared.EntityEffects.Effects.Solution; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Fluids.EntitySystems; @@ -264,31 +264,30 @@ private void ReactWithEntity(EntityUid entity, EntityUid smokeUid, Solution solu if (!TryComp(entity, out var bloodstream)) return; - if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.ChemicalSolutionName, ref bloodstream.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0) + if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution) || bloodSolution.AvailableVolume <= 0) return; var blockIngestion = _internals.AreInternalsWorking(entity); var cloneSolution = solution.Clone(); var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate); - var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume); + var transferAmount = FixedPoint2.Min(availableTransfer, bloodSolution.AvailableVolume); var transferSolution = cloneSolution.SplitSolution(transferAmount); foreach (var reagentQuantity in transferSolution.Contents.ToArray()) { if (reagentQuantity.Quantity == FixedPoint2.Zero) continue; - var reagentProto = _prototype.Index(reagentQuantity.Reagent.Prototype); - _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution); + _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentQuantity); if (!blockIngestion) - _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution); + _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity); } if (blockIngestion) return; - if (_blood.TryAddToChemicals((entity, bloodstream), transferSolution)) + if (_blood.TryAddToBloodstream((entity, bloodstream), transferSolution)) { // Log solution addition by smoke _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}"); diff --git a/Content.Server/Fluids/EntitySystems/SpraySystem.cs b/Content.Server/Fluids/EntitySystems/SpraySystem.cs index 2a6b0644be3..3b7b4327c4d 100644 --- a/Content.Server/Fluids/EntitySystems/SpraySystem.cs +++ b/Content.Server/Fluids/EntitySystems/SpraySystem.cs @@ -1,6 +1,5 @@ using Content.Server.Chemistry.Components; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Fluids.Components; using Content.Server.Gravity; using Content.Server.Popups; using Content.Shared.CCVar; @@ -16,11 +15,14 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; using System.Numerics; +using Content.Shared.Fluids.EntitySystems; +using Content.Shared.Fluids.Components; +using Robust.Server.Containers; using Robust.Shared.Map; namespace Content.Server.Fluids.EntitySystems; -public sealed class SpraySystem : EntitySystem +public sealed class SpraySystem : SharedSpraySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly GravitySystem _gravity = default!; @@ -33,6 +35,7 @@ public sealed class SpraySystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ContainerSystem _container = default!; private float _gridImpulseMultiplier; @@ -54,7 +57,7 @@ private void OnActivateInWorld(Entity entity, ref UserActivateIn var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery().GetComponent(args.Target)); - Spray(entity, args.User, targetMapPos); + Spray(entity, targetMapPos, args.User); } private void UpdateGridMassMultiplier(float value) @@ -71,36 +74,49 @@ private void OnAfterInteract(Entity entity, ref AfterInteractEve var clickPos = _transform.ToMapCoordinates(args.ClickLocation); - Spray(entity, args.User, clickPos); + Spray(entity, clickPos, args.User); } - public void Spray(Entity entity, EntityUid user, MapCoordinates mapcoord) + public override void Spray(Entity entity, EntityUid? user = null) { - if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution)) + var xform = Transform(entity); + var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance; + var direction = xform.Coordinates.Offset(throwing); + + Spray(entity, _transform.ToMapCoordinates(direction), user); + } + + public override void Spray(Entity entity, MapCoordinates mapcoord, EntityUid? user = null) + { + if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution)) return; var ev = new SprayAttemptEvent(user); RaiseLocalEvent(entity, ref ev); if (ev.Cancelled) + { + if (ev.CancelPopupMessage != null && user != null) + _popupSystem.PopupEntity(Loc.GetString(ev.CancelPopupMessage), entity.Owner, user.Value); return; + } - if (TryComp(entity, out var useDelay) - && _useDelay.IsDelayed((entity, useDelay))) + if (_useDelay.IsDelayed((entity, null))) return; if (solution.Volume <= 0) { - _popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user); + if (user != null) + _popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value); return; } var xformQuery = GetEntityQuery(); - var userXform = xformQuery.GetComponent(user); + var sprayerXform = xformQuery.GetComponent(entity); - var userMapPos = _transform.GetMapCoordinates(userXform); + var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform); var clickMapPos = mapcoord; - var diffPos = clickMapPos.Position - userMapPos.Position; + var diffPos = clickMapPos.Position - sprayerMapPos.Position; if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN) return; @@ -127,12 +143,12 @@ public void Spray(Entity entity, EntityUid user, MapCoordinates Angle.FromDegrees(spread * (amount - 1) / 2)); // Calculate the destination for the vapor cloud. Limit to the maximum spray distance. - var target = userMapPos + var target = sprayerMapPos .Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter); - var distance = (target.Position - userMapPos.Position).Length(); + var distance = (target.Position - sprayerMapPos.Position).Length(); if (distance > entity.Comp.SprayDistance) - target = userMapPos.Offset(diffNorm * entity.Comp.SprayDistance); + target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance); var adjustedSolutionAmount = entity.Comp.TransferAmount / entity.Comp.VaporAmount; var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount); @@ -141,7 +157,7 @@ public void Spray(Entity entity, EntityUid user, MapCoordinates break; // Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is. - var vaporPos = userMapPos.Offset(distance < 1 ? quarter : threeQuarters); + var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters); var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos); var vaporXform = xformQuery.GetComponent(vapor); @@ -164,17 +180,21 @@ public void Spray(Entity entity, EntityUid user, MapCoordinates _vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user); - if (TryComp(user, out var body)) + var thingGettingPushed = entity.Owner; + if (_container.TryGetOuterContainer(entity, sprayerXform, out var container)) + thingGettingPushed = container.Owner; + + if (TryComp(thingGettingPushed, out var body)) { - if (_gravity.IsWeightless(user)) + if (_gravity.IsWeightless(thingGettingPushed)) { // push back the player - _physics.ApplyLinearImpulse(user, -impulseDirection * entity.Comp.PushbackAmount, body: body); + _physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body); } else { // push back the grid the player is standing on - var userTransform = Transform(user); + var userTransform = Transform(thingGettingPushed); if (userTransform.GridUid == userTransform.ParentUid) { // apply both linear and angular momentum depending on the player position @@ -187,7 +207,6 @@ public void Spray(Entity entity, EntityUid user, MapCoordinates _audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f)); - if (useDelay != null) - _useDelay.TryResetDelay((entity, useDelay)); + _useDelay.TryResetDelay(entity); } } diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs index 8370014ad66..4c28265fed5 100644 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs @@ -13,6 +13,8 @@ using Content.Shared.Forensics; using Content.Shared.Forensics.Components; using Content.Shared.Forensics.Systems; +using Content.Shared.Gibbing; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; @@ -39,7 +41,7 @@ public override void Initialize() // The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing SubscribeLocalEvent(OnDNAInit, after: new[] { typeof(BloodstreamSystem) }); - SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent(OnBeingGibbed); SubscribeLocalEvent(OnMeleeHit); SubscribeLocalEvent(OnRehydrated); SubscribeLocalEvent(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) }); @@ -85,14 +87,14 @@ private void OnDNAInit(Entity ent, ref MapInitEvent args) } } - private void OnBeingGibbed(EntityUid uid, ForensicsComponent component, BeingGibbedEvent args) + private void OnBeingGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) { string dna = Loc.GetString("forensics-dna-unknown"); - if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null) + if (TryComp(ent, out DnaComponent? dnaComp) && dnaComp.DNA != null) dna = dnaComp.DNA; - foreach (EntityUid part in args.GibbedParts) + foreach (var part in args.Giblets) { var partComp = EnsureComp(part); partComp.DNAs.Add(dna); @@ -196,8 +198,8 @@ private void OnUtilityVerb(Entity entity, ref GetVerbs { Act = () => TryStartCleaning(entity, user, target), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")), - Text = Loc.GetString(Loc.GetString("forensics-verb-text")), - Message = Loc.GetString(Loc.GetString("forensics-verb-message")), + Text = Loc.GetString("forensics-verb-text"), + Message = Loc.GetString("forensics-verb-message"), // This is important because if its true using the cleaning device will count as touching the object. DoContactInteraction = false }; @@ -216,7 +218,7 @@ public bool TryStartCleaning(Entity cleanForensicsEnti { if (!TryComp(target, out var forensicsComp)) { - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", target)), user, user, PopupType.MediumCaution); + _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); return false; } @@ -237,13 +239,13 @@ public bool TryStartCleaning(Entity cleanForensicsEnti _doAfterSystem.TryStartDoAfter(doAfterArgs); - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning", ("target", target)), user, user); + _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning", ("target", Identity.Entity(target, EntityManager))), user, user); return true; } else { - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", target)), user, user, PopupType.MediumCaution); + _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); return false; } diff --git a/Content.Server/GameTicking/Commands/ForceMapCommand.cs b/Content.Server/GameTicking/Commands/ForceMapCommand.cs index 577464acadb..ec42d971796 100644 --- a/Content.Server/GameTicking/Commands/ForceMapCommand.cs +++ b/Content.Server/GameTicking/Commands/ForceMapCommand.cs @@ -3,6 +3,7 @@ using Content.Server.Maps; using Content.Shared.Administration; using Content.Shared.CCVar; +using Content.Shared.Maps; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -22,7 +23,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 1) { - shell.WriteLine(Loc.GetString(Loc.GetString($"shell-need-exactly-one-argument"))); + shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument")); return; } diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index a32a2f94955..3f5e294754a 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -17,6 +17,9 @@ sealed class JoinGameCommand : IConsoleCommand [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private readonly ISawmill _sawmill; public string Command => "joingame"; public string Description => ""; @@ -25,7 +28,10 @@ sealed class JoinGameCommand : IConsoleCommand public JoinGameCommand() { IoCManager.InjectDependencies(this); + + _sawmill = _logManager.GetSawmill("security"); } + public void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 2) @@ -46,8 +52,8 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) && status == PlayerGameStatus.JoinedGame) { - Logger.InfoS("security", $"{player.Name} ({player.UserId}) attempted to latejoin while in-game."); - shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported."); + _sawmill.Info($"{player.Name} ({player.UserId}) attempted to latejoin while in-game."); + shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported."); return; } diff --git a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs index 6114a4ca0d3..5c02dea5132 100644 --- a/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs +++ b/Content.Server/GameTicking/Commands/SetGamePresetCommand.cs @@ -2,9 +2,7 @@ using Content.Server.Administration; using Content.Server.GameTicking.Presets; using Content.Shared.Administration; -using Linguini.Shared.Util; using Robust.Shared.Console; -using Robust.Shared.Prototypes; namespace Content.Server.GameTicking.Commands { @@ -12,7 +10,6 @@ namespace Content.Server.GameTicking.Commands public sealed class SetGamePresetCommand : IConsoleCommand { [Dependency] private readonly IEntityManager _entity = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; public string Command => "setgamepreset"; public string Description => Loc.GetString("set-game-preset-command-description", ("command", Command)); @@ -20,7 +17,7 @@ public sealed class SetGamePresetCommand : IConsoleCommand public void Execute(IConsoleShell shell, string argStr, string[] args) { - if (!args.Length.InRange(1, 3)) + if (args.Length is < 1 or > 3) { shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 1), ("upper", 3), ("currentAmount", args.Length))); return; diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index 40608e45cbc..4634b7820bc 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -4,6 +4,7 @@ using Content.Server.GameTicking.Presets; using Content.Server.Maps; using Content.Shared.CCVar; +using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Shared.Player; @@ -35,6 +36,7 @@ public sealed partial class GameTicker private bool StartPreset(ICommonSession[] origReadyPlayers, bool force) { + _sawmill.Info($"Attempting to start preset '{CurrentPreset?.ID}'"); var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force); RaiseLocalEvent(startAttempt); @@ -56,10 +58,13 @@ void FailedPresetRestart() var fallbackPresets = _cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(","); var startFailed = true; + _sawmill.Info($"Fallback - Failed to start round, attempting to start fallback presets."); foreach (var preset in fallbackPresets) { + _sawmill.Info($"Fallback - Clearing up gamerules"); ClearGameRules(); - SetGamePreset(preset); + _sawmill.Info($"Fallback - Attempting to start '{preset}'"); + SetGamePreset(preset, resetDelay: 1); AddGamePresetRules(); StartGamePresetRules(); @@ -76,6 +81,7 @@ void FailedPresetRestart() startFailed = false; break; } + _sawmill.Info($"Fallback - '{preset}' failed to start."); } if (startFailed) @@ -87,6 +93,7 @@ void FailedPresetRestart() else { + _sawmill.Info($"Fallback - Failed to start preset but fallbacks are disabled. Returning to Lobby."); FailedPresetRestart(); return false; } @@ -129,11 +136,11 @@ public void SetGamePreset(GamePresetPrototype? preset, bool force = false, GameP } } - public void SetGamePreset(string preset, bool force = false) + public void SetGamePreset(string preset, bool force = false, int? resetDelay = null) { var proto = FindGamePreset(preset); if (proto != null) - SetGamePreset(proto, force); + SetGamePreset(proto, force, null, resetDelay); } public GamePresetPrototype? FindGamePreset(string preset) diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 1750d3c27a0..cf1d6eb1a89 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -5,6 +5,7 @@ using Content.Shared.Database; using Content.Shared.GameTicking.Components; using Content.Shared.Prototypes; +using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Console; using Robust.Shared.Map; @@ -17,6 +18,8 @@ public sealed partial class GameTicker { [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new(); + [Dependency] private readonly EntityWhitelistSystem _whitelist = null!; + /// /// A list storing the start times of all game rules that have been started this round. /// Game rules can be started and stopped at any time, including midround. @@ -233,6 +236,22 @@ public bool IsGameRuleAdded(string rule) return false; } + /// + /// Returns true if a game rule that passes the whitelist and blacklist has been added. + /// + /// whitelist for the game rules + /// blacklist for the game rules + public bool IsGameRuleAdded(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) + { + foreach (var ruleEntity in GetAddedGameRules()) + { + if (_whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) + return true; + } + + return false; + } + /// /// Returns true if a game rule with the given component is active.. /// @@ -265,6 +284,22 @@ public bool IsGameRuleActive(string rule) return false; } + /// + /// Returns true if a game rule that passes the whitelist and blacklist is active. + /// + /// whitelist for the game rules + /// blacklist for the game rules + public bool IsGameRuleActive(EntityWhitelist? ruleWhitelist, EntityWhitelist? ruleBlacklist = null) + { + foreach (var ruleEntity in GetActiveGameRules()) + { + if (_whitelist.CheckBoth(ruleEntity, ruleBlacklist, ruleWhitelist)) + return true; + } + + return false; + } + public void ClearGameRules() { foreach (var rule in GetAddedGameRules()) @@ -274,7 +309,7 @@ public void ClearGameRules() } /// - /// Gets all the gamerule entities which are currently active. + /// Gets all the gamerule entities that have been added. /// public IEnumerable GetAddedGameRules() { @@ -286,6 +321,20 @@ public IEnumerable GetAddedGameRules() } } + /// + /// Gets all the gamerule entities with {T} component that have been added. + /// + public IEnumerable> GetAddedGameRules() where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var ruleData)) + { + if (IsGameRuleAdded(uid, ruleData)) + yield return (uid, comp); + } + } + + /// /// Gets all the gamerule entities which are currently active. /// @@ -298,6 +347,18 @@ public IEnumerable GetActiveGameRules() } } + /// + /// Gets all the gamerule entities with {T} component that are currently active. + /// + public IEnumerable> GetActiveGameRules() where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out _, out _)) + { + yield return (uid, comp); + } + } + /// /// Gets all gamerule prototypes /// diff --git a/Content.Server/GameTicking/GameTicker.LobbyBackground.cs b/Content.Server/GameTicking/GameTicker.LobbyBackground.cs index 2090e3e31f0..82460572f7d 100644 --- a/Content.Server/GameTicking/GameTicker.LobbyBackground.cs +++ b/Content.Server/GameTicking/GameTicker.LobbyBackground.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Prototypes; +using Content.Shared.GameTicking.Prototypes; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; @@ -8,24 +9,37 @@ namespace Content.Server.GameTicking; public sealed partial class GameTicker { [ViewVariables] - public string? LobbyBackground { get; private set; } + public ProtoId? LobbyBackground { get; private set; } [ViewVariables] - private List? _lobbyBackgrounds; + private List>? _lobbyBackgrounds; private static readonly string[] WhitelistedBackgroundExtensions = new string[] {"png", "jpg", "jpeg", "webp"}; private void InitializeLobbyBackground() { - _lobbyBackgrounds = _prototypeManager.EnumeratePrototypes() - .Select(x => x.Background) - .Where(x => WhitelistedBackgroundExtensions.Contains(x.Extension)) - .ToList(); + var allprotos = _prototypeManager.EnumeratePrototypes().ToList(); + _lobbyBackgrounds ??= new List>(); + + //create protoids from them + foreach (var proto in allprotos) + { + var ext = proto.Background.Extension; + if (!WhitelistedBackgroundExtensions.Contains(ext)) + continue; + + //create a protoid and add it to the list + _lobbyBackgrounds.Add(new ProtoId(proto.ID)); + } RandomizeLobbyBackground(); } - private void RandomizeLobbyBackground() { - LobbyBackground = _lobbyBackgrounds!.Any() ? _robustRandom.Pick(_lobbyBackgrounds!).ToString() : null; + private void RandomizeLobbyBackground() + { + if (_lobbyBackgrounds != null && _lobbyBackgrounds.Count != 0) + LobbyBackground = _robustRandom.Pick(_lobbyBackgrounds); + else + LobbyBackground = null; } } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index 336fe4c6af1..5ceb470e46e 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -1,5 +1,6 @@ using Content.Shared.Administration; using Content.Shared.CCVar; +using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.GameWindow; using Content.Shared.Players; @@ -93,6 +94,7 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar else SpawnWaitDb(); + _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); break; } @@ -119,6 +121,8 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar } } + _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} connected to the game."); + break; } @@ -131,6 +135,8 @@ private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs ar } _userDb.ClientDisconnected(session); + + _adminLogger.Add(LogType.Connection, LogImpact.Low, $"User {args.Session:Player} attached to {(args.Session.AttachedEntity != null ? ToPrettyString(args.Session.AttachedEntity) : "nothing"):entity} disconnected from the game."); break; } } diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 97284889521..a6fc9eb7f68 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -9,6 +9,7 @@ using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.GameTicking; +using Content.Shared.Maps; using Content.Shared.Mind; using Content.Shared.Players; using Content.Shared.Preferences; @@ -27,8 +28,6 @@ using Content.Shared.Imperial.ICCVar; //Imperial using Content.Shared.Voting; //Imperial using Content.Server.Voting.Managers; -using Content.Shared.Imperial.ICCVar; //Imperial -using Robust.Shared.Configuration; //Imperial namespace Content.Server.GameTicking { @@ -401,7 +400,9 @@ public void StartRound(bool force = false) } else { - profile = HumanoidCharacterProfile.Random(); + var speciesToBlacklist = + new HashSet(_cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(",")); + profile = HumanoidCharacterProfile.Random(speciesToBlacklist); } readyPlayerProfiles.Add(userId, profile); } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index d5b16c4f5bd..768aa1ab686 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -201,7 +201,7 @@ private void SpawnPlayer(ICommonSession player, } speciesId = roundStart.Count == 0 - ? SharedHumanoidAppearanceSystem.DefaultSpecies + ? HumanoidCharacterProfile.DefaultSpecies : _robustRandom.Pick(roundStart); } else @@ -211,6 +211,7 @@ private void SpawnPlayer(ICommonSession player, } character = HumanoidCharacterProfile.RandomWithSpecies(speciesId); + character.Appearance = HumanoidCharacterAppearance.EnsureValid(character.Appearance, character.Species, character.Sex); } // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc) @@ -254,28 +255,7 @@ private void SpawnPlayer(ICommonSession player, return; } - PlayerJoinGame(player, silent); - - var data = player.ContentData(); - - DebugTools.AssertNotNull(data); - - var newMind = _mind.CreateMind(data!.UserId, character.Name); - _mind.SetUserId(newMind, data.UserId); - - var jobPrototype = _prototypeManager.Index(jobId); - - _playTimeTrackings.PlayerRolesChanged(player); - - var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character); - DebugTools.AssertNotNull(mobMaybe); - var mob = mobMaybe!.Value; - - _mind.TransferTo(newMind, mob); - - _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId); - var jobName = _jobs.MindTryGetJobName(newMind); - _admin.UpdatePlayerList(player); + DoSpawn(player, character, station, jobId, silent, out var mob, out var jobPrototype, out var jobName); if (lateJoin && !silent) { @@ -348,6 +328,43 @@ private void SpawnPlayer(ICommonSession player, RaiseLocalEvent(mob, aev, true); } + /// + /// Creates a mob on the specified station, creates the new mind, equips job-specific starting gear and loadout + /// + public void DoSpawn( + ICommonSession player, + HumanoidCharacterProfile character, + EntityUid station, + string jobId, + bool silent, + out EntityUid mob, + out JobPrototype jobPrototype, + out string jobName) + { + PlayerJoinGame(player, silent); + + var data = player.ContentData(); + + DebugTools.AssertNotNull(data); + + jobPrototype = _prototypeManager.Index(jobId); + + var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character); + DebugTools.AssertNotNull(mobMaybe); + mob = mobMaybe!.Value; + + var newMind = _mind.CreateMind(data.UserId, Name(mob)); + _mind.SetUserId(newMind, data.UserId); + + _playTimeTrackings.PlayerRolesChanged(player); + + _mind.TransferTo(newMind, mob); + + _roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId); + jobName = _jobs.MindTryGetJobName(newMind); + _admin.UpdatePlayerList(player); + } + public void Respawn(ICommonSession player) { _mind.WipeMind(player); diff --git a/Content.Server/GameTicking/Prototypes/LobbyBackgroundPrototype.cs b/Content.Server/GameTicking/Prototypes/LobbyBackgroundPrototype.cs deleted file mode 100644 index 5201fb389a2..00000000000 --- a/Content.Server/GameTicking/Prototypes/LobbyBackgroundPrototype.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking.Prototypes; - -/// -/// Prototype for a lobby background the game can choose. -/// -[Prototype] -public sealed partial class LobbyBackgroundPrototype : IPrototype -{ - /// - [IdDataField] - public string ID { get; set; } = default!; - - /// - /// The sprite to use as the background. This should ideally be 1920x1080. - /// - [DataField("background", required: true)] - public ResPath Background = default!; -} diff --git a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs index 84f87a487bd..22916f0c18e 100644 --- a/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs @@ -2,6 +2,7 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Humanoid; using Content.Server.Preferences.Managers; +using Content.Shared.Body; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; @@ -11,9 +12,10 @@ namespace Content.Server.GameTicking.Rules; public sealed class AntagLoadProfileRuleSystem : GameRuleSystem { - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IServerPreferencesManager _prefs = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; public override void Initialize() { @@ -34,7 +36,7 @@ private void OnSelectEntity(Entity ent, ref Antag if (profile?.Species is not { } speciesId || !_proto.Resolve(speciesId, out var species)) { - species = _proto.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); + species = _proto.Index(HumanoidCharacterProfile.DefaultSpecies); } if (ent.Comp.SpeciesOverride != null @@ -44,6 +46,10 @@ private void OnSelectEntity(Entity ent, ref Antag } args.Entity = Spawn(species.Prototype); - _humanoid.LoadProfile(args.Entity.Value, profile?.WithSpecies(species.ID)); + if (profile?.WithSpecies(species.ID) is { } humanoidProfile) + { + _visualBody.ApplyProfileTo(args.Entity.Value, humanoidProfile); + _humanoidProfile.ApplyProfileTo(args.Entity.Value, humanoidProfile); + } } } diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs index f09fff5eafa..f85a5b8e851 100644 --- a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs @@ -1,6 +1,5 @@ -using Content.Server.GameTicking.Rules; -using Content.Server.Maps; using Content.Shared.GridPreloader.Prototypes; +using Content.Shared.Maps; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -34,7 +33,11 @@ public sealed partial class LoadMapRuleComponent : Component /// /// A to move to a new map. /// If there are no instances left nothing is done. + /// + /// This is deprecated. Do not create new content that uses this field, + /// and migrate existing content to be loaded dynamically during the round. + /// /// - [DataField] + [DataField, Obsolete("Do not pre-load grids. This causes the server to have to keep that grid loaded in memory during the entire round, even if that grid is never summoned to the playspace.")] public ProtoId? PreloadedGrid; } diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 5626f11e0e3..6d9c0de4999 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -141,5 +141,8 @@ public enum WinCondition : byte NukiesAbandoned, AllNukiesDead, SomeNukiesAlive, - AllNukiesAlive + AllNukiesAlive, + // Imperial Space EnergyCore start + CoreExploded + // Imperial Space EnergyCore end } diff --git a/Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs new file mode 100644 index 00000000000..4a018d7e6c6 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/XenoborgsRuleComponent.cs @@ -0,0 +1,46 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.GameTicking.Rules.Components; + +[RegisterComponent, Access(typeof(XenoborgsRuleSystem))] +[AutoGenerateComponentPause] +public sealed partial class XenoborgsRuleComponent : Component +{ + /// + /// When the round will next check for round end. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? NextRoundEndCheck; + + /// + /// The amount of time between each check for the end of the round. + /// + [DataField] + public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(15); + + /// + /// After this amount of the crew become xenoborgs, the shuttle will be automatically called. + /// + [DataField] + public float XenoborgShuttleCallPercentage = 0.7f; + + /// + /// The most xenoborgs that existed at one point. + /// + [DataField] + public int MaxNumberXenoborgs = 0; + + /// + /// If the announcment of the death of the mothership core was sent + /// + [DataField] + public bool MothershipCoreDeathAnnouncmentSent = false; + + /// + /// If the emergency shuttle trigged by was already called. + /// Will only call once. if a admin recalls it. it won't call again unless this is set to false by a admin + /// + [DataField] + public bool XenoborgShuttleCalled = false; +} diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 9ae8c5ff837..aafed207042 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -129,5 +129,6 @@ protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponen } args.AddLine(Loc.GetString("point-scoreboard-header")); args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup()); + args.AddLine(""); } } diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index cd93eae502b..8f1f08269b0 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -116,7 +116,7 @@ protected bool TryFindRandomTileOnStation(Entity station, tile = new Vector2i(randomX, randomY); if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile) - || _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp)) + || _atmosphere.IsTileAirBlockedCached(targetGrid, tile)) { continue; } diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index cb5b1175495..6b4240dd7ff 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -38,6 +38,8 @@ private void OnStartAttempt(RoundStartAttemptEvent args) while (query.MoveNext(out var uid, out _, out var gameRule)) { var minPlayers = gameRule.MinPlayers; + var name = ToPrettyString(uid); + if (args.Players.Length >= minPlayers) continue; @@ -46,8 +48,10 @@ private void OnStartAttempt(RoundStartAttemptEvent args) ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", ("readyPlayersCount", args.Players.Length), ("minimumPlayers", minPlayers), - ("presetName", ToPrettyString(uid)))); + ("presetName", name))); args.Cancel(); + //TODO remove this once announcements are logged + Log.Info($"Rule '{name}' requires {minPlayers} players, but only {args.Players.Length} are ready."); } else { diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 9c3562b1529..47a75ac4763 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -3,41 +3,64 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Nuke; using Content.Server.NukeOps; +using Content.Server.Pinpointer; using Content.Server.Popups; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Systems; using Content.Server.Station.Components; +using Content.Server.StationRecords.Systems; using Content.Server.Store.Systems; +using Content.Shared.Access.Systems; using Content.Shared.GameTicking.Components; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; +using Content.Shared.Roles; using Content.Shared.Roles.Components; +using Content.Shared.Roles.Jobs; +using Content.Shared.Station; +using Content.Shared.Station.Components; +using Content.Shared.StationRecords; using Content.Shared.Store; +using Content.Shared.Store.Components; using Content.Shared.Tag; using Content.Shared.Zombies; +using Robust.Server.Player; +using Robust.Shared.Containers; using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; +using System.Data; using System.Linq; -using Content.Shared.Station.Components; -using Content.Shared.Store.Components; -using Robust.Shared.Prototypes; +using System.Text; namespace Content.Server.GameTicking.Rules; -public sealed class NukeopsRuleSystem : GameRuleSystem +public sealed partial class NukeopsRuleSystem : GameRuleSystem // Добавлен модификатор partial Imperial { [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; + [Dependency] private readonly SharedIdCardSystem _idCard = default!; + [Dependency] private readonly SharedJobSystem _jobs = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly NavMapSystem _navMap = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly SharedStationSystem _station = default!; + [Dependency] private readonly StationRecordsSystem _records = default!; [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly TagSystem _tag = default!; @@ -65,6 +88,7 @@ public override void Initialize() SubscribeLocalEvent(OnAfterAntagEntSelected); SubscribeLocalEvent(OnRuleLoadedGrids); + InitializeImperial(); // Imperial Инициализация } protected override void Started(EntityUid uid, @@ -86,6 +110,8 @@ protected override void Started(EntityUid uid, return; component.TargetStation = RobustRandom.Pick(eligible); + var ev = new NukeopsTargetStationSelectedEvent(uid, component.TargetStation); + RaiseLocalEvent(ref ev); } #region Event Handlers @@ -103,6 +129,63 @@ protected override void AppendRoundEndText(EntityUid uid, args.AddLine(text); } + // Print disk location if nuke didn't explode and is not armed + List diskWinConditions = [WinCondition.NukeDiskOnCentCom, WinCondition.NukeDiskNotOnCentCom]; + if (component.WinConditions.Any(diskWinConditions.Contains)) + { + var diskQuery = AllEntityQuery(); + while (diskQuery.MoveNext(out var diskUid, out _, out var transform)) + { + StringBuilder text = new StringBuilder(Loc.GetString("nukeops-disk-location-title")); + + List containers = new List(); + bool carriedByMob = false; + + var tempParent = diskUid; + while (_containers.TryGetContainingContainer((tempParent, null), out var container) && !carriedByMob) + { + if (HasComp(container.Owner)) + { + carriedByMob = true; + } + var containermeta = MetaData(container.Owner); + containers.Add(containermeta.EntityName); + tempParent = container.Owner; + } + + string location = FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((diskUid, transform))); + + if (carriedByMob) + { + GetDiskCarrierData(tempParent, out var name, out var job, out var username); + text.Append(Loc.GetString("nukeops-disk-carried-by", + ("name", name), + ("job", job), + ("user", username), + ("location", location))); + } + else + { + if (containers.Count > 0) + { + string hierarchy = string.Empty; + for (var i = 0; i < containers.Count; i++) + { + hierarchy = (Loc.GetString( + "storage-hierarchy-list", + ("item", containers[i]), + ("existing-text", hierarchy), + ("items-left", containers.Count - i - 1))); + } + text.Append(hierarchy); + } + text.Append(" "); + text.Append(location); + } + args.AddLine(text.ToString()); + } + } + args.AddLine(Loc.GetString("nukeops-list-start")); var antags = _antag.GetAntagIdentifiers(uid); @@ -111,6 +194,7 @@ protected override void AppendRoundEndText(EntityUid uid, { args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName))); } + args.AddLine(""); } private void OnNukeExploded(NukeExplodedEvent ev) @@ -178,7 +262,6 @@ private void OnNukeExploded(NukeExplodedEvent ev) } } } - private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { if (ev.New is not GameRunLevel.PostRound) @@ -547,4 +630,89 @@ private void OnGetBriefing(Entity role, ref GetBriefingEve return null; } + + private void GetDiskCarrierData(EntityUid carrier, + out string name, + out string job, + out string username) + { + name = Name(carrier); + job = Loc.GetString("job-name-unknown"); + username = "unknown"; // magic word in Fluent selector + + Entity? mind = null; + + if (_mind.TryGetMind(carrier, out _, out var mindComp)) + { + mind = (carrier, mindComp); + } + else + { + var allMinds = EntityQueryEnumerator(); + while (allMinds.MoveNext(out _, out mindComp)) + { + if (mindComp.CharacterName != name) + continue; + + mind = (carrier, mindComp); + break; + } + } + + if (mind is not null) + { + NetUserId? userId = mind.Value.Comp.UserId; + if (userId is not null && _player.TryGetPlayerData(userId.Value, out var sessionData)) + username = sessionData.UserName; + + // Role/job is the trickiest since it can be unknown in some cases + // For example, after "make ghost role" verb + var roles = _roles.MindGetAllRoleInfo(mind.Value.Owner); + if (roles.Count > 0) + { + job = Loc.GetString(roles.First().Name); + return; + } + + if (_jobs.MindTryGetJobName(mind, out var jobName)) + { + job = jobName; + return; + } + } + + // Try station records + var xform = Transform(carrier); + var station = _station.GetStationInMap(xform.MapID); + if (station != null && _records.GetRecordByName(station.Value, name) is { } id) + { + var key = new StationRecordKey(id, station.Value); + if (_records.TryGetRecord(key, out var record)) + { + job = record.JobTitle; + return; + } + } + + // Fallback to ID + if (_idCard.TryFindIdCard(carrier, out var idCard)) + job = idCard.Comp.LocalizedJobTitle ?? job; + } +} + +/// +/// Raised when a station has been assigned as a target for the NukeOps rule. +/// +[ByRefEvent] +public readonly struct NukeopsTargetStationSelectedEvent(EntityUid ruleEntity, EntityUid? targetStation) +{ + /// + /// The entity containing the NukeOps gamerule. + /// + public readonly EntityUid RuleEntity = ruleEntity; + + /// + /// The target station, if it exists. + /// + public readonly EntityUid? TargetStation = targetStation; } diff --git a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs b/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs index ab8864caaa7..7eb076d0d52 100644 --- a/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs @@ -7,17 +7,20 @@ using Content.Shared.Gibbing.Components; using Content.Shared.Medical.SuitSensor; using Content.Shared.Mind; +using Content.Shared.Objectives.Systems; +using Content.Shared.Random.Helpers; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; public sealed class ParadoxCloneRuleSystem : GameRuleSystem { - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly CloningSystem _cloning = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SuitSensorSystem _sensor = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -32,7 +35,7 @@ protected override void Started(EntityUid uid, ParadoxCloneRuleComponent compone base.Started(uid, component, gameRule, args); // check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up - var allHumans = _mind.GetAliveHumans(); + var allHumans = _target.GetAliveHumans(); if (allHumans.Count == 0) { @@ -59,7 +62,7 @@ private void OnAntagSelectEntity(Entity ent, ref Anta else { // get possible targets - var allAliveHumanoids = _mind.GetAliveHumans(); + var allAliveHumanoids = _target.GetAliveHumans(); // we already checked when starting the gamerule, but someone might have died since then. if (allAliveHumanoids.Count == 0) diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index 8df75f5e771..41049682731 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -118,6 +118,7 @@ protected override void AppendRoundEndText(EntityUid uid, // TODO: someone suggested listing all alive? revs maybe implement at some point } + args.AddLine(""); } private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args) @@ -150,10 +151,11 @@ private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref Aft if (HasComp(ev.Target) || HasComp(ev.Target) || - !HasComp(ev.Target) && + !HasComp(ev.Target) && !alwaysConvertible || !_mobState.IsAlive(ev.Target) || - HasComp(ev.Target)) + HasComp(ev.Target) || + !HasComp(ev.Used)) { return; } @@ -395,7 +397,7 @@ public bool IsConvertable(EntityUid uid) // - Мертв или является зомби if (HasComp(uid) || HasComp(uid) || - !HasComp(uid) && !alwaysConvertible || + !HasComp(uid) && !alwaysConvertible || !_mobState.IsAlive(uid) || HasComp(uid)) { diff --git a/Content.Server/GameTicking/Rules/RuleGridsSystem.cs b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs index 9eae9e3c95f..ef182339096 100644 --- a/Content.Server/GameTicking/Rules/RuleGridsSystem.cs +++ b/Content.Server/GameTicking/Rules/RuleGridsSystem.cs @@ -65,6 +65,12 @@ private void OnSelectLocation(Entity ent, ref AntagSelectLoc if (_whitelist.IsWhitelistFail(ent.Comp.SpawnerWhitelist, uid)) continue; + if (TryComp(uid, out var gridSpawnPointWhitelistComponent)) + { + if (!_whitelist.CheckBoth(args.Entity, gridSpawnPointWhitelistComponent.Blacklist, gridSpawnPointWhitelistComponent.Whitelist)) + continue; + } + args.Coordinates.Add(_transform.GetMapCoordinates(xform)); } } diff --git a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs index d3f5cca97ec..ef79eca6594 100644 --- a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs @@ -1,12 +1,13 @@ using Content.Server.Antag; using Content.Server.GameTicking.Rules.Components; -using Content.Server.Mind; using Content.Server.Roles; using Content.Server.Shuttles.Systems; using Content.Shared.GameTicking.Components; using Content.Shared.Imperial.CrewSkills; +using Content.Shared.Imperial.Helpers; using Content.Shared.Mind; using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Systems; using Content.Shared.Roles.Components; using Content.Shared.Survivor.Components; using Content.Shared.Tag; @@ -17,13 +18,14 @@ namespace Content.Server.GameTicking.Rules; public sealed class SurvivorRuleSystem : GameRuleSystem { - [Dependency] private readonly RoleSystem _role = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!; - [Dependency] private readonly TransformSystem _xform = default!; [Dependency] private readonly EmergencyShuttleSystem _eShuttle = default!; - [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly TargetSystem _target = default!; + [Dependency] private readonly TransformSystem _xform = default!; + [Dependency] private readonly SharedCrewSkillsSystem _crewSkills = default!; // Imperial Space Crew Skills private static readonly ProtoId InvalidForSurvivorAntagTag = "InvalidForSurvivorAntag"; @@ -39,7 +41,7 @@ protected override void Started(EntityUid uid, SurvivorRuleComponent component, { base.Started(uid, component, gameRule, args); - var allAliveHumanMinds = _mind.GetAliveHumans(); + var allAliveHumanMinds = _target.GetAliveHumans(); foreach (var humanMind in allAliveHumanMinds) { @@ -56,10 +58,7 @@ protected override void Started(EntityUid uid, SurvivorRuleComponent component, _role.MindAddRole(mind, "MindRoleSurvivor"); _antag.SendBriefing(ent, Loc.GetString("survivor-role-greeting"), Color.Olive, null); - // Imperial Space Crew Skills Start - var crewSkillsComponent = EnsureComp(ent); - crewSkillsComponent.Skills.Add("skillShooting"); - // Imperial Space Crew Skills End + _crewSkills.AddSkillToMindAttempt(mind, SkillsHelper.ShootingSkill); // Imperial Space Crew Skills } } @@ -112,6 +111,7 @@ protected override void AppendRoundEndText(EntityUid uid, args.AddLine(Loc.GetString("survivor-round-end-dead-count", ("deadCount", deadSurvivors))); args.AddLine(Loc.GetString("survivor-round-end-alive-count", ("aliveCount", aliveMarooned))); args.AddLine(Loc.GetString("survivor-round-end-alive-on-shuttle-count", ("aliveCount", aliveOnShuttle))); + args.AddLine(""); // Player manifest at EOR shows who's a survivor so no need for extra info here. } diff --git a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs b/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs index 75bdd5387b3..00ad6e8934d 100644 --- a/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ThiefRuleSystem.cs @@ -38,7 +38,7 @@ private void OnGetBriefing(Entity role, ref GetBriefingEvent private string MakeBriefing(EntityUid ent) { - var isHuman = HasComp(ent); + var isHuman = HasComp(ent); var briefing = isHuman ? Loc.GetString("thief-role-greeting-human") : Loc.GetString("thief-role-greeting-animal"); diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index e72e9d5f732..bc053c80bad 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Text; using Content.Server.Codewords; +using Robust.Shared.Map; namespace Content.Server.GameTicking.Rules; @@ -148,34 +149,31 @@ public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component) private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing) { var pda = _uplink.FindUplinkTarget(traitor); - Note[]? code = null; Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add"); - var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true); + var uplinked = _uplink.AddUplink(traitor, startingBalance, out var code, pda, giveDiscounts: true, bindToPda: false); - if (pda is not null && uplinked) + if (code != null && uplinked == AddUplinkResult.Pda) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA"); - // Codes are only generated if the uplink is a PDA - var ev = new GenerateUplinkCodeEvent(); - RaiseLocalEvent(pda.Value, ref ev); - - if (ev.Code is { } generatedCode) - { - code = generatedCode; - // If giveUplink is false the uplink code part is omitted - briefing = string.Format("{0}\n{1}", - briefing, - Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); - return (code, briefing); - } + // If giveUplink is false the uplink code part is omitted + briefing = string.Format("{0}\n{1}", + briefing, + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + return (code, briefing); } - else if (pda is null && uplinked) + + if (uplinked == AddUplinkResult.Implant) { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant"); briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); } + else + { + Log.Error($"MakeTraitor failed on {ToPrettyString(traitor)} - No uplink could be added"); + } + return (null, briefing); } diff --git a/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs index 372de4bbb45..c9712b65e20 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/CutWireVariationPassSystem.cs @@ -25,7 +25,7 @@ protected override void ApplyVariation(Entity ent continue; // Check against blacklist - if (_whitelistSystem.IsBlacklistPass(ent.Comp.Blacklist, uid)) + if (_whitelistSystem.IsWhitelistPass(ent.Comp.Blacklist, uid)) continue; if (Random.Prob(ent.Comp.WireCutChance)) diff --git a/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs index dae6981f957..002014ee901 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs @@ -39,7 +39,7 @@ protected override void ApplyVariation(Entity(uid); _poweredLight.ReplaceSpawnedPrototype((uid, comp), ent.Comp.AgedLightTubePrototype); } else diff --git a/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs b/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs new file mode 100644 index 00000000000..932774e9a17 --- /dev/null +++ b/Content.Server/GameTicking/Rules/XenoborgsRuleSystem.cs @@ -0,0 +1,180 @@ +using Content.Server.Antag; +using Content.Server.Chat.Systems; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.RoundEnd; +using Content.Server.Station.Systems; +using Content.Shared.Destructible; +using Content.Shared.GameTicking.Components; +using Content.Shared.Mind; +using Content.Shared.Mobs.Systems; +using Content.Shared.Objectives.Systems; +using Content.Shared.Xenoborgs.Components; +using Robust.Shared.Timing; + +namespace Content.Server.GameTicking.Rules; + +public sealed class XenoborgsRuleSystem : GameRuleSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly RoundEndSystem _roundEnd = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly TargetSystem _target = default!; + + private static readonly Color AnnouncmentColor = Color.Gold; + + public void SendXenoborgDeathAnnouncement(Entity ent, bool mothershipCoreAlive) + { + if (ent.Comp.MothershipCoreDeathAnnouncmentSent) + return; + + var status = mothershipCoreAlive ? "alive" : "dead"; + _chatSystem.DispatchGlobalAnnouncement( + Loc.GetString($"xenoborgs-no-more-threat-mothership-core-{status}-announcement"), + colorOverride: AnnouncmentColor); + } + + public void SendMothershipDeathAnnouncement(Entity ent) + { + _chatSystem.DispatchGlobalAnnouncement( + Loc.GetString("mothership-destroyed-announcement"), + colorOverride: AnnouncmentColor); + + ent.Comp.MothershipCoreDeathAnnouncmentSent = true; + } + + // TODO: Refactor the end of round text + protected override void AppendRoundEndText(EntityUid uid, + XenoborgsRuleComponent component, + GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) + { + base.AppendRoundEndText(uid, component, gameRule, ref args); + + var numXenoborgs = GetNumberXenoborgs(); + var numHumans = _target.GetAliveHumans().Count; + + if (numXenoborgs < 5) + args.AddLine(Loc.GetString("xenoborgs-crewmajor")); + else if (4 * numXenoborgs < numHumans) + args.AddLine(Loc.GetString("xenoborgs-crewmajor")); + else if (2 * numXenoborgs < numHumans) + args.AddLine(Loc.GetString("xenoborgs-crewminor")); + else if (1.5 * numXenoborgs < numHumans) + args.AddLine(Loc.GetString("xenoborgs-neutral")); + else if (numXenoborgs < numHumans) + args.AddLine(Loc.GetString("xenoborgs-borgsminor")); + else + args.AddLine(Loc.GetString("xenoborgs-borgsmajor")); + + var numMothershipCores = GetNumberMothershipCores(); + + if (numMothershipCores == 0) + args.AddLine(Loc.GetString("xenoborgs-cond-all-xenoborgs-dead-core-dead")); + else if (numXenoborgs == 0) + args.AddLine(Loc.GetString("xenoborgs-cond-all-xenoborgs-dead-core-alive")); + else + { + args.AddLine(Loc.GetString("xenoborg-number-xenoborg-alive-end", ("count", numXenoborgs))); + args.AddLine(Loc.GetString("xenoborg-number-crew-alive-end", ("count", numHumans))); + } + + args.AddLine(Loc.GetString("xenoborg-max-number", ("count", component.MaxNumberXenoborgs))); + + args.AddLine(Loc.GetString("xenoborgs-list-start")); + + var antags = _antag.GetAntagIdentifiers(uid); + + foreach (var (_, sessionData, name) in antags) + { + args.AddLine(Loc.GetString("xenoborgs-list", ("name", name), ("user", sessionData.UserName))); + } + args.AddLine(""); + } + + private void CheckRoundEnd(XenoborgsRuleComponent xenoborgsRuleComponent) + { + var numXenoborgs = GetNumberXenoborgs(); + var numHumans = _target.GetAliveHumans().Count; + + xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs); + + if (xenoborgsRuleComponent.XenoborgShuttleCalled + || (float)numXenoborgs / (numHumans + numXenoborgs) <= xenoborgsRuleComponent.XenoborgShuttleCallPercentage + || _roundEnd.IsRoundEndRequested()) + return; + + foreach (var station in _station.GetStations()) + { + _chatSystem.DispatchStationAnnouncement(station, Loc.GetString("xenoborg-shuttle-call"), colorOverride: Color.BlueViolet); + } + _roundEnd.RequestRoundEnd(null, null, false, cantRecall: true); + xenoborgsRuleComponent.XenoborgShuttleCalled = true; + } + + protected override void Started(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; + } + + protected override void ActiveTick(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, float frameTime) + { + base.ActiveTick(uid, component, gameRule, frameTime); + + if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) + return; + + CheckRoundEnd(component); + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; + } + + /// + /// Get the number of xenoborgs + /// + /// if it should only include xenoborgs with a mind + /// if it should only include xenoborgs that are alive + /// the number of xenoborgs + private int GetNumberXenoborgs(bool playerControlled = true, bool alive = true) + { + var numberXenoborgs = 0; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var xenoborg, out _)) + { + if (HasComp(xenoborg)) + continue; + + if (playerControlled && !_mindSystem.TryGetMind(xenoborg, out _, out _)) + continue; + + if (alive && !_mobState.IsAlive(xenoborg)) + continue; + + numberXenoborgs++; + } + + return numberXenoborgs; + } + + /// + /// Gets the number of xenoborg cores + /// + /// the number of xenoborg cores + private int GetNumberMothershipCores() + { + var numberMothershipCores = 0; + + var mothershipCoreQuery = EntityQueryEnumerator(); + while (mothershipCoreQuery.MoveNext(out _, out _)) + { + numberMothershipCores++; + } + + return numberMothershipCores; + } +} diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 9caaa50cd9d..ab27d1df7b9 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -109,6 +109,7 @@ protected override void AppendRoundEndText(EntityUid uid, ("name", meta.EntityName), ("username", username))); } + args.AddLine(""); } /// @@ -126,7 +127,7 @@ private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent) { _chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson); } - _roundEnd.RequestRoundEnd(null, false); + _roundEnd.RequestRoundEnd(checkCooldown: false); } // we include dead for this count because we don't want to end the round @@ -174,7 +175,7 @@ private float GetInfectedFraction(bool includeOffStation = true, bool includeDea { var players = GetHealthyHumans(includeOffStation); var zombieCount = 0; - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out _, out _, out var mob)) { if (!includeDead && mob.CurrentState == MobState.Dead) @@ -204,7 +205,7 @@ private List GetHealthyHumans(bool includeOffStation = true) } } - var players = AllEntityQuery(); + var players = AllEntityQuery(); var zombers = GetEntityQuery(); while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform)) { diff --git a/Content.Server/Ghost/Components/GhostOnMoveComponent.cs b/Content.Server/Ghost/Components/GhostOnMoveComponent.cs deleted file mode 100644 index e3abc976885..00000000000 --- a/Content.Server/Ghost/Components/GhostOnMoveComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Content.Server.Ghost.Components -{ - [RegisterComponent] - public sealed partial class GhostOnMoveComponent : Component - { - [DataField("canReturn")] public bool CanReturn { get; set; } = true; - - [DataField("mustBeDead")] - public bool MustBeDead = false; - } -} diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index d6dac0d1d69..b98f2095e87 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -3,19 +3,20 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Managers; using Content.Server.GameTicking; -using Content.Server.Ghost.Components; using Content.Server.Mind; using Content.Server.Roles.Jobs; using Content.Shared.Actions; using Content.Shared.CCVar; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.Database; -using Content.Shared.Examine; using Content.Shared.Eye; using Content.Shared.FixedPoint; using Content.Shared.Follower; using Content.Shared.Ghost; +using Content.Shared.GhostTypes; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs; @@ -36,7 +37,6 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Ghost { @@ -46,7 +46,6 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly FollowerSystem _followerSystem = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly MindSystem _minds = default!; @@ -67,6 +66,7 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly NameModifierSystem _nameMod = default!; + [Dependency] private readonly GhostSpriteStateSystem _ghostState = default!; private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; @@ -85,8 +85,6 @@ public override void Initialize() SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnGhostShutdown); - SubscribeLocalEvent(OnGhostExamine); - SubscribeLocalEvent(OnMindRemovedMessage); SubscribeLocalEvent(OnMindUnvisitedMessage); SubscribeLocalEvent(OnPlayerDetached); @@ -201,8 +199,10 @@ private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentSt } _eye.RefreshVisibilityMask(uid); - var time = _gameTiming.CurTime; + var time = _gameTiming.RealTime; component.TimeOfDeath = time; + + Dirty(uid, component); } private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) @@ -233,16 +233,6 @@ private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent arg _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction); } - private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args) - { - var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath); - var deathTimeInfo = timeSinceDeath.Minutes > 0 - ? Loc.GetString("comp-ghost-examine-time-minutes", ("minutes", timeSinceDeath.Minutes)) - : Loc.GetString("comp-ghost-examine-time-seconds", ("seconds", timeSinceDeath.Seconds)); - - args.PushMarkup(deathTimeInfo); - } - #region Ghost Deletion private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args) @@ -331,7 +321,8 @@ private void OnGhostnadoRequest(GhostnadoRequestEvent msg, EntitySessionEventArg if (_followerSystem.GetMostGhostFollowed() is not { } target) return; - WarpTo(uid, target); + // If there is a ghostnado happening you almost definitely wanna join it, so we automatically follow instead of just warping. + _followerSystem.StartFollowingEntity(uid, target); } private void WarpTo(EntityUid uid, EntityUid target) @@ -478,6 +469,11 @@ private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition) var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value); var ghostComponent = Comp(ghost); + if (TryComp(ghost, out var state)) // If more TryComps are added this should be turned into an event + { + _ghostState.SetGhostSprite((ghost, state), mind); + } + // Try setting the ghost entity name to either the character name or the player name. // If all else fails, it'll default to the default entity prototype name, "observer". // However, that should rarely happen. @@ -580,12 +576,13 @@ public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaComma && TryComp(playerEntity, out var thresholds)) { var playerDeadThreshold = _mobThresholdSystem.GetThresholdForState(playerEntity.Value, MobState.Dead, thresholds); - dealtDamage = playerDeadThreshold - damageable.TotalDamage; + dealtDamage = playerDeadThreshold - + _damageable.GetTotalDamage((playerEntity.Value, damageable)); } DamageSpecifier damage = new(_prototypeManager.Index(AsphyxiationDamageType), dealtDamage); - _damageable.TryChangeDamage(playerEntity, damage, true); + _damageable.ChangeDamage(playerEntity.Value, damage, true); } } diff --git a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs index 7bb25a63fbd..13f90d8bd1f 100644 --- a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs @@ -49,15 +49,23 @@ private void OnUseInHand(EntityUid uid, ToggleableGhostRoleComponent component, UpdateAppearance(uid, ToggleableGhostRoleStatus.Searching); - var ghostRole = EnsureComp(uid); - EnsureComp(uid); - - //GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent - ghostRole.RoleName = Loc.GetString(component.RoleName); - ghostRole.RoleDescription = Loc.GetString(component.RoleDescription); - ghostRole.RoleRules = Loc.GetString(component.RoleRules); - ghostRole.JobProto = component.JobProto; - ghostRole.MindRoles = component.MindRoles; + ActivateGhostRole((uid, component)); + } + + public void ActivateGhostRole(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + var ghostRole = EnsureComp(ent); + EnsureComp(ent); + + // GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent + ghostRole.RoleName = Loc.GetString(ent.Comp.RoleName); + ghostRole.RoleDescription = Loc.GetString(ent.Comp.RoleDescription); + ghostRole.RoleRules = Loc.GetString(ent.Comp.RoleRules); + ghostRole.JobProto = ent.Comp.JobProto; + ghostRole.MindRoles = ent.Comp.MindRoles; } private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args) diff --git a/Content.Server/Ghost/SpookySpeakerSystem.cs b/Content.Server/Ghost/SpookySpeakerSystem.cs index ce3a2705a0d..e16c2cf9b39 100644 --- a/Content.Server/Ghost/SpookySpeakerSystem.cs +++ b/Content.Server/Ghost/SpookySpeakerSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Ghost.Components; +using Content.Shared.Chat; using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; using Robust.Shared.Random; diff --git a/Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs b/Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs index d23832984f8..8736908da63 100644 --- a/Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs +++ b/Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs @@ -2,12 +2,12 @@ using Content.Shared.Gibbing.Components; using Content.Shared.Mind; using Content.Shared.Objectives.Systems; -using Content.Server.Body.Systems; +using Content.Shared.Gibbing; namespace Content.Server.Gibbing.Systems; public sealed class GibOnRoundEndSystem : EntitySystem { - [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; @@ -49,7 +49,7 @@ private void OnRoundEnd(RoundEndMessageEvent args) if (gibComp.SpawnProto != null) SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates); - _body.GibBody(uid, splatModifier: 5f); + _gibbing.Gib(uid); } } } diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs deleted file mode 100644 index c715a5e5f35..00000000000 --- a/Content.Server/Gravity/GravityGeneratorComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Shared.Gravity; -using Content.Shared.Construction.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Gravity -{ - [RegisterComponent] - [Access(typeof(GravityGeneratorSystem))] - public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorComponent - { - [DataField("lightRadiusMin")] public float LightRadiusMin { get; set; } - [DataField("lightRadiusMax")] public float LightRadiusMax { get; set; } - - /// - /// Is the gravity generator currently "producing" gravity? - /// - [ViewVariables] - public bool GravityActive { get; set; } = false; - } -} diff --git a/Content.Server/Gravity/GravityGeneratorSystem.cs b/Content.Server/Gravity/GravityGeneratorSystem.cs index 9d58b82d690..8e714cf8842 100644 --- a/Content.Server/Gravity/GravityGeneratorSystem.cs +++ b/Content.Server/Gravity/GravityGeneratorSystem.cs @@ -4,7 +4,7 @@ namespace Content.Server.Gravity; -public sealed class GravityGeneratorSystem : EntitySystem +public sealed class GravityGeneratorSystem : SharedGravityGeneratorSystem { [Dependency] private readonly GravitySystem _gravitySystem = default!; [Dependency] private readonly SharedPointLightSystem _lights = default!; @@ -36,6 +36,7 @@ public override void Update(float frameTime) private void OnActivated(Entity ent, ref ChargedMachineActivatedEvent args) { ent.Comp.GravityActive = true; + Dirty(ent, ent.Comp); var xform = Transform(ent); @@ -48,6 +49,7 @@ private void OnActivated(Entity ent, ref ChargedMachi private void OnDeactivated(Entity ent, ref ChargedMachineDeactivatedEvent args) { ent.Comp.GravityActive = false; + Dirty(ent, ent.Comp); var xform = Transform(ent); diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs index ea1a6f4f4ff..16907dda8d1 100644 --- a/Content.Server/Guardian/GuardianSystem.cs +++ b/Content.Server/Guardian/GuardianSystem.cs @@ -1,9 +1,9 @@ -using Content.Server.Body.Systems; using Content.Server.Popups; using Content.Shared.Actions; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Examine; +using Content.Shared.Gibbing; using Content.Shared.Guardian; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -31,7 +31,7 @@ public sealed class GuardianSystem : EntitySystem [Dependency] private readonly SharedActionsSystem _actionSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; @@ -130,7 +130,7 @@ private void OnHostShutdown(EntityUid uid, GuardianHostComponent component, Comp // Ensure held items are dropped before deleting guardian. if (HasComp(guardian)) - _bodySystem.GibBody(component.HostedGuardian.Value); + _gibbing.Gib(component.HostedGuardian.Value); QueueDel(guardian); QueueDel(component.ActionEntity); @@ -285,8 +285,8 @@ private void OnGuardianDamaged(EntityUid uid, GuardianComponent component, Damag if (args.DamageDelta == null || component.Host == null || component.DamageShare == 0) return; - _damageSystem.TryChangeDamage( - component.Host, + _damageSystem.ChangeDamage( + component.Host.Value, args.DamageDelta * component.DamageShare, origin: args.Origin, ignoreResistances: true, diff --git a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs index 8fd088408c0..930368255ca 100644 --- a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs @@ -5,6 +5,7 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; +using Content.Shared.EntityConditions; using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; @@ -42,7 +43,7 @@ public static void PublishJson(StreamWriter file) Converters = { new UniversalJsonConverter(), - new UniversalJsonConverter(), + new UniversalJsonConverter(), new UniversalJsonConverter(), new UniversalJsonConverter(), new FixedPointJsonConverter() diff --git a/Content.Server/GuideGenerator/ReagentEntry.cs b/Content.Server/GuideGenerator/ReagentEntry.cs index 8b597ad61bc..4a0e1042052 100644 --- a/Content.Server/GuideGenerator/ReagentEntry.cs +++ b/Content.Server/GuideGenerator/ReagentEntry.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Text.Json.Serialization; using Content.Server.Body.Components; -using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects; @@ -43,7 +42,7 @@ public ReagentEntry(ReagentPrototype proto) Description = proto.LocalizedDescription; PhysicalDescription = proto.LocalizedPhysicalDescription; SubstanceColor = proto.SubstanceColor.ToHex(); - Metabolisms = proto.Metabolisms?.ToDictionary(x => x.Key.Id, x => x.Value); + Metabolisms = proto.Metabolisms?.Metabolisms.ToDictionary(x => x.Key.Id, x => x.Value); } } @@ -76,7 +75,7 @@ public ReactionEntry(ReactionPrototype proto) proto.Products .Select(x => KeyValuePair.Create(x.Key, x.Value.Float())) .ToDictionary(x => x.Key, x => x.Value); - Effects = proto.Effects; + Effects = proto.Effects.ToList(); } } diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index c187ef8ad51..70b65cb8668 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -3,7 +3,6 @@ using Content.Server.Stack; using Content.Server.Stunnable; using Content.Shared.ActionBlocker; -using Content.Shared.Body.Part; using Content.Shared.CombatMode; using Content.Shared.Damage.Systems; using Content.Shared.Explosion; @@ -53,9 +52,6 @@ public override void Initialize() SubscribeLocalEvent(OnDisarmed, before: new[] {typeof(StunSystem), typeof(SharedStaminaSystem)}); - SubscribeLocalEvent(HandleBodyPartAdded); - SubscribeLocalEvent(HandleBodyPartRemoved); - SubscribeLocalEvent(GetComponentState); SubscribeLocalEvent(OnExploded); @@ -112,32 +108,6 @@ private void OnDisarmed(EntityUid uid, HandsComponent component, ref DisarmedEve args.Handled = true; // no shove/stun. } - private void HandleBodyPartAdded(Entity ent, ref BodyPartAddedEvent args) - { - if (args.Part.Comp.PartType != BodyPartType.Hand) - return; - - // If this annoys you, which it should. - // Ping Smugleaf. - var location = args.Part.Comp.Symmetry switch - { - BodyPartSymmetry.None => HandLocation.Middle, - BodyPartSymmetry.Left => HandLocation.Left, - BodyPartSymmetry.Right => HandLocation.Right, - _ => throw new ArgumentOutOfRangeException(nameof(args.Part.Comp.Symmetry)) - }; - - AddHand(ent.AsNullable(), args.Slot, location); - } - - private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) - { - if (args.Part.Comp.PartType != BodyPartType.Hand) - return; - - RemoveHand(uid, args.Slot); - } - #region interactions private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity) @@ -170,7 +140,7 @@ public bool ThrowHeldItem(EntityUid player, EntityCoordinates coordinates, float if (TryComp(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually) { - var splitStack = _stackSystem.Split(throwEnt.Value, 1, Comp(player).Coordinates, stack); + var splitStack = _stackSystem.Split((throwEnt.Value, stack), 1, Comp(player).Coordinates); if (splitStack is not {Valid: true}) return false; diff --git a/Content.Server/Holopad/HolopadSystem.cs b/Content.Server/Holopad/HolopadSystem.cs index fe8150aba3f..c2aaf827dae 100644 --- a/Content.Server/Holopad/HolopadSystem.cs +++ b/Content.Server/Holopad/HolopadSystem.cs @@ -10,7 +10,6 @@ using Content.Shared.IdentityManagement; using Content.Shared.Labels.Components; using Content.Shared.Mobs; -using Content.Shared.Mobs.Systems; using Content.Shared.Power; using Content.Shared.Silicons.StationAi; using Content.Shared.Speech; @@ -24,6 +23,7 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; using System.Linq; +using Content.Shared.Power.EntitySystems; namespace Content.Server.Holopad; @@ -41,7 +41,7 @@ public sealed class HolopadSystem : SharedHolopadSystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPowerStateSystem _powerState = default!; private float _updateTimer = 1.0f; private const float UpdateTime = 1.0f; @@ -407,6 +407,9 @@ private void AddToggleProjectorVerb(Entity entity, ref GetVerb if (!this.IsPowered(entity, EntityManager)) return; + if (HasComp(entity)) + return; + if (!TryComp(entity, out var entityTelephone) || _telephoneSystem.IsTelephoneEngaged((entity, entityTelephone))) return; @@ -547,10 +550,14 @@ private void GenerateHologram(Entity entity) { _telephoneSystem.SetSpeakerForTelephone((entity, entityTelephone), (hologramUid, hologramSpeech)); } + + _powerState.SetWorkingState(entity.Owner, true); } private void DeleteHologram(Entity hologram, Entity attachedHolopad) { + _powerState.SetWorkingState(attachedHolopad.Owner, false); + attachedHolopad.Comp.Hologram = null; QueueDel(hologram); diff --git a/Content.Server/Holosign/HolosignProjectorComponent.cs b/Content.Server/Holosign/HolosignProjectorComponent.cs deleted file mode 100644 index bdc826f1304..00000000000 --- a/Content.Server/Holosign/HolosignProjectorComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Holosign -{ - [RegisterComponent] - public sealed partial class HolosignProjectorComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("signProto", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string SignProto = "HolosignWetFloor"; - - /// - /// How much charge a single use expends. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("chargeUse")] - public float ChargeUse = 50f; - } -} diff --git a/Content.Server/Holosign/HolosignSystem.cs b/Content.Server/Holosign/HolosignSystem.cs deleted file mode 100644 index 58ed77ecf83..00000000000 --- a/Content.Server/Holosign/HolosignSystem.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Content.Shared.Examine; -using Content.Shared.Coordinates.Helpers; -using Content.Server.Power.Components; -using Content.Server.PowerCell; -using Content.Shared.Interaction; -using Content.Shared.Storage; - -namespace Content.Server.Holosign; - -public sealed class HolosignSystem : EntitySystem -{ - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnBeforeInteract); - SubscribeLocalEvent(OnExamine); - } - - private void OnExamine(EntityUid uid, HolosignProjectorComponent component, ExaminedEvent args) - { - // TODO: This should probably be using an itemstatus - // TODO: I'm too lazy to do this rn but it's literally copy-paste from emag. - _powerCell.TryGetBatteryFromSlot(uid, out var battery); - var charges = UsesRemaining(component, battery); - var maxCharges = MaxUses(component, battery); - - using (args.PushGroup(nameof(HolosignProjectorComponent))) - { - args.PushMarkup(Loc.GetString("limited-charges-charges-remaining", ("charges", charges))); - - if (charges > 0 && charges == maxCharges) - { - args.PushMarkup(Loc.GetString("limited-charges-max-charges")); - } - } - } - - private void OnBeforeInteract(EntityUid uid, HolosignProjectorComponent component, BeforeRangedInteractEvent args) - { - - if (args.Handled - || !args.CanReach // prevent placing out of range - || HasComp(args.Target) // if it's a storage component like a bag, we ignore usage so it can be stored - || !_powerCell.TryUseCharge(uid, component.ChargeUse, user: args.User) // if no battery or no charge, doesn't work - ) - return; - - // places the holographic sign at the click location, snapped to grid. - // overlapping of the same holo on one tile remains allowed to allow holofan refreshes - var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager)); - var xform = Transform(holoUid); - if (!xform.Anchored) - _transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it) - - args.Handled = true; - } - - private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null) - { - if (battery == null || - component.ChargeUse == 0f) return 0; - - return (int) (battery.CurrentCharge / component.ChargeUse); - } - - private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null) - { - if (battery == null || - component.ChargeUse == 0f) return 0; - - return (int) (battery.MaxCharge / component.ChargeUse); - } -} diff --git a/Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs b/Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs index 5efea024f4e..a7bb4c51fd2 100644 --- a/Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs +++ b/Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs @@ -1,14 +1,8 @@ -using Content.Shared.Humanoid.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; - -namespace Content.Server.CharacterAppearance.Components; +namespace Content.Server.Humanoid.Components; [RegisterComponent] public sealed partial class RandomHumanoidAppearanceComponent : Component { - [DataField("randomizeName")] public bool RandomizeName = true; - /// - /// After randomizing, sets the hair style to this, if possible - /// - [DataField] public string? Hair = null; + [DataField] + public bool RandomizeName = true; } diff --git a/Content.Server/Humanoid/HideableHumanoidLayersSystem.cs b/Content.Server/Humanoid/HideableHumanoidLayersSystem.cs new file mode 100644 index 00000000000..2619ca2b55d --- /dev/null +++ b/Content.Server/Humanoid/HideableHumanoidLayersSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Humanoid; + +namespace Content.Server.Humanoid; + +public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem; diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs deleted file mode 100644 index 7744d161519..00000000000 --- a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Content.Server.Administration.Managers; -using Content.Shared.Administration; -using Content.Shared.Humanoid; -using Content.Shared.Verbs; -using Robust.Server.GameObjects; -using Robust.Shared.Player; -using Robust.Shared.Utility; - -namespace Content.Server.Humanoid; - -public sealed partial class HumanoidAppearanceSystem -{ - [Dependency] private readonly IAdminManager _adminManager = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - - private void OnVerbsRequest(EntityUid uid, HumanoidAppearanceComponent component, GetVerbsEvent args) - { - if (!TryComp(args.User, out var actor)) - { - return; - } - - if (!_adminManager.HasAdminFlag(actor.PlayerSession, AdminFlags.Fun)) - { - return; - } - - args.Verbs.Add(new Verb - { - Text = "Modify markings", - Category = VerbCategory.Tricks, - Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"), - Act = () => - { - _uiSystem.OpenUi(uid, HumanoidMarkingModifierKey.Key, actor.PlayerSession); - _uiSystem.SetUiState( - uid, - HumanoidMarkingModifierKey.Key, - new HumanoidMarkingModifierState(component.MarkingSet, component.Species, - component.Sex, - component.SkinColor, - component.CustomBaseLayers - )); - } - }); - } - - private void OnBaseLayersSet(EntityUid uid, HumanoidAppearanceComponent component, - HumanoidMarkingModifierBaseLayersSetMessage message) - { - if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun)) - { - return; - } - - if (message.Info == null) - { - component.CustomBaseLayers.Remove(message.Layer); - } - else - { - component.CustomBaseLayers[message.Layer] = message.Info.Value; - } - - Dirty(uid, component); - - if (message.ResendState) - { - _uiSystem.SetUiState( - uid, - HumanoidMarkingModifierKey.Key, - new HumanoidMarkingModifierState(component.MarkingSet, component.Species, - component.Sex, - component.SkinColor, - component.CustomBaseLayers - )); - } - } - - private void OnMarkingsSet(EntityUid uid, HumanoidAppearanceComponent component, - HumanoidMarkingModifierMarkingSetMessage message) - { - if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun)) - { - return; - } - - component.MarkingSet = message.MarkingSet; - Dirty(uid, component); - - if (message.ResendState) - { - _uiSystem.SetUiState( - uid, - HumanoidMarkingModifierKey.Key, - new HumanoidMarkingModifierState(component.MarkingSet, component.Species, - component.Sex, - component.SkinColor, - component.CustomBaseLayers - )); - } - - } -} diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs deleted file mode 100644 index 9e719bda660..00000000000 --- a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Markings; -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Preferences; -using Content.Shared.Verbs; -using Robust.Shared.GameObjects.Components.Localization; - -namespace Content.Server.Humanoid; - -public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem -{ - [Dependency] private readonly MarkingManager _markingManager = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnMarkingsSet); - SubscribeLocalEvent(OnBaseLayersSet); - SubscribeLocalEvent>(OnVerbsRequest); - } - - /// - /// Removes a marking from a humanoid by ID. - /// - /// Humanoid mob's UID - /// The marking to try and remove. - /// Whether to immediately sync this to the humanoid - /// Humanoid component of the entity - public void RemoveMarking(EntityUid uid, string marking, bool sync = true, HumanoidAppearanceComponent? humanoid = null) - { - if (!Resolve(uid, ref humanoid) - || !_markingManager.Markings.TryGetValue(marking, out var prototype)) - { - return; - } - - humanoid.MarkingSet.Remove(prototype.MarkingCategory, marking); - - if (sync) - Dirty(uid, humanoid); - } - - /// - /// Removes a marking from a humanoid by category and index. - /// - /// Humanoid mob's UID - /// Category of the marking - /// Index of the marking - /// Humanoid component of the entity - public void RemoveMarking(EntityUid uid, MarkingCategories category, int index, HumanoidAppearanceComponent? humanoid = null) - { - if (index < 0 - || !Resolve(uid, ref humanoid) - || !humanoid.MarkingSet.TryGetCategory(category, out var markings) - || index >= markings.Count) - { - return; - } - - humanoid.MarkingSet.Remove(category, index); - Dirty(uid, humanoid); - } - - /// - /// Sets the marking ID of the humanoid in a category at an index in the category's list. - /// - /// Humanoid mob's UID - /// Category of the marking - /// Index of the marking - /// The marking ID to use - /// Humanoid component of the entity - public void SetMarkingId(EntityUid uid, MarkingCategories category, int index, string markingId, HumanoidAppearanceComponent? humanoid = null) - { - if (index < 0 - || !_markingManager.MarkingsByCategory(category).TryGetValue(markingId, out var markingPrototype) - || !Resolve(uid, ref humanoid) - || !humanoid.MarkingSet.TryGetCategory(category, out var markings) - || index >= markings.Count) - { - return; - } - - var marking = markingPrototype.AsMarking(); - for (var i = 0; i < marking.MarkingColors.Count && i < markings[index].MarkingColors.Count; i++) - { - marking.SetColor(i, markings[index].MarkingColors[i]); - } - - humanoid.MarkingSet.Replace(category, index, marking); - Dirty(uid, humanoid); - } - - /// - /// Sets the marking colors of the humanoid in a category at an index in the category's list. - /// - /// Humanoid mob's UID - /// Category of the marking - /// Index of the marking - /// The marking colors to use - /// Humanoid component of the entity - public void SetMarkingColor(EntityUid uid, MarkingCategories category, int index, List colors, - HumanoidAppearanceComponent? humanoid = null) - { - if (index < 0 - || !Resolve(uid, ref humanoid) - || !humanoid.MarkingSet.TryGetCategory(category, out var markings) - || index >= markings.Count) - { - return; - } - - for (var i = 0; i < markings[index].MarkingColors.Count && i < colors.Count; i++) - { - markings[index].SetColor(i, colors[i]); - } - - Dirty(uid, humanoid); - } -} diff --git a/Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs b/Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs index 2822fb69e14..537ae122765 100644 --- a/Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs +++ b/Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs @@ -1,4 +1,5 @@ -using Content.Server.CharacterAppearance.Components; +using Content.Server.Humanoid.Components; +using Content.Shared.Body; using Content.Shared.Humanoid; using Content.Shared.Preferences; @@ -6,8 +7,9 @@ namespace Content.Server.Humanoid.Systems; public sealed class RandomHumanoidAppearanceSystem : EntitySystem { - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; public override void Initialize() { @@ -19,17 +21,13 @@ public override void Initialize() private void OnMapInit(EntityUid uid, RandomHumanoidAppearanceComponent component, MapInitEvent args) { // If we have an initial profile/base layer set, do not randomize this humanoid. - if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid) || !string.IsNullOrEmpty(humanoid.Initial)) - { + if (!TryComp(uid, out var humanoid)) return; - } var profile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species); - //If we have a specified hair style, change it to this - if(component.Hair != null) - profile = profile.WithCharacterAppearance(profile.Appearance.WithHairStyleName(component.Hair)); - _humanoid.LoadProfile(uid, profile, humanoid); + _visualBody.ApplyProfileTo(uid, profile); + _humanoidProfile.ApplyProfileTo(uid, profile); if (component.RandomizeName) _metaData.SetEntityName(uid, profile.Name); diff --git a/Content.Server/Humanoid/Systems/RandomHumanoidSystem.cs b/Content.Server/Humanoid/Systems/RandomHumanoidSystem.cs index 126416daa90..4ae69d2fd7c 100644 --- a/Content.Server/Humanoid/Systems/RandomHumanoidSystem.cs +++ b/Content.Server/Humanoid/Systems/RandomHumanoidSystem.cs @@ -1,6 +1,8 @@ using Content.Server.Humanoid.Components; using Content.Server.RandomMetadata; +using Content.Shared.Body; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Humanoid; using Content.Shared.Preferences; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -13,11 +15,11 @@ namespace Content.Server.Humanoid.Systems; /// public sealed class RandomHumanoidSystem : EntitySystem { + [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; - - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; /// public override void Initialize() @@ -44,8 +46,6 @@ public EntityUid SpawnRandomHumanoid(string prototypeId, EntityCoordinates coord _metaData.SetEntityName(humanoid, prototype.RandomizeName ? profile.Name : name); - _humanoid.LoadProfile(humanoid, profile); - if (prototype.Components != null) { foreach (var entry in prototype.Components.Values) @@ -58,6 +58,9 @@ public EntityUid SpawnRandomHumanoid(string prototypeId, EntityCoordinates coord EntityManager.InitializeAndStartEntity(humanoid); + _visualBody.ApplyProfileTo(humanoid, profile); + _humanoidProfile.ApplyProfileTo(humanoid, profile); + return humanoid; } } diff --git a/Content.Server/IP/IPAddressExt.cs b/Content.Server/IP/IPAddressExt.cs index a61477e01bc..8c514e96b91 100644 --- a/Content.Server/IP/IPAddressExt.cs +++ b/Content.Server/IP/IPAddressExt.cs @@ -11,22 +11,14 @@ public static class IPAddressExt { // Npgsql used to map inet types as a tuple like this. // I'm upgrading the dependencies and I don't wanna rewrite a bunch of DB code, so a few helpers it shall be. - [return: NotNullIfNotNull(nameof(tuple))] - public static NpgsqlInet? ToNpgsqlInet(this (IPAddress, int)? tuple) + public static NpgsqlInet ToNpgsqlInet(this (IPAddress, int) tuple) { - if (tuple == null) - return null; - - return new NpgsqlInet(tuple.Value.Item1, (byte) tuple.Value.Item2); + return new NpgsqlInet(tuple.Item1, (byte)tuple.Item2); } - [return: NotNullIfNotNull(nameof(inet))] - public static (IPAddress, int)? ToTuple(this NpgsqlInet? inet) + public static (IPAddress, int) ToTuple(this NpgsqlInet inet) { - if (inet == null) - return null; - - return (inet.Value.Address, inet.Value.Netmask); + return (inet.Address, inet.Netmask); } // Taken from https://stackoverflow.com/a/56461160/4678631 diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs index bcbcfda9af0..013f0294764 100644 --- a/Content.Server/ImmovableRod/ImmovableRodSystem.cs +++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs @@ -1,11 +1,12 @@ -using Content.Server.Body.Systems; using Content.Server.Destructible; -using Content.Server.Examine; using Content.Server.Polymorph.Components; using Content.Server.Popups; -using Content.Shared.Body.Components; -using Content.Shared.Damage; +using Content.Shared.Administration.Logs; +using Content.Shared.Body; +using Content.Shared.Damage.Systems; +using Content.Shared.Database; using Content.Shared.Examine; +using Content.Shared.Gibbing; using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; @@ -21,7 +22,7 @@ public sealed class ImmovableRodSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -29,6 +30,7 @@ public sealed class ImmovableRodSystem : EntitySystem [Dependency] private readonly DestructibleSystem _destructible = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; public override void Update(float frameTime) { @@ -95,7 +97,9 @@ private void OnCollide(EntityUid uid, ImmovableRodComponent component, ref Start { // oh god. var coords = Transform(uid).Coordinates; + _popup.PopupCoordinates(Loc.GetString("immovable-rod-collided-rod-not-good"), coords, PopupType.LargeCaution); + _adminLogger.Add(LogType.Gib, LogImpact.Low, $"{ToPrettyString(uid)} and {ToPrettyString(ent)} created singularity at X:{coords.X} Y:{coords.Y}"); Del(uid); Del(ent); @@ -112,7 +116,7 @@ private void OnCollide(EntityUid uid, ImmovableRodComponent component, ref Start } // gib or damage em - if (TryComp(ent, out var body)) + if (HasComp(ent)) { component.MobCount++; _popup.PopupEntity(Loc.GetString("immovable-rod-penetrated-mob", ("rod", uid), ("mob", ent)), uid, PopupType.LargeCaution); @@ -126,7 +130,10 @@ private void OnCollide(EntityUid uid, ImmovableRodComponent component, ref Start return; } - _bodySystem.GibBody(ent, body: body); + var coords = Transform(uid).Coordinates; + _adminLogger.Add(LogType.Gib, LogImpact.Low, $"Entity {ToPrettyString(uid)} gibbed {ToPrettyString(ent)} at X:{coords.X} Y:{coords.Y}"); + + _gibbing.Gib(ent); return; } diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Components/HydrogenGasFilterComponent.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Components/HydrogenGasFilterComponent.cs new file mode 100644 index 00000000000..b4473aab63f --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Components/HydrogenGasFilterComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Atmos; + +namespace Content.Server.Imperial.Atmos.Piping.Trinary.Components +{ + [RegisterComponent] + public sealed partial class HydrogenGasFilterComponent : Component + { + [DataField] + public bool Enabled = true; + + [DataField("inlet")] + public string InletName = "inlet"; + + [DataField("filter")] + public string FilterName = "filter"; + + [DataField("outlet")] + public string OutletName = "outlet"; + + [DataField] + public float TransferRate = Atmospherics.HydrogenMaxTransferRate; + + [DataField] + public float MaxTransferRate = Atmospherics.HydrogenMaxTransferRate; + + [DataField] + public Gas? FilteredGas; + } +} diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Systems/HydrogenGasFilterSystem.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Systems/HydrogenGasFilterSystem.cs new file mode 100644 index 00000000000..d2185a7cfed --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenGasFilter/Systems/HydrogenGasFilterSystem.cs @@ -0,0 +1,216 @@ +using Content.Server.Administration.Logs; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.Imperial.Atmos.Piping.Trinary.Components; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.NodeContainer.Nodes; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Piping; +using Content.Shared.Atmos.Piping.Components; +using Content.Shared.Audio; +using Content.Shared.Database; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Content.Shared.Imperial.Atmos.Piping.Trinary.Components; +using Content.Shared.Atmos.Components; + +namespace Content.Server.Imperial.Atmos.Piping.Trinary.EntitySystems +{ + [UsedImplicitly] + public sealed class HydrogenGasFilterSystem : EntitySystem + { + [Dependency] private UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private IAdminLogManager _adminLogger = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnFilterUpdated); + SubscribeLocalEvent(OnFilterLeaveAtmosphere); + SubscribeLocalEvent(OnFilterActivate); + SubscribeLocalEvent(OnFilterAnalyzed); + // Bound UI subscriptions + SubscribeLocalEvent(OnTransferRateChangeMessage); + SubscribeLocalEvent(OnSelectGasMessage); + SubscribeLocalEvent(OnToggleStatusMessage); + + } + + private void OnInit(EntityUid uid, HydrogenGasFilterComponent filter, ComponentInit args) + { + UpdateAppearance(uid, filter); + } + + private void OnFilterUpdated(EntityUid uid, HydrogenGasFilterComponent filter, ref AtmosDeviceUpdateEvent args) + { + if (!filter.Enabled + || !_nodeContainer.TryGetNodes(uid, filter.InletName, filter.FilterName, filter.OutletName, out PipeNode? inletNode, out PipeNode? filterNode, out PipeNode? outletNode) + || outletNode.Air.Pressure >= Atmospherics.HydrogenMaxOutputPressure) // No need to transfer if target is full. + { + _ambientSoundSystem.SetAmbience(uid, false); + return; + } + + // We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters. + var transferVol = filter.TransferRate * _atmosphereSystem.PumpSpeedup() * args.dt; + + if (transferVol <= 0) + { + _ambientSoundSystem.SetAmbience(uid, false); + return; + } + + var removed = inletNode.Air.RemoveVolume(transferVol); + + if (filter.FilteredGas.HasValue) + { + var filteredOut = new GasMixture() { Temperature = removed.Temperature }; + + filteredOut.SetMoles(filter.FilteredGas.Value, removed.GetMoles(filter.FilteredGas.Value)); + removed.SetMoles(filter.FilteredGas.Value, 0f); + + var target = filterNode.Air.Pressure < Atmospherics.HydrogenMaxOutputPressure ? filterNode : inletNode; + _atmosphereSystem.Merge(target.Air, filteredOut); + _ambientSoundSystem.SetAmbience(uid, filteredOut.TotalMoles > 0f); + } + + _atmosphereSystem.Merge(outletNode.Air, removed); + } + + private void OnFilterLeaveAtmosphere(EntityUid uid, HydrogenGasFilterComponent filter, ref AtmosDeviceDisabledEvent args) + { + filter.Enabled = false; + + UpdateAppearance(uid, filter); + _ambientSoundSystem.SetAmbience(uid, false); + + DirtyUI(uid, filter); + _userInterfaceSystem.CloseUi(uid, HydrogenGasFilterUiKey.Key); + } + + private void OnFilterActivate(EntityUid uid, HydrogenGasFilterComponent filter, ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + if (Comp(uid).Anchored) + { + _userInterfaceSystem.OpenUi(uid, HydrogenGasFilterUiKey.Key, actor.PlayerSession); + DirtyUI(uid, filter); + } + else + { + _popupSystem.PopupCursor(Loc.GetString("comp-gas-filter-ui-needs-anchor"), args.User); + } + + args.Handled = true; + } + + private void DirtyUI(EntityUid uid, HydrogenGasFilterComponent? filter) + { + if (!Resolve(uid, ref filter)) + return; + + _userInterfaceSystem.SetUiState(uid, HydrogenGasFilterUiKey.Key, + new HydrogenGasFilterBoundUserInterfaceState(MetaData(uid).EntityName, filter.TransferRate, filter.Enabled, filter.FilteredGas)); + } + + private void UpdateAppearance(EntityUid uid, HydrogenGasFilterComponent? filter = null) + { + if (!Resolve(uid, ref filter, false)) + return; + + _appearanceSystem.SetData(uid, FilterVisuals.Enabled, filter.Enabled); + } + + private void OnToggleStatusMessage(EntityUid uid, HydrogenGasFilterComponent filter, HydrogenGasFilterToggleStatusMessage args) + { + filter.Enabled = args.Enabled; + _adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}"); + DirtyUI(uid, filter); + UpdateAppearance(uid, filter); + } + + private void OnTransferRateChangeMessage(EntityUid uid, HydrogenGasFilterComponent filter, HydrogenGasFilterChangeRateMessage args) + { + filter.TransferRate = Math.Clamp(args.Rate, 0f, filter.MaxTransferRate); + _adminLogger.Add(LogType.AtmosVolumeChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the transfer rate on {ToPrettyString(uid):device} to {args.Rate}"); + DirtyUI(uid, filter); + + } + + private void OnSelectGasMessage(EntityUid uid, HydrogenGasFilterComponent filter, HydrogenGasFilterSelectGasMessage args) + { + if (args.Gas.HasValue) + { + if (Enum.IsDefined(typeof(Gas), args.Gas)) + { + filter.FilteredGas = args.Gas; + _adminLogger.Add(LogType.AtmosFilterChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {args.Gas.ToString()}"); + DirtyUI(uid, filter); + } + else + { + Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.Gas}"); + } + } + else + { + filter.FilteredGas = null; + _adminLogger.Add(LogType.AtmosFilterChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to none"); + DirtyUI(uid, filter); + } + } + + /// + /// Returns the gas mixture for the gas analyzer + /// + private void OnFilterAnalyzed(EntityUid uid, HydrogenGasFilterComponent component, GasAnalyzerScanEvent args) + { + args.GasMixtures ??= new List<(string, GasMixture?)>(); + + // multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system + if (_nodeContainer.TryGetNode(uid, component.InletName, out PipeNode? inlet) && inlet.Air.Volume != 0f) + { + var inletAirLocal = inlet.Air.Clone(); + inletAirLocal.Multiply(inlet.Volume / inlet.Air.Volume); + inletAirLocal.Volume = inlet.Volume; + args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-inlet"), inletAirLocal)); + } + if (_nodeContainer.TryGetNode(uid, component.FilterName, out PipeNode? filterNode) && filterNode.Air.Volume != 0f) + { + var filterNodeAirLocal = filterNode.Air.Clone(); + filterNodeAirLocal.Multiply(filterNode.Volume / filterNode.Air.Volume); + filterNodeAirLocal.Volume = filterNode.Volume; + args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-filter"), filterNodeAirLocal)); + } + if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f) + { + var outletAirLocal = outlet.Air.Clone(); + outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume); + outletAirLocal.Volume = outlet.Volume; + args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal)); + } + + args.DeviceFlipped = inlet != null && filterNode != null && inlet.CurrentPipeDirection.ToDirection() == filterNode.CurrentPipeDirection.ToDirection().GetClockwise90Degrees(); + } + } +} diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Components/HydrogenGasMixerComponent.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Components/HydrogenGasMixerComponent.cs new file mode 100644 index 00000000000..813cd852363 --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Components/HydrogenGasMixerComponent.cs @@ -0,0 +1,42 @@ +using Content.Server.Imperial.Atmos.Piping.Trinary.EntitySystems; +using Content.Shared.Atmos; + +namespace Content.Server.Imperial.Atmos.Piping.Trinary.Components +{ + [RegisterComponent] + [Access(typeof(HydrogenGasMixerSystem))] + public sealed partial class HydrogenGasMixerComponent : Component + { + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public bool Enabled = true; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("inletOne")] + public string InletOneName = "inletOne"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("inletTwo")] + public string InletTwoName = "inletTwo"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("outlet")] + public string OutletName = "outlet"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float TargetPressure = Atmospherics.OneAtmosphere; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float MaxTargetPressure = Atmospherics.HydrogenMaxOutputPressure; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float InletOneConcentration = 0.5f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float InletTwoConcentration = 0.5f; + } +} diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Systems/HydrogenGasMixerSystem.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Systems/HydrogenGasMixerSystem.cs new file mode 100644 index 00000000000..97ab7abd91e --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenMixer/Systems/HydrogenGasMixerSystem.cs @@ -0,0 +1,241 @@ +using Content.Server.Administration.Logs; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.NodeContainer.Nodes; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Piping; +using Content.Shared.Atmos.Piping.Components; +using Content.Shared.Imperial.Atmos.Piping.Trinary.Components; +using Content.Shared.Audio; +using Content.Shared.Database; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Content.Server.Imperial.Atmos.Piping.Trinary.Components; +using Content.Shared.Atmos.Components; + +namespace Content.Server.Imperial.Atmos.Piping.Trinary.EntitySystems +{ + [UsedImplicitly] + public sealed class HydrogenGasMixerSystem : EntitySystem + { + [Dependency] private UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private IAdminLogManager _adminLogger = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnMixerUpdated); + SubscribeLocalEvent(OnMixerActivate); + SubscribeLocalEvent(OnMixerAnalyzed); + // Bound UI subscriptions + SubscribeLocalEvent(OnOutputPressureChangeMessage); + SubscribeLocalEvent(OnChangeNodePercentageMessage); + SubscribeLocalEvent(OnToggleStatusMessage); + + SubscribeLocalEvent(OnMixerLeaveAtmosphere); + } + + private void OnInit(EntityUid uid, HydrogenGasMixerComponent mixer, ComponentInit args) + { + UpdateAppearance(uid, mixer); + } + + private void OnMixerUpdated(EntityUid uid, HydrogenGasMixerComponent mixer, ref AtmosDeviceUpdateEvent args) + { + // TODO ATMOS: Cache total moles since it's expensive. + + if (!mixer.Enabled + || !_nodeContainer.TryGetNodes(uid, mixer.InletOneName, mixer.InletTwoName, mixer.OutletName, out PipeNode? inletOne, out PipeNode? inletTwo, out PipeNode? outlet)) + { + _ambientSoundSystem.SetAmbience(uid, false); + return; + } + + var outputStartingPressure = outlet.Air.Pressure; + + if (outputStartingPressure >= mixer.TargetPressure) + return; // Target reached, no need to mix. + + var generalTransfer = (mixer.TargetPressure - outputStartingPressure) * outlet.Air.Volume / Atmospherics.R; + + var transferMolesOne = inletOne.Air.Temperature > 0 ? mixer.InletOneConcentration * generalTransfer / inletOne.Air.Temperature : 0f; + var transferMolesTwo = inletTwo.Air.Temperature > 0 ? mixer.InletTwoConcentration * generalTransfer / inletTwo.Air.Temperature : 0f; + + if (mixer.InletTwoConcentration <= 0f) + { + if (inletOne.Air.Temperature <= 0f) + return; + + transferMolesOne = MathF.Min(transferMolesOne, inletOne.Air.TotalMoles); + transferMolesTwo = 0f; + } + + else if (mixer.InletOneConcentration <= 0) + { + if (inletTwo.Air.Temperature <= 0f) + return; + + transferMolesOne = 0f; + transferMolesTwo = MathF.Min(transferMolesTwo, inletTwo.Air.TotalMoles); + } + else + { + if (inletOne.Air.Temperature <= 0f || inletTwo.Air.Temperature <= 0f) + return; + + if (transferMolesOne <= 0 || transferMolesTwo <= 0) + { + _ambientSoundSystem.SetAmbience(uid, false); + return; + } + + if (inletOne.Air.TotalMoles < transferMolesOne || inletTwo.Air.TotalMoles < transferMolesTwo) + { + var ratio = MathF.Min(inletOne.Air.TotalMoles / transferMolesOne, inletTwo.Air.TotalMoles / transferMolesTwo); + transferMolesOne *= ratio; + transferMolesTwo *= ratio; + } + } + + // Actually transfer the gas now. + var transferred = false; + + if (transferMolesOne > 0f) + { + transferred = true; + var removed = inletOne.Air.Remove(transferMolesOne); + _atmosphereSystem.Merge(outlet.Air, removed); + } + + if (transferMolesTwo > 0f) + { + transferred = true; + var removed = inletTwo.Air.Remove(transferMolesTwo); + _atmosphereSystem.Merge(outlet.Air, removed); + } + + if (transferred) + _ambientSoundSystem.SetAmbience(uid, true); + } + + private void OnMixerLeaveAtmosphere(EntityUid uid, HydrogenGasMixerComponent mixer, ref AtmosDeviceDisabledEvent args) + { + mixer.Enabled = false; + + DirtyUI(uid, mixer); + UpdateAppearance(uid, mixer); + _userInterfaceSystem.CloseUi(uid, HydrogenGasMixerUiKey.Key); + } + + private void OnMixerActivate(EntityUid uid, HydrogenGasMixerComponent mixer, ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + if (!TryComp(args.User, out ActorComponent? actor)) + return; + + if (Transform(uid).Anchored) + { + _userInterfaceSystem.OpenUi(uid, HydrogenGasMixerUiKey.Key, actor.PlayerSession); + DirtyUI(uid, mixer); + } + else + { + _popup.PopupCursor(Loc.GetString("comp-gas-mixer-ui-needs-anchor"), args.User); + } + + args.Handled = true; + } + + private void DirtyUI(EntityUid uid, HydrogenGasMixerComponent? mixer) + { + if (!Resolve(uid, ref mixer)) + return; + + _userInterfaceSystem.SetUiState(uid, HydrogenGasMixerUiKey.Key, + new HydrogenGasMixerBoundUserInterfaceState(Comp(uid).EntityName, mixer.TargetPressure, mixer.Enabled, mixer.InletOneConcentration)); + } + + private void UpdateAppearance(EntityUid uid, HydrogenGasMixerComponent? mixer = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref mixer, ref appearance, false)) + return; + + _appearance.SetData(uid, FilterVisuals.Enabled, mixer.Enabled, appearance); + } + + private void OnToggleStatusMessage(EntityUid uid, HydrogenGasMixerComponent mixer, HydrogenGasMixerToggleStatusMessage args) + { + mixer.Enabled = args.Enabled; + _adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}"); + DirtyUI(uid, mixer); + UpdateAppearance(uid, mixer); + } + + private void OnOutputPressureChangeMessage(EntityUid uid, HydrogenGasMixerComponent mixer, HydrogenGasMixerChangeOutputPressureMessage args) + { + mixer.TargetPressure = Math.Clamp(args.Pressure, 0f, mixer.MaxTargetPressure); + _adminLogger.Add(LogType.AtmosPressureChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(uid):device} to {args.Pressure}kPa"); + DirtyUI(uid, mixer); + } + + private void OnChangeNodePercentageMessage(EntityUid uid, HydrogenGasMixerComponent mixer, + HydrogenGasMixerChangeNodePercentageMessage args) + { + float nodeOne = Math.Clamp(args.NodeOne, 0f, 100.0f) / 100.0f; + mixer.InletOneConcentration = nodeOne; + mixer.InletTwoConcentration = 1.0f - mixer.InletOneConcentration; + _adminLogger.Add(LogType.AtmosRatioChanged, LogImpact.Medium, + $"{ToPrettyString(args.Actor):player} set the ratio on {ToPrettyString(uid):device} to {mixer.InletOneConcentration}:{mixer.InletTwoConcentration}"); + DirtyUI(uid, mixer); + } + + /// + /// Returns the gas mixture for the gas analyzer + /// + private void OnMixerAnalyzed(EntityUid uid, HydrogenGasMixerComponent component, GasAnalyzerScanEvent args) + { + args.GasMixtures ??= new List<(string, GasMixture?)>(); + + // multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system + if (_nodeContainer.TryGetNode(uid, component.InletOneName, out PipeNode? inletOne) && inletOne.Air.Volume != 0f) + { + var inletOneAirLocal = inletOne.Air.Clone(); + inletOneAirLocal.Multiply(inletOne.Volume / inletOne.Air.Volume); + inletOneAirLocal.Volume = inletOne.Volume; + args.GasMixtures.Add(($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOneAirLocal)); + } + if (_nodeContainer.TryGetNode(uid, component.InletTwoName, out PipeNode? inletTwo) && inletTwo.Air.Volume != 0f) + { + var inletTwoAirLocal = inletTwo.Air.Clone(); + inletTwoAirLocal.Multiply(inletTwo.Volume / inletTwo.Air.Volume); + inletTwoAirLocal.Volume = inletTwo.Volume; + args.GasMixtures.Add(($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwoAirLocal)); + } + if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f) + { + var outletAirLocal = outlet.Air.Clone(); + outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume); + outletAirLocal.Volume = outlet.Volume; + args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal)); + } + + args.DeviceFlipped = inletOne != null && inletTwo != null && inletOne.CurrentPipeDirection.ToDirection() == inletTwo.CurrentPipeDirection.ToDirection().GetClockwise90Degrees(); + } + } +} diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenPressurePump/Systems/HydrogenGasPressurePumpSystem.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenPressurePump/Systems/HydrogenGasPressurePumpSystem.cs new file mode 100644 index 00000000000..9bc8e94fcc7 --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenPressurePump/Systems/HydrogenGasPressurePumpSystem.cs @@ -0,0 +1,58 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Imperial.Atmos.Components; +using Content.Shared.Imperial.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Shared.Audio; +using JetBrains.Annotations; +using Content.Shared.Atmos.Components; + +namespace Content.Server.Imperial.Atmos.Piping.Binary.EntitySystems; + +[UsedImplicitly] +public sealed class HydrogenGasPressurePumpSystem : SharedHydrogenGasPressurePumpSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + [Dependency] private readonly PowerReceiverSystem _power = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPumpUpdated); + } + + private void OnPumpUpdated(Entity ent, ref AtmosDeviceUpdateEvent args) + { + if (!ent.Comp.Enabled + || !_power.IsPowered(ent) + || !_nodeContainer.TryGetNodes(ent.Owner, ent.Comp.InletName, ent.Comp.OutletName, out PipeNode? inlet, out PipeNode? outlet)) + { + _ambientSoundSystem.SetAmbience(ent, false); + return; + } + + var outputStartingPressure = outlet.Air.Pressure; + + if (outputStartingPressure >= ent.Comp.TargetPressure) + { + _ambientSoundSystem.SetAmbience(ent, false); + return; // No need to pump gas if target has been reached. + } + + if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0) + { + // We calculate the necessary moles to transfer using our good ol' friend PV=nRT. + var pressureDelta = ent.Comp.TargetPressure - outputStartingPressure; + var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R); + + var removed = inlet.Air.Remove(transferMoles); + _atmosphereSystem.Merge(outlet.Air, removed); + _ambientSoundSystem.SetAmbience(ent, removed.TotalMoles > 0f); + } + } +} diff --git a/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenVolumePump/Systems/HydrogenGasVolumePumpSystem.cs b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenVolumePump/Systems/HydrogenGasVolumePumpSystem.cs new file mode 100644 index 00000000000..a07c9e73350 --- /dev/null +++ b/Content.Server/Imperial/Atmos/Hydrogen/Pipes/HydrogenVolumePump/Systems/HydrogenGasVolumePumpSystem.cs @@ -0,0 +1,123 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Monitor.Systems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.DeviceNetwork.Systems; +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Shared.Imperial.Atmos.Piping.Binary.Systems; +using Content.Shared.Atmos.Piping.Components; +using Content.Shared.Audio; +using Content.Shared.DeviceNetwork; +using Content.Shared.DeviceNetwork.Events; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.Imperial.Atmos.Piping.Binary.Components; +using Content.Shared.Atmos.Components; + +namespace Content.Server.Imperial.Atmos.Piping.Binary.EntitySystems +{ + [UsedImplicitly] + public sealed class HydrogenGasVolumePumpSystem : SharedHydrogenGasVolumePumpSystem + { + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnVolumePumpUpdated); + SubscribeLocalEvent(OnVolumePumpLeaveAtmosphere); + + SubscribeLocalEvent(OnPacketRecv); + } + + private void OnVolumePumpUpdated(EntityUid uid, HydrogenGasVolumePumpComponent pump, ref AtmosDeviceUpdateEvent args) + { + if (!pump.Enabled || + (TryComp(uid, out var power) && !power.Powered) || + !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet)) + { + _ambientSoundSystem.SetAmbience(uid, false); + return; + } + + var inputStartingPressure = inlet.Air.Pressure; + var outputStartingPressure = outlet.Air.Pressure; + + var previouslyBlocked = pump.Blocked; + pump.Blocked = false; + + // Pump mechanism won't do anything if the pressure is too high/too low unless you overclock it. + if ((inputStartingPressure < pump.LowerThreshold) || (outputStartingPressure > pump.HigherThreshold) && !pump.Overclocked) + { + pump.Blocked = true; + } + + // Overclocked pumps can only force gas a certain amount. + if ((outputStartingPressure - inputStartingPressure > pump.OverclockThreshold) && pump.Overclocked) + { + pump.Blocked = true; + } + + if (previouslyBlocked != pump.Blocked) + UpdateAppearance(uid, pump); + if (pump.Blocked) + return; + + // We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters. + var removed = inlet.Air.RemoveVolume(pump.TransferRate * _atmosphereSystem.PumpSpeedup() * args.dt); + + // Some of the gas from the mixture leaks when overclocked. + if (pump.Overclocked) + { + var tile = _atmosphereSystem.GetTileMixture(uid, excite: true); + + if (tile != null) + { + var leaked = removed.RemoveRatio(pump.LeakRatio); + _atmosphereSystem.Merge(tile, leaked); + } + } + + pump.LastMolesTransferred = removed.TotalMoles; + + _atmosphereSystem.Merge(outlet.Air, removed); + _ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f); + } + + private void OnVolumePumpLeaveAtmosphere(EntityUid uid, HydrogenGasVolumePumpComponent pump, ref AtmosDeviceDisabledEvent args) + { + pump.Enabled = false; + Dirty(uid, pump); + UpdateAppearance(uid, pump); + _userInterfaceSystem.CloseUi(uid, HydrogenGasVolumePumpUiKey.Key); + } + + private void OnPacketRecv(EntityUid uid, HydrogenGasVolumePumpComponent component, DeviceNetworkPacketEvent args) + { + if (!TryComp(uid, out DeviceNetworkComponent? netConn) + || !args.Data.TryGetValue(DeviceNetworkConstants.Command, out var cmd)) + { + return; + } + + var payload = new NetworkPayload(); + + switch (cmd) + { + case AtmosDeviceNetworkSystem.SyncData: + payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData); + payload.Add(AtmosDeviceNetworkSystem.SyncData, new HydrogenGasVolumePumpData(component.LastMolesTransferred)); + + _deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, device: netConn); + return; + } + } + } +} diff --git a/Content.Server/Imperial/Atmos/PPGenerator/Components/PPCirculatorComponent.cs b/Content.Server/Imperial/Atmos/PPGenerator/Components/PPCirculatorComponent.cs new file mode 100644 index 00000000000..206a55bb162 --- /dev/null +++ b/Content.Server/Imperial/Atmos/PPGenerator/Components/PPCirculatorComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Imperial.Power.Generation.PPG; + +[RegisterComponent] +[Access(typeof(PPGSystem))] +public sealed partial class PPGCirculatorComponent : Component +{ +} diff --git a/Content.Server/Imperial/Atmos/PPGenerator/Components/PPGeneratorComponent.cs b/Content.Server/Imperial/Atmos/PPGenerator/Components/PPGeneratorComponent.cs new file mode 100644 index 00000000000..e6265c7408e --- /dev/null +++ b/Content.Server/Imperial/Atmos/PPGenerator/Components/PPGeneratorComponent.cs @@ -0,0 +1,33 @@ +namespace Content.Server.Imperial.Power.Generation.PPG; + +[RegisterComponent] +[Access(typeof(PPGSystem))] +public sealed partial class PPGeneratorComponent : Component +{ + [DataField] + public float RampFactor = 1.05f; + [DataField] + public int PowerLevel; + [DataField] + public float Power = 0f; + [DataField] + public bool Active = false; + [DataField] + public float InitMissingGasTwo = 0f; + [DataField] + public float InitMissingGas = 0f; + [DataField] + public bool DeuteriumReactionActive = false; + [DataField] + public bool SecondaryGasActive = false; + [DataField] + public bool PrimaryGasActive = false; + [DataField] + public float Deuterium = 0f; + [DataField] + public float MinimumEnergy = 500f; + [DataField] + public float MinimumPressure = 10f; + [DataField] + public float MissingDeuteriumRate = 0.5f; +} diff --git a/Content.Server/Imperial/Atmos/PPGenerator/NodeGroups/NodeGroup.cs b/Content.Server/Imperial/Atmos/PPGenerator/NodeGroups/NodeGroup.cs new file mode 100644 index 00000000000..19f5b8895c1 --- /dev/null +++ b/Content.Server/Imperial/Atmos/PPGenerator/NodeGroups/NodeGroup.cs @@ -0,0 +1,169 @@ +using System.Linq; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.NodeGroups; +using Content.Server.NodeContainer.Nodes; +using Content.Shared.NodeContainer; +using Content.Shared.NodeContainer.NodeGroups; +using Robust.Server.GameObjects; +using Robust.Shared.Map.Components; +using Robust.Shared.Utility; + +namespace Content.Server.Imperial.Power.Generation.PPG; + +[NodeGroup(NodeGroupID.PPG)] +public sealed class PPGNodeGroup : BaseNodeGroup +{ + [ViewVariables(VVAccess.ReadWrite)] + public bool IsFullyBuilt { get; private set; } + [ViewVariables(VVAccess.ReadWrite)] + public PPGNodeGenerator? Generator { get; private set; } + [ViewVariables(VVAccess.ReadWrite)] + public PPGNodeCirculator? CirculatorA { get; set; } + [ViewVariables(VVAccess.ReadWrite)] + public PPGNodeCirculator? CirculatorB { get; set; } + private IEntityManager? _entityManager; + + public override void Initialize(Node sourceNode, IEntityManager entMan) + { + base.Initialize(sourceNode, entMan); + + _entityManager = entMan; + } + public override void LoadNodes(List groupNodes) + { + DebugTools.Assert(_entityManager != null); + + base.LoadNodes(groupNodes); + + if (groupNodes.Count > 3) + { + return; + } + + Generator = groupNodes.OfType().SingleOrDefault(); + if (Generator != null) + { + // If we have a generator, we can assign CirculatorA and CirculatorB based on relative rotation. + var xformGenerator = _entityManager.GetComponent(Generator.Owner); + var genDir = xformGenerator.LocalRotation.GetDir(); + + foreach (var node in groupNodes) + { + if (node is not PPGNodeCirculator circulator) + continue; + + var xform = _entityManager.GetComponent(node.Owner); + var dir = xform.LocalRotation.GetDir(); + if (genDir.GetClockwise90Degrees() == dir) + { + CirculatorA = circulator; + } + else + { + CirculatorB = circulator; + } + } + + } + if (Generator != null && CirculatorA != null && CirculatorB != null && CirculatorA.NodeGroup != null && CirculatorB.NodeGroup != null) + IsFullyBuilt = true; + else + IsFullyBuilt = false; + + var ppgSystem = _entityManager.EntitySysManager.GetEntitySystem(); + foreach (var node in groupNodes) + { + if (node is PPGNodeGenerator generator) + ppgSystem.UpdateGeneratorConnectivity(generator.Owner, this); + + if (node is PPGNodeCirculator circulator) + ppgSystem.UpdateCirculatorConnectivity(circulator.Owner, this); + } + } +} + +[DataDefinition] +public sealed partial class PPGNodeGenerator : Node +{ + public override IEnumerable GetReachableNodes( + Entity xform, + EntityQuery nodeQuery, + EntityQuery xformQuery, + Entity? grid, + IEntityManager entMan) + { + if (!xform.Comp.Anchored || grid == null) + yield break; + + var mapSystm = entMan.System(); + var gridIndex = mapSystm.TileIndicesFor(grid.Value, xform.Comp.Coordinates); + + var dir = xform.Comp.LocalRotation.GetDir(); + var a = FindCirculator(dir); + var b = FindCirculator(dir.GetOpposite()); + + if (a != null) + yield return a; + + if (b != null) + yield return b; + + PPGNodeCirculator? FindCirculator(Direction searchDir) + { + var targetIdx = gridIndex.Offset(searchDir); + + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx)) + { + if (node is not PPGNodeCirculator circulator) + continue; + + var entity = node.Owner; + var entityXform = xformQuery.GetComponent(entity); + var entityDir = entityXform.LocalRotation.GetDir(); + + if (entityDir == searchDir.GetClockwise90Degrees()) + return circulator; + } + + return null; + } + } +} + +[DataDefinition] +public sealed partial class PPGNodeCirculator : Node +{ + public override IEnumerable GetReachableNodes( + Entity xform, + EntityQuery nodeQuery, + EntityQuery xformQuery, + Entity? grid, + IEntityManager entMan) + { + if (!xform.Comp.Anchored || grid == null) + yield break; + + var mapSystm = entMan.System(); + var gridIndex = mapSystm.TileIndicesFor(grid.Value, xform.Comp.Coordinates); + + var dir = xform.Comp.LocalRotation.GetDir(); + var searchDir = dir.GetClockwise90Degrees(); + var targetIdx = gridIndex.Offset(searchDir); + + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx)) + { + if (node is not PPGNodeGenerator generator) + continue; + + var entity = node.Owner; + var entityXform = xformQuery.GetComponent(entity); + var entityDir = entityXform.LocalRotation.GetDir(); + + if (entityDir == searchDir || entityDir == searchDir.GetOpposite()) + { + yield return generator; + break; + } + } + } +} diff --git a/Content.Server/Imperial/Atmos/PPGenerator/Systems/PPGeneratorSystem.cs b/Content.Server/Imperial/Atmos/PPGenerator/Systems/PPGeneratorSystem.cs new file mode 100644 index 00000000000..8f4b45e9407 --- /dev/null +++ b/Content.Server/Imperial/Atmos/PPGenerator/Systems/PPGeneratorSystem.cs @@ -0,0 +1,245 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Atmos.Piping.Components; +using Content.Server.Audio; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Shared.Atmos; +using Content.Shared.Examine; +using Content.Shared.NodeContainer; +using Content.Shared.Power; +using Content.Shared.Power.EntitySystems; +using Robust.Server.GameObjects; +using Robust.Shared.Utility; +using Content.Shared.Imperial.Power.Generation.PPG; +using Content.Server.Imperial.Atmos.Reactions.Prototypes; +using System.Linq; +using Robust.Shared.Prototypes; +using Content.Shared.Atmos.Components; + +namespace Content.Server.Imperial.Power.Generation.PPG; + +public sealed class PPGSystem : EntitySystem +{ + private const string NodeNamePPG = "ppg"; + private const string NodeNameInlet = "inlet"; + private const string NodeNameOutlet = "outlet"; + [Dependency] private readonly AmbientSoundSystem _ambientSound = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + + private EntityQuery _nodeContainerQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(GeneratorUpdate); + SubscribeLocalEvent(GeneratorPowerChange); + + SubscribeLocalEvent(GeneratorExamined); + _nodeContainerQuery = GetEntityQuery(); + } + + private void GeneratorExamined(EntityUid uid, PPGeneratorComponent component, ExaminedEvent args) + { + if (GetNodeGroup(uid) is not { IsFullyBuilt: true }) + { + args.PushMarkup(Loc.GetString("teg-generator-examine-connection")); + } + else + { + var supplier = Comp(uid); + + using (args.PushGroup(nameof(PPGeneratorComponent))) + { + args.PushMarkup(Loc.GetString("teg-generator-examine-power", ("power", supplier.CurrentSupply))); + args.PushMarkup(Loc.GetString("teg-generator-examine-power-max-output", ("power", supplier.MaxSupply))); + } + } + } + + public void GeneratorUpdate(EntityUid uid, PPGeneratorComponent component, ref AtmosDeviceUpdateEvent args) + { + var supplier = Comp(uid); + var powerReceiver = Comp(uid); + if (!powerReceiver.Powered) + { + supplier.MaxSupply = 0; + return; + } + + var ppgGroup = GetNodeGroup(uid); + if (ppgGroup is not { IsFullyBuilt: true }) + return; + + var gasReactions = _protoMan.EnumeratePrototypes().ToArray(); + var circA = ppgGroup.CirculatorA!.Owner; + var circB = ppgGroup.CirculatorB!.Owner; + var (inletA, outletA) = GetPipes(circA); + var (inletB, outletB) = GetPipes(circB); + + var (airA, δpA) = GetCirculatorAirTransfer(inletA.Air, outletA.Air); + var (airB, δpB) = GetCirculatorAirTransfer(inletB.Air, outletB.Air); + var initACap = airA.Pressure; + var initBCap = airB.Pressure; + foreach (var prototype in gasReactions) + { + var initMissingGasIDA = airA.GetMoles(prototype.MissingGasID); + var initMissingGasIDB = airB.GetMoles(prototype.MissingGasID); + var initMissingDeuteriumA = airA.GetMoles(Gas.Deuterium); + var initMissingDeuteriumB = airB.GetMoles(Gas.Deuterium); + var missingDeuteriumA = component.MissingDeuteriumRate * initMissingDeuteriumA; + var missingDeuteriumB = component.MissingDeuteriumRate * initMissingDeuteriumB; + if (initMissingDeuteriumA > 0.1 || initMissingDeuteriumB > 0.1) + { + component.DeuteriumReactionActive = true; + airA.AdjustMoles(Gas.Deuterium, -missingDeuteriumA); + airB.AdjustMoles(Gas.Deuterium, -missingDeuteriumB); + } + component.Deuterium = initMissingDeuteriumA + initMissingDeuteriumB; + if (prototype.UseGasTwo) + { + var initMissingGasIDTwoA = airA.GetMoles(prototype.MissingGasIDTwo); + var initMissingGasIDTwoB = airB.GetMoles(prototype.MissingGasIDTwo); + if (initMissingGasIDTwoA > 0.1 || initMissingGasIDTwoB > 0.1) + { + component.SecondaryGasActive = true; + airA.AdjustMoles(prototype.MissingGasIDTwo, -initMissingGasIDTwoA); + airB.AdjustMoles(prototype.MissingGasIDTwo, -initMissingGasIDTwoB); + } + component.InitMissingGasTwo = initMissingGasIDTwoA + initMissingGasIDTwoB; + } + if (!prototype.UseGasTwo) + component.SecondaryGasActive = true; + if (initMissingGasIDA > 0.1 || initMissingGasIDB > 0.1) + component.PrimaryGasActive = true; + if (component.DeuteriumReactionActive && component.SecondaryGasActive && component.PrimaryGasActive) + { + airA.AdjustMoles(prototype.MissingGasID, -initMissingGasIDA); + airB.AdjustMoles(prototype.MissingGasID, -initMissingGasIDB); + var initMissingGas = initMissingGasIDA + initMissingGasIDB; + // output gas = Missing Gas moles (Id's in reaction prototype) * Missing Second Gas moles (Id's in reaction prototype) * (OutputFactor in reaction prototype / 10) + var output = initMissingGas * component.InitMissingGasTwo * (prototype.OutputFactor / 10); + airA.AdjustMoles(prototype.AddedGasID, output); + airB.AdjustMoles(prototype.AddedGasID, output); + // power = EnergyScale in reaction prototype * (Capacity A circulator + Capacity B circulator) + var power = prototype.EnergyScale * (initACap + initBCap); + supplier.MaxSupply = power * component.RampFactor; + if (power > component.MinimumEnergy) + component.Active = true; + if (power < component.MinimumEnergy) + component.Active = false; + UpdateAppearance(uid, component, powerReceiver, ppgGroup); + } + if (initACap < component.MinimumPressure && initBCap < component.MinimumPressure) + { + component.DeuteriumReactionActive = false; + component.SecondaryGasActive = false; + component.PrimaryGasActive = false; + } + _atmosphere.Merge(outletA.Air, airA); + _atmosphere.Merge(outletB.Air, airB); + continue; + } + return; + } + + private void UpdateAppearance( + EntityUid uid, + PPGeneratorComponent component, + ApcPowerReceiverComponent powerReceiver, + PPGNodeGroup nodeGroup) + { + _ambientSound.SetAmbience(uid, component.Active); + if (component.Active && GetNodeGroup(uid) is { IsFullyBuilt: true }) + component.PowerLevel = 1; + else + component.PowerLevel = 0; + _appearance.SetData(uid, PPGVisualsState.PowerOutput, component.PowerLevel); + } + + [Access(typeof(PPGNodeGroup))] + public void UpdateGeneratorConnectivity( + EntityUid uid, + PPGNodeGroup group, + PPGeneratorComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + var powerReceiver = Comp(uid); + _receiver.SetPowerDisabled(uid, !group.IsFullyBuilt, powerReceiver); + UpdateAppearance(uid, component, powerReceiver, group); + } + + [Access(typeof(PPGNodeGroup))] + public void UpdateCirculatorConnectivity( + EntityUid uid, + PPGNodeGroup group, + PPGCirculatorComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + } + + private void GeneratorPowerChange(EntityUid uid, PPGeneratorComponent component, ref PowerChangedEvent args) + { + // TODO: I wish power events didn't go out on shutdown. + if (TerminatingOrDeleted(uid)) + return; + var nodeGroup = GetNodeGroup(uid); + if (nodeGroup == null) + return; + UpdateAppearance(uid, component, Comp(uid), nodeGroup); + } + + /// Null if the node group is not yet available. This can happen during initialization. + private PPGNodeGroup? GetNodeGroup(EntityUid uidGenerator) + { + NodeContainerComponent? nodeContainer = null; + if (!_nodeContainerQuery.Resolve(uidGenerator, ref nodeContainer)) + return null; + + if (!nodeContainer.Nodes.TryGetValue(NodeNamePPG, out var ppgNode)) + return null; + + if (ppgNode.NodeGroup is not PPGNodeGroup ppgGroup) + return null; + + return ppgGroup; + } + + private static (GasMixture, float δp) GetCirculatorAirTransfer(GasMixture airInlet, GasMixture airOutlet) + { + var n1 = airInlet.TotalMoles; + var n2 = airOutlet.TotalMoles; + var p1 = airInlet.Pressure; + var p2 = airOutlet.Pressure; + var v1 = airInlet.Volume; + var v2 = airOutlet.Volume; + var t1 = airInlet.Temperature; + var t2 = airOutlet.Temperature; + + var deltap = p1 - p2; + + var denom = t1 * v2 + t2 * v1; + + if (deltap > 0 && p1 > 0 && denom > 0) + { + var transferMoles = n1 - (n1 + n2) * t2 * v1 / denom; + return (airInlet.Remove(transferMoles), deltap); + } + + return (new GasMixture(), deltap); + } + + private (PipeNode inlet, PipeNode outlet) GetPipes(EntityUid uidCirculator) + { + var nodeContainer = _nodeContainerQuery.GetComponent(uidCirculator); + var inlet = (PipeNode)nodeContainer.Nodes[NodeNameInlet]; + var outlet = (PipeNode)nodeContainer.Nodes[NodeNameOutlet]; + + return (inlet, outlet); + } +} diff --git a/Content.Server/Imperial/Atmos/PPGenerator/license.txt b/Content.Server/Imperial/Atmos/PPGenerator/license.txt new file mode 100644 index 00000000000..d4251c94752 --- /dev/null +++ b/Content.Server/Imperial/Atmos/PPGenerator/license.txt @@ -0,0 +1,2 @@ +license: This content is under ICLA. Copyright holder: kaktyc900 (discord) (Read more on: https://wiki.imperialspace.net/ru/icla) +copyright: "kaktyc900 (discord)" \ No newline at end of file diff --git a/Content.Server/Imperial/Atmos/Reactions/DeuteriumProductionReaction.cs b/Content.Server/Imperial/Atmos/Reactions/DeuteriumProductionReaction.cs new file mode 100644 index 00000000000..78f2c682dfb --- /dev/null +++ b/Content.Server/Imperial/Atmos/Reactions/DeuteriumProductionReaction.cs @@ -0,0 +1,35 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Reactions; +using JetBrains.Annotations; +using Content.Server.Atmos.Reactions; +using Content.Server.Atmos; + +namespace Content.Server.Imperial.Atmos.Reactions; + +[UsedImplicitly] +public sealed partial class DeuteriumProductionReaction : IGasReactionEffect +{ + public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale) + { + var capacity = mixture.Pressure; + if (capacity > Atmospherics.DeuteriumProductionMaxCapacity) + return ReactionResult.NoReaction; + var initialHydrogen = mixture.GetMoles(Gas.Hydrogen); + var initialNitr = mixture.GetMoles(Gas.Nitrogen); + + var efficiency = mixture.Temperature / Atmospherics.DeuteriumProductionMaxEfficiencyTemperature; + + var hydrConversion = initialHydrogen / Atmospherics.DeuteriumProductionConversionRate; + var nitrConversion = initialNitr / Atmospherics.DeuteriumProductionConversionRate; + var total = nitrConversion + hydrConversion; + + mixture.AdjustMoles(Gas.Hydrogen, -hydrConversion); + mixture.AdjustMoles(Gas.Nitrogen, -nitrConversion); + mixture.AdjustMoles(Gas.Deuterium, total * efficiency); + mixture.AdjustMoles(Gas.Tritium, total * efficiency); + + return ReactionResult.Reacting; + } + +} diff --git a/Content.Server/Imperial/Atmos/Reactions/HydrogenFireReaction.cs b/Content.Server/Imperial/Atmos/Reactions/HydrogenFireReaction.cs index 82ba1fb672e..f37a0cdc561 100644 --- a/Content.Server/Imperial/Atmos/Reactions/HydrogenFireReaction.cs +++ b/Content.Server/Imperial/Atmos/Reactions/HydrogenFireReaction.cs @@ -23,7 +23,6 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, Atmos mixture.ReactionResults[(byte)GasReaction.Fire] = 0f; var burnedFuel = 0f; var initialHydrogen = mixture.GetMoles(Gas.Hydrogen); - if (mixture.GetMoles(Gas.Oxygen) < initialHydrogen || Atmospherics.MinimumHydrogenOxyburnEnergy > (temperature * oldHeatCapacity * heatScale)) { @@ -36,7 +35,7 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, Atmos else { burnedFuel = initialHydrogen; - mixture.SetMoles(Gas.Hydrogen, mixture.GetMoles(Gas.Hydrogen ) * (1 - 1 / Atmospherics.HydrogenBurnHydrFactor)); + mixture.SetMoles(Gas.Hydrogen, mixture.GetMoles(Gas.Hydrogen) * (1 - 1 / Atmospherics.HydrogenBurnHydrFactor)); mixture.AdjustMoles(Gas.Oxygen, -mixture.GetMoles(Gas.Hydrogen)); energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel * (Atmospherics.HydrogenBurnHydrFactor - 1)); } @@ -45,15 +44,12 @@ public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, Atmos { energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel); - // TODO ATMOS Radiation pulse here! - - // Conservation of mass is important. mixture.AdjustMoles(Gas.WaterVapor, burnedFuel); mixture.ReactionResults[(byte)GasReaction.Fire] += burnedFuel; } - energyReleased /= heatScale; // adjust energy to make sure speedup doesn't cause mega temperature rise + energyReleased /= heatScale; if (energyReleased > 0) { var newHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true); diff --git a/Content.Server/Imperial/Atmos/Reactions/N2OzoniumProductionReaction.cs b/Content.Server/Imperial/Atmos/Reactions/N2OzoniumProductionReaction.cs deleted file mode 100644 index c511d3b7190..00000000000 --- a/Content.Server/Imperial/Atmos/Reactions/N2OzoniumProductionReaction.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Shared.Atmos; -using Content.Shared.Atmos.Reactions; -using JetBrains.Annotations; -using Content.Server.Atmos.Reactions; -using Content.Server.Atmos; - -namespace Content.Server.Imperial.Atmos.Reactions; - -[UsedImplicitly] -public sealed partial class N2OzoniumProductionReaction : IGasReactionEffect -{ - public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale) - { - var initialNit = mixture.GetMoles(Gas.Nitrogen); - var initialOzon = mixture.GetMoles(Gas.Ozonium); - - var efficiency = mixture.Temperature / Atmospherics.N2OzonProductionMaxEfficiencyTemperature; - - var nitConversion = initialNit / Atmospherics.N2OzonProductionConversionRate; - var ozonConversion = initialOzon / Atmospherics.N2OzonProductionConversionRate; - var total = nitConversion + ozonConversion; - - mixture.AdjustMoles(Gas.Nitrogen, -nitConversion); - mixture.AdjustMoles(Gas.Ozonium, -ozonConversion); - mixture.AdjustMoles(Gas.NitrousOxide, total * efficiency); - - return ReactionResult.Reacting; - } - -} diff --git a/Content.Server/Imperial/BroadphaseCheck/BroadphaseCheckSystem.cs b/Content.Server/Imperial/BroadphaseCheck/BroadphaseCheckSystem.cs deleted file mode 100644 index b50c9eb30da..00000000000 --- a/Content.Server/Imperial/BroadphaseCheck/BroadphaseCheckSystem.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Reflection; -using Content.Shared.Imperial.ICCVar; -using Robust.Server.Containers; -using Robust.Shared.Configuration; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; -using Robust.Shared.Player; -using Robust.Shared.Timing; - -namespace Content.Server.Imperial.BroadphaseCheck; - - -/// -/// Checks all players broadphase. If the player is not in the container and the broadphase is null, we update it using reflection -/// -/// To do this, it would be enough to simply put a try-catch in the engine, so when the wizards fix it, this system will need to be removed. -/// -/// -public sealed partial class BroadphaseCheckSystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; - [Dependency] private readonly ContainerSystem _containerSystem = default!; - [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - - private TimeSpan _updateRate = TimeSpan.FromSeconds(1); - private TimeSpan _nextUpdate = TimeSpan.Zero; - private bool _enabled = false; - - - public override void Initialize() - { - base.Initialize(); - - _enabled = _configurationManager.GetCVar(ICCVars.BroadphaseCheckEnabled); - _updateRate = TimeSpan.FromSeconds(_configurationManager.GetCVar(ICCVars.BroadphaseCheckUpdateRate)); - - _configurationManager.OnValueChanged(ICCVars.BroadphaseCheckEnabled, newVal => _enabled = newVal); - _configurationManager.OnValueChanged(ICCVars.BroadphaseCheckUpdateRate, newVal => _updateRate = TimeSpan.FromSeconds(newVal)); - } - - - public override void Update(float frameTime) - { - base.Update(frameTime); - - if (!_enabled) return; - if (_timing.CurTime <= _nextUpdate) return; - - _nextUpdate = _timing.CurTime + _updateRate; - - var enumerator = EntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var transformComponent, out var _, out var _, out var _)) - { - if (_entityLookupSystem.TryGetCurrentBroadphase(transformComponent, out var _)) continue; - - var metaData = MetaData(uid); - - if ((metaData.Flags & MetaDataFlags.InContainer) == 0) continue; - if (_containerSystem.TryGetContainingContainer((uid, transformComponent, metaData), out var _)) continue; - - FixEntityBroadphaseTree(uid, transformComponent); - } - } - - #region Public API - - public void FixEntityBroadphaseTree(EntityUid target, TransformComponent? transformComponent = null) - { - if (!Resolve(target, ref transformComponent)) - return; - - _metaDataSystem.RemoveFlag(target, MetaDataFlags.InContainer); - _containerSystem.AttachParentToContainerOrGrid((target, transformComponent)); - - // Change transformComponent.Broadphase with reflection because it iternal - var fields = transformComponent.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic); - foreach (var field in fields) - { - if (field.Name != "Broadphase") continue; - - field.SetValue(transformComponent, null); - } - - _entityLookupSystem.FindAndAddToEntityTree(target, true, transformComponent); - } - - #endregion -} diff --git a/Content.Server/Imperial/BroadphaseCheck/license.txt b/Content.Server/Imperial/BroadphaseCheck/license.txt deleted file mode 100644 index 1c9a3287cb8..00000000000 --- a/Content.Server/Imperial/BroadphaseCheck/license.txt +++ /dev/null @@ -1,2 +0,0 @@ -license: This content is under ICLA. Copyright holder: orix0689 (discord) (Read more on: https://wiki.imperialspace.net/ru/icla) -copyright: "orix0689 (discord)" diff --git a/Content.Server/Imperial/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/Imperial/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs new file mode 100644 index 00000000000..5f0f020255f --- /dev/null +++ b/Content.Server/Imperial/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs @@ -0,0 +1,825 @@ +using Content.Server.Administration.Logs; +using Content.Server.CartridgeLoader; +using Content.Server.GameTicking; +using Content.Server.Power.Components; +using Content.Shared.Access.Components; +using Content.Shared.CartridgeLoader; +using Content.Shared.Database; +using Content.Shared.GPS.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Imperial.CartridgeLoader.Cartridges; +using Content.Shared.PDA; +using Content.Shared.Paper; +using Content.Shared.Power; +using Content.Shared.StatusIcon; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Linq; +using Content.Shared.Whitelist; + +namespace Content.Server.Imperial.CartridgeLoader.Cartridges; + +/// +/// Система, отвечающая за работу функционала картриджей НаноЧата. +/// Управляет связью с серверами, обработкой сообщений, синхронизацией пользователей и интерфейсом. +/// +/// +/// +public sealed class NanoChatCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _loaderSystem = null!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = null!; + [Dependency] private readonly GameTicker _gameTicker = null!; + [Dependency] private readonly IAdminLogManager _adminLogger = null!; + [Dependency] private readonly IComponentFactory _componentFactory = null!; + [Dependency] private readonly IGameTiming _timing = null!; + [Dependency] private readonly IPrototypeManager _prototype = null!; + [Dependency] private readonly MetaDataSystem _metaDataSystem = null!; + [Dependency] private readonly PaperSystem _paper = null!; + [Dependency] private readonly SharedAudioSystem _audio = null!; + [Dependency] private readonly SharedHandsSystem _hands = null!; + [Dependency] private readonly SharedUserInterfaceSystem _userInterfaceSystem = null!; + [Dependency] private readonly TransformSystem _transform = null!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUiMessage); + SubscribeLocalEvent(OnUiReady); + + SubscribeLocalEvent(OnCartridgeAdded); + SubscribeLocalEvent(OnComponentCartridgeRemoved); + + SubscribeLocalEvent(OnPowerChanged); + + SubscribeLocalEvent(OnServerStartup); + SubscribeLocalEvent(OnServerShutdown); + } + + private void OnUiMessage(Entity ent, ref CartridgeMessageEvent args) + { + switch (args) + { + case NanoChatCreateChatEvent create: + HandleCreateChat(ent, create.ChatName, create.Actor); + break; + case NanoChatEditChatEvent edit: + HandleEditChat(ent, edit); + break; + + case NanoChatAddMembersEvent addMembers: + HandleAddMembers(ent, addMembers); + break; + case NanoChatRemoveMembersEvent removeMembers: + HandleRemoveMembers(ent, removeMembers); + break; + + case NanoChatSelectChatEvent select: + ent.Comp.SelectedChat = select.ChatId; + ent.Comp.UnreadMessages.Remove(select.ChatId); + UpdateUi(ent); + break; + + case NanoChatSendEvent send: + HandleSendText(ent, send.Text, args.Actor); + break; + case NanoChatTypingEvent: + HandleTyping(ent); + break; + case NanoChatPrintEvent print: + HandlePrintChat(ent, print); + break; + + + case NanoChatUiActionEvent action: + switch (action.Action) + { + case NanoChatUiAction.NotificationSwitch: + ent.Comp.NotificationsOn = !ent.Comp.NotificationsOn; + UpdateUi(ent); + break; + case NanoChatUiAction.SendLocation: + var parent = Transform(ent).ParentUid; + if (!HasComp(parent)) + return; + + var posText = Loc.GetString("nano-chat-ui-chat-windows-message-error"); + var pos = _transform.GetMapCoordinates(ent); + + if (pos.MapId != MapId.Nullspace) + { + var x = (int) pos.Position.X; + var y = (int) pos.Position.Y; + posText = $"({x}, {y})"; + } + + HandleSendText(ent, Loc.GetString("handheld-gps-coordinates-title", ("coordinates", posText)), args.Actor); + break; + default: + return; + } + break; + } + } + + private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args) + { + SyncUserToServer(ent, out var server); + if (ent.Comp.SelectedChat != null) + ent.Comp.UnreadMessages.Remove(ent.Comp.SelectedChat.Value); + + + if (server != null) + UpdateAllClients(server.Value); + else + UpdateUi(ent); + } + + private void OnCartridgeAdded(Entity ent, ref CartridgeAddedEvent args) + { + SyncUserToServer(ent, out var server); + if (server != null) + UpdateAllClients(server.Value); + } + + private void OnComponentCartridgeRemoved(Entity ent, ref ComponentRemove args) + { + if (ent.Comp.ConnectedServer is not { } serverUid || + !TryComp(serverUid, out NanoChatServerComponent? server)) + return; + + UpdateAllClients((serverUid, server)); + } + + private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) + { + UpdateAllClients(ent); + } + + private void OnServerStartup(Entity ent, ref NanoChatServerStartupEvent args) + { + UpdateAllClients(ent); + } + + private void OnServerShutdown(Entity ent, ref NanoChatServerShutdownEvent args) + { + UpdateAllClients(ent); + } + + + /// + /// Обновляет UI картриджа для клиента. + /// + private void UpdateUi(Entity ent, Entity? server = null) + { + var comp = ent.Comp; + var loaderUid = Transform(ent).ParentUid; + + var typingUsers = new Dictionary(); + var curTime = _timing.CurTime; + + if (!loaderUid.IsValid()) + return; + + if (server == null) + SyncUserToServer(ent, out server); + + var isServerOnline = + server != null && + TryComp(server.Value, out var receiverComponent) && + receiverComponent.Powered; + + var isContactReachable = false; + var contacts = new List(); + var chats = new List(); + NanoChatChat? currentChat = null; + var canSendLocation = false; + + if (isServerOnline && server != null && comp.UserId != null) + { + contacts = ent.Comp.Visible + ? server.Value.Comp.Users.Where(contact => contact.Visible).ToList() + : server.Value.Comp.Users; + + if (ent.Comp.Visible) + { + chats = server.Value.Comp.Chats + .Where(c => c.Members.Contains(comp.UserId.Value)) + .OrderBy(c => c.Name) + .ToList(); + } + else + chats = server.Value.Comp.Chats.OrderBy(c => c.Name).ToList(); + + currentChat = server.Value.Comp.Chats.FirstOrDefault(c => + comp.SelectedChat != null && + c.Id == comp.SelectedChat.Value); + + if (comp.SelectedChat != null && currentChat != null) + { + canSendLocation = HasComp(loaderUid); + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out _, out var other)) + { + if (other.ConnectedServer != server.Value.Owner) + continue; + + if (other.UserId == null || other.UserId == comp.UserId) + continue; + + if (!currentChat.Members.Contains(other.UserId.Value)) + continue; + + isContactReachable = true; + break; + } + } + } + + if (currentChat != null && + server != null && + server.Value.Comp.TypingTimeouts.TryGetValue(currentChat.Id, out var chatTyping)) + { + var expiredUsers = new List(); + + foreach (var (typistId, expiration) in chatTyping) + { + if (curTime > expiration) + { + expiredUsers.Add(typistId); + continue; + } + + if (typistId == comp.UserId) + continue; + + if (server.Value.Comp.Users.All(user => user.Id != typistId)) + continue; + + var contact = server.Value.Comp.Users.First(user => user.Id == typistId); + typingUsers[typistId] = contact.Name; + } + + foreach (var expired in expiredUsers) + chatTyping.Remove(expired); + } + + var state = new NanoChatBoundUserInterfaceState( + comp.NotificationsOn, + comp.UserId, + currentChat, + chats, + contacts, + isServerOnline, + isContactReachable, + canSendLocation, + _timing.CurTime >= comp.NextPrintAllowedAfter, + !ent.Comp.Visible, + comp.UnreadMessages, + typingUsers + ); + + if (TryComp(loaderUid, out var loader) && + _userInterfaceSystem.IsUiOpen(loaderUid, loader.UiKey)) + { + _userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state); + } + } + + + /// + /// Создает новый чат на сервере. + /// + private void HandleCreateChat(Entity creator, string chatName, EntityUid actor, List? initialMembers = null) + { + if (string.IsNullOrWhiteSpace(chatName) || creator.Comp.UserId == null) + return; + + var server = GetServerForCartridge(creator); + if (server == null) + return; + + var members = initialMembers ?? [creator.Comp.UserId.Value]; + + var newChatId = server.Value.Comp.NextChatId++; + var newChat = new NanoChatChat(newChatId, chatName, creator.Comp.UserId.Value, members, [], false); + server.Value.Comp.Chats.Add(newChat); + creator.Comp.SelectedChat = newChatId; + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"НаноЧат: {ToPrettyString(actor):user} создал чат \"{chatName}\" ({newChatId})"); + UpdateAllClients(server.Value); + } + + /// + /// Обрабатывает изменение названия существующего чата. + /// + private void HandleEditChat(Entity sender, NanoChatEditChatEvent args) + { + if (sender.Comp.SelectedChat == null || sender.Comp.UserId == null) + return; + + var server = GetServerForCartridge(sender); + + var chat = server?.Comp.Chats.FirstOrDefault(c => c.Id == args.ChatId + && c.Owner == sender.Comp.UserId.Value); + if (server == null || chat == null) + return; + + var oldName = chat.Name; + chat.Name = args.NewName; + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"НаноЧат: {ToPrettyString(args.Actor):user} переименовал чат ({chat.Id}) \"{oldName}\" в \"{args.NewName}\""); + + Dirty(server.Value); + UpdateAllClients(server.Value); + } + + + /// + /// Добавляет новых участников в указанный чат. + /// + private void HandleAddMembers(Entity ent, NanoChatAddMembersEvent args) + { + var server = GetServerForCartridge(ent); + if (server == null || ent.Comp.UserId == null) + return; + + var chat = server.Value.Comp.Chats.FirstOrDefault(c => c.Id == args.ChatId); + if (chat == null || !chat.Members.Contains(ent.Comp.UserId.Value)) + return; + + if (chat.Automated) + { + var newMembers = chat.Members.ToList(); + var addedAnyNew = false; + + foreach (var newMember in args.AddedMembers.Where(newMember => !newMembers.Contains(newMember))) + { + newMembers.Add(newMember); + addedAnyNew = true; + } + + if (!addedAnyNew) + return; + + var memberNames = newMembers + .Select(id => server.Value.Comp.Users.FirstOrDefault(user => user.Id == id).Name ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")) + .ToList(); + + var newChatName = string.Join(", ", memberNames); + + HandleCreateChat(ent, newChatName, args.Actor, newMembers); + return; + } + + var addedAny = false; + var newGroupMembers = new List(); + foreach (var newMember in args.AddedMembers.Where(newMember => !chat.Members.Contains(newMember))) + { + newGroupMembers.Add(newMember); + addedAny = true; + } + + if (!addedAny) + return; + + chat.Members.AddRange(newGroupMembers); + + var groupAddedString = newGroupMembers.Aggregate("", (current, member) => current + $"{ToPrettyString(member)}, "); + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"НаноЧат: {ToPrettyString(args.Actor):user} добавил [ {groupAddedString} ] в чат \"{chat.Name}\"({chat.Id})"); + + if (string.IsNullOrEmpty(chat.Name)) + { + var memberNames = chat.Members + .Select(id => + server.Value.Comp.Users + .FirstOrDefault(user => user.Id == id) + .Name + ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")) + .ToList(); + + chat.Name = string.Join(", ", memberNames); + } + + Dirty(server.Value); + UpdateAllClients(server.Value); + } + + /// + /// Удаляет участников из указанного чата. + /// + private void HandleRemoveMembers(Entity ent, NanoChatRemoveMembersEvent args) + { + var server = GetServerForCartridge(ent); + if (server == null || ent.Comp.UserId == null) + return; + + var chat = server.Value.Comp.Chats.FirstOrDefault(c => c.Id == args.ChatId); + if (chat == null) + return; + + if (!chat.Members.Contains(ent.Comp.UserId.Value) || chat.Owner != ent.Comp.UserId.Value) + return; + + var removed = args.RemovedMembers + .Where(member => chat.Members.Contains(member)) + .ToList(); + + foreach (var member in removed) + chat.Members.Remove(member); + + if (removed.Count > 0) + { + var removedString = string.Join(", ", + removed.Select(ToPrettyString)); + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"НаноЧат: {ToPrettyString(args.Actor):user} удалил [ {removedString} ] из чата \"{chat.Name}\" ({chat.Id})"); + } + + Dirty(server.Value); + UpdateAllClients(server.Value); + } + + + /// + /// Отправляет текстовое сообщение в выбранный чат и рассылает уведомления участникам. + /// + private void HandleSendText(Entity sender, string text, EntityUid actor) + { + if (string.IsNullOrWhiteSpace(text) || sender.Comp.SelectedChat == null || sender.Comp.UserId == null) + return; + + var server = GetServerForCartridge(sender); + if (server == null) + return; + + var senderId = sender.Comp.UserId.Value; + var chatId = sender.Comp.SelectedChat.Value; + + var chat = server.Value.Comp.Chats.FirstOrDefault(c => c.Id == chatId); + if (chat == null) + return; + + if (server.Value.Comp.TypingTimeouts.TryGetValue(chatId, out var chatTyping)) + chatTyping.Remove(senderId); + + var senderName = sender.Comp.PdaCardName ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown"); + var message = new NanoChatMessage( + senderId, + senderName, + text, + _gameTicker.RoundDuration() + ); + + chat.Messages.Add(message); + Dirty(server.Value); + + var query = EntityQueryEnumerator(); + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"НаноЧат{(chat.Name is not "" ? $" {chat.Name} " : "")} ({chat.Id}) {ToPrettyString(actor):user}: {text}"); + + while (query.MoveNext(out var clientUid, out var targetComp)) + { + if (targetComp.ConnectedServer != server.Value.Owner) + continue; + + if (targetComp.UserId == null) + continue; + + if (!chat.Members.Contains(targetComp.UserId.Value) && targetComp.Visible) + continue; + + if (targetComp.UserId == senderId) + continue; + + if (targetComp.SelectedChat != chatId) + { + targetComp.UnreadMessages.TryAdd(chatId, 0); + targetComp.UnreadMessages[chatId]++; + } + + if (!targetComp.NotificationsOn) + continue; + + var pda = Transform(clientUid).ParentUid; + + if (!TryComp(pda, out var loader) || + _userInterfaceSystem.IsUiOpen(pda, loader.UiKey)) + { + _audio.PlayPvs(targetComp.NotificationSound, pda); + continue; + } + + var notifSender = chat.Automated + ? (sender.Comp.PdaCardName ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown")) + : chat.Name; + var notifMessage = chat.Automated + ? Loc.GetString("nano-chat-pda-notification-message", + ("sender", notifSender)) + : Loc.GetString("nano-chat-pda-notification-message-group", + ("sender", notifSender)); + + _loaderSystem.SendNotification( + pda, + Loc.GetString("nano-chat-pda-notification-header"), + notifMessage, + loader); + } + + UpdateAllClients(server.Value); + } + + /// + /// Регистрирует статус набора текста для текущего пользователя. + /// + private void HandleTyping(Entity sender) + { + if (sender.Comp.SelectedChat == null || sender.Comp.UserId == null) + return; + + var server = GetServerForCartridge(sender); + if (server == null) + return; + + var chatId = sender.Comp.SelectedChat.Value; + var userId = sender.Comp.UserId.Value; + + if (!server.Value.Comp.TypingTimeouts.ContainsKey(chatId)) + server.Value.Comp.TypingTimeouts[chatId] = new Dictionary(); + + server.Value.Comp.TypingTimeouts[chatId][userId] = _timing.CurTime + server.Value.Comp.TypingTimeout; + + Dirty(server.Value); + UpdateAllClients(server.Value); + } + + /// + /// Распечатывает историю чата на бумаге с использованием штампа. + /// + private void HandlePrintChat(Entity ent, NanoChatPrintEvent args) + { + if (ent.Comp.SelectedChat == null || ent.Comp.UserId == null) + return; + + if (_timing.CurTime < ent.Comp.NextPrintAllowedAfter) + return; + + var server = GetServerForCartridge(ent); + + var chat = server?.Comp.Chats.FirstOrDefault(c => c.Id == ent.Comp.SelectedChat.Value); + if (chat == null) + return; + + ent.Comp.NextPrintAllowedAfter = _timing.CurTime + ent.Comp.PrintDelay; + + var loaderUid = Transform(ent).ParentUid; + var coordinates = Transform(loaderUid).Coordinates; + + var printed = Spawn(ent.Comp.PaperId, coordinates); + _hands.PickupOrDrop(args.Actor, printed); + + _audio.PlayPvs(ent.Comp.PrintingSound, loaderUid); + + if (!TryComp(printed, out var paper)) + return; + + _metaDataSystem.SetEntityName(printed, + Loc.GetString("nano-chat-print-name", + ("name", Name(printed)), + ("id", chat.Id))); + + + var msg = new FormattedMessage(); + var chatName = string.IsNullOrEmpty(chat.Name) + ? Loc.GetString("nano-chat-print-default-title") + : chat.Name; + + var spacesCount = Math.Max(0, 19 - (chatName.Length / 2)); + var padding = new string(' ', spacesCount); + + if (!_prototype.TryIndex(ent.Comp.StampId, out var stampProto)) + return; + + if (!stampProto.TryGetComponent(out var stamp, _componentFactory)) + return; + + var stampInfo = new StampDisplayInfo + { + StampedName = stamp.StampedName, + StampedColor = stamp.StampedColor, + }; + + _paper.TryStamp((printed, paper), stampInfo, stamp.StampState); + + msg.AddMarkupOrThrow($"[head=3]{padding}{FormattedMessage.EscapeText(chatName)}"); + msg.PushNewline(); + + foreach (var message in chat.Messages) + { + var timeString = message.SendTime.ToString(@"hh\:mm\:ss"); + + msg.AddMarkupOrThrow($@"[bold]{FormattedMessage.EscapeText(message.SenderName)}[/bold] [color=#888888]\[{timeString}\][/color]:"); + msg.PushNewline(); + msg.AddMarkupOrThrow(FormattedMessage.EscapeText(message.Content)); + msg.PushNewline(); + msg.PushNewline(); + } + + _adminLogger.Add(LogType.Chat, + LogImpact.Low, + $"НаноЧат: {ToPrettyString(args.Actor):user} распечатал чат \"{chat.Name}\" ({chat.Id})"); + + _paper.SetContent((printed, paper), msg.ToMarkup()); + UpdateUi(ent); + } + + /// + /// Синхронизирует активных пользователей на всех серверах, удаляя тех, кто отключился. + /// + private void SyncUsers() + { + var activeUsersByServer = new Dictionary>(); + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + SyncUserToServer((uid, comp), out var server); + + if (server == null || comp.UserId == null) + continue; + + activeUsersByServer.TryAdd(server.Value.Owner, new HashSet()); + activeUsersByServer[server.Value.Owner].Add(comp.UserId.Value); + } + + foreach (var (serverUid, activeUsers) in activeUsersByServer) + { + if (!TryComp(serverUid, out var serverComp)) + continue; + + var removedUsers = serverComp.Users + .Where(id => !activeUsers.Contains(id.Id)) + .ToList(); + + foreach (var user in removedUsers) + { + serverComp.Users.Remove(user); + + foreach (var chat in serverComp.Chats.Where(chat => chat.Members.Contains(user.Id))) + chat.Members.Remove(user.Id); + } + + serverComp.Chats.RemoveAll(chat => + chat.Members.Count == 0); + + if (removedUsers.Count > 0) + Dirty(serverUid, serverComp); + } + } + + + + /// + /// Синхронизирует данные текущего картриджа с сервером. + /// + private void SyncUserToServer(Entity ent, out Entity? server) + { + var loaderUid = Transform(ent).ParentUid; + var pdaData = GetPdaInfo(loaderUid); + + if (pdaData.Id == null) + { + server = null; + return; + } + + ent.Comp.UserId = pdaData.Id.Value; + ent.Comp.PdaCardName = pdaData.Name; + + server = GetServerForCartridge(ent); + + if (server == null) + return; + + ent.Comp.ConnectedServer = server.Value.Owner; + + var contact = new NanoChatContact( + pdaData.Id.Value, + pdaData.Name ?? Loc.GetString("nano-chat-ui-chat-window-sender-unknown"), + ent.Comp.Visible, + pdaData.Job ?? Loc.GetString("nano-chat-ui-contact-job-unknown"), + pdaData.Icon); + + var index = server.Value.Comp.Users.FindIndex(u => u.Id == pdaData.Id.Value); + + if (index >= 0) + server.Value.Comp.Users[index] = contact; + else + server.Value.Comp.Users.Add(contact); + + if (!ent.Comp.Visible) + { + Dirty(server.Value); + return; + } + foreach (var otherUser in server.Value.Comp.Users.Where(otherUser => otherUser.Id != pdaData.Id.Value && otherUser.Visible)) + { + var exists = server.Value.Comp.Chats.Any(c => + c.Members.Count == 2 && + c.Members.Contains(pdaData.Id.Value) && + c.Members.Contains(otherUser.Id)); + + if (exists) + continue; + + server.Value.Comp.Chats.Add( + new NanoChatChat( + server.Value.Comp.NextChatId++, + string.Empty, + null, + [ + pdaData.Id.Value, + otherUser.Id, + ], + [])); + } + + Dirty(server.Value); + } + + /// + /// Обновляет UI всех клиентов. + /// + private void UpdateAllClients(Entity server) + { + SyncUsers(); + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + SyncUserToServer((uid, comp), out var connectedServer); + + if (connectedServer?.Owner != server.Owner) + continue; + + UpdateUi((uid, comp), connectedServer); + } + } + + /// + /// Находит активный сервер НаноЧата на той же карте. + /// + private Entity? GetServerForCartridge(EntityUid cartridgeUid) + { + var cartTrans = Transform(cartridgeUid); + + var serverQuery = EntityQueryEnumerator(); + while (serverQuery.MoveNext(out var serverUid, out var serverComponent, out var serverTransform, out var power)) + { + if (serverTransform.MapID != cartTrans.MapID || !power.Powered) + continue; + + if (serverComponent.CartridgeWhitelist != null && !_whitelist.IsValid(serverComponent.CartridgeWhitelist, cartridgeUid)) + continue; + + return (serverUid, serverComponent); + } + return null; + } + + /// + /// Извлекает данные пользователя из ID-карты, установленной в КПК. + /// + private (NetEntity? Id, string? Name, string? Job, ProtoId? Icon) GetPdaInfo(EntityUid pdaUid) + { + if (!TryComp(pdaUid, out var pda)) + return (null, null, null, null); + + var idCardUid = pda.ContainedId; + if (idCardUid == null || !TryComp(idCardUid, out var idCard)) + return (null, null, null, null); + + var fullName = string.IsNullOrEmpty(idCard.FullName) ? Loc.GetString("nano-chat-ui-chat-window-sender-unknown") : idCard.FullName; + var jobTitle = string.IsNullOrEmpty(idCard.LocalizedJobTitle) ? Loc.GetString("nano-chat-ui-contact-job-unknown") : idCard.LocalizedJobTitle; + var id = GetNetEntity(idCardUid.Value); + return (id, fullName, jobTitle, idCard.JobIcon); + } +} diff --git a/Content.Server/Imperial/ChemistryRework/ImperialEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/ImperialEntityEffectSystem.cs deleted file mode 100644 index f214b6bd7a1..00000000000 --- a/Content.Server/Imperial/ChemistryRework/ImperialEntityEffectSystem.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Content.Server.Administration.Systems; -using Content.Server.Flash; -using Content.Server.Humanoid; -using Content.Server.Mind; -using Content.Server.Popups; -using Content.Server.Singularity.EntitySystems; -using Content.Shared.Chemistry.ReactionEffects; -using Content.Shared.Damage; -using Content.Shared.EntityEffects; -using Content.Shared.Popups; -using Robust.Server.GameObjects; - -namespace Content.Server.Imperial.ChemistryRework; - -public sealed class ImperialEntityEffectSystem : EntitySystem -{ - [Dependency] private readonly PointLightSystem _pointLightSystem = default!; - [Dependency] private readonly FlashSystem _flashSystem = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly GravityWellSystem _gravityWellSystem = default!; - [Dependency] private readonly RejuvenateSystem _rejuvenateSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!; - - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnExecuteRemoveMark); - SubscribeLocalEvent>(OnExecuteResurrection); - SubscribeLocalEvent>(OnExecuteGravityReactionEffect); - SubscribeLocalEvent>(OnExecuteChangeMarkingColor); - SubscribeLocalEvent>(OnExecuteImperialFlashReactionEffect); - } - - private void OnExecuteRemoveMark(ref ExecuteEntityEffectEvent args) - { - if (!Enum.TryParse(args.Effect.MarkingCategory, out Shared.Humanoid.Markings.MarkingCategories marking)) return; - - _humanoidAppearanceSystem.RemoveMarking(args.Args.TargetEntity, marking, 0); - } - - private void OnExecuteResurrection(ref ExecuteEntityEffectEvent args) - { - _rejuvenateSystem.PerformRejuvenate(args.Args.TargetEntity); - - _damageableSystem.TryChangeDamage( - args.Args.TargetEntity, - args.Effect.PenaltyDamage, - true - ); - - if (_mindSystem.TryGetMind(args.Args.TargetEntity, out var mindUid, out var mind)) - _mindSystem.UnVisit(mindUid, mind); - - _popupSystem.PopupEntity(Loc.GetString("reagent-effect-body-resurrection"), args.Args.TargetEntity, PopupType.LargeCaution); - } - - private void OnExecuteGravityReactionEffect(ref ExecuteEntityEffectEvent args) - { - if (args.Args is not EntityEffectReagentArgs reagentArgs) return; - - var range = MathF.Min((float)(reagentArgs.Quantity * args.Effect.ImpulsePerUnit), args.Effect.MaxRange); - - _gravityWellSystem.GravPulse( - args.Args.TargetEntity, - range, - args.Effect.MinRange, - args.Effect.BaseRadialDeltaV, - args.Effect.BaseTangentialDeltaV - ); - } - - private void OnExecuteChangeMarkingColor(ref ExecuteEntityEffectEvent args) - { - if (!Enum.TryParse(args.Effect.MarkingCategory, out Shared.Humanoid.Markings.MarkingCategories marking) && args.Effect.MarkingCategory != "Skin") return; - - var color = args.Effect.InvertColor ? args.Effect.InvertMarkingColor(args.Args, marking) : args.Effect.GenerateColor(); - - if (args.Effect.MarkingCategory == "Skin") - _humanoidAppearanceSystem.SetSkinColor(args.Args.TargetEntity, color); - else - _humanoidAppearanceSystem.SetMarkingColor(args.Args.TargetEntity, marking, 0, new List { color }); - } - - private void OnExecuteImperialFlashReactionEffect(ref ExecuteEntityEffectEvent args) - { - var transform = Comp(args.Args.TargetEntity); - var uid = Spawn(args.Effect.FlashEffectPrototype, _transformSystem.GetMapCoordinates(transform)); - - var range = 1f; - - _transformSystem.AttachToGridOrMap(uid); - - if (TryComp(uid, out var pointLightComp)) - _pointLightSystem.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp); - - if (args.Args is not EntityEffectReagentArgs reagentArgs) - { - if (args.Effect.SlowOnlyTarget) - { - _flashSystem.Flash( - args.Args.TargetEntity, - null, - null, - args.Effect.MaxDuration, - 1.0f - ); - - return; - } - - _flashSystem.FlashArea( - args.Args.TargetEntity, - null, - args.Effect.MaxRange, - args.Effect.MaxDuration, - args.Effect.SlowTo - ); - - return; - } - - range = MathF.Min((float)(reagentArgs.Quantity * args.Effect.PowerPerUnit), args.Effect.MaxRange); - var duration = TimeSpan.FromSeconds(MathF.Min((float)(reagentArgs.Quantity * args.Effect.PowerPerUnit), (float)args.Effect.MaxDuration.TotalSeconds)); - - if (args.Effect.SlowOnlyTarget) - { - _flashSystem.Flash( - args.Args.TargetEntity, - null, - null, - duration, - args.Effect.SlowTo - ); - - return; - } - - _flashSystem.FlashArea( - args.Args.TargetEntity, - null, - range, - duration, - args.Effect.SlowTo - ); - } -} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/ChangeMarkingColorEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/Systems/ChangeMarkingColorEntityEffectSystem.cs new file mode 100644 index 00000000000..654bffd1d00 --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/ChangeMarkingColorEntityEffectSystem.cs @@ -0,0 +1,153 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Body; +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.EntityEffects; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Imperial.ChemistryRework; + + +public sealed partial class ChangeMarkingColorEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; + [Dependency] private readonly MarkingManager _marking = default!; + + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (!_visualBody.TryGatherMarkingsData(entity.Owner, null, out var profiles, out _, out var applied)) + return; + + if (args.Effect.MarkingCategory == ChemicalMarkingCategory.Skin) + { + ApplySkinColor(entity.Owner, profiles, args); + return; + } + + if (!TryGetLayer(args.Effect.MarkingCategory, out var layer) || applied == null) + return; + + var updated = new Dictionary, Dictionary>>(); + + foreach (var (category, organMarkings) in applied) + { + if (!organMarkings.TryGetValue(layer, out var markings) || markings.Count == 0) + continue; + + var clone = organMarkings.ToDictionary( + kvp => kvp.Key, + kvp => new List(kvp.Value)); + + if (!TryRecolorMarking(clone[layer][0], args, out var recolored)) + continue; + + clone[layer][0] = recolored; + _marking.EnsureValidColors(clone); + updated[category] = clone; + } + + if (updated.Count > 0) + _visualBody.ApplyMarkings(entity.Owner, updated); + } + + private void ApplySkinColor( + EntityUid body, + Dictionary, OrganProfileData> profiles, + EntityEffectEvent args) + { + var color = args.Effect.InvertColor + ? InvertSkinColor(profiles, args) + : GenerateColor(args.Effect.PaintingColor); + + var updated = new Dictionary, OrganProfileData>(); + + foreach (var (category, profile) in profiles) + { + var clone = profile; + clone.SkinColor = color; + updated[category] = clone; + } + + if (updated.Count > 0) + _visualBody.ApplyProfiles(body, updated); + } + + private bool TryRecolorMarking( + Marking marking, + EntityEffectEvent args, + out Marking recolored) + { + recolored = marking; + + if (!_marking.TryGetMarking(marking, out var proto)) + return false; + + if (marking.MarkingColors.Count != proto.Sprites.Count) + marking = new Marking(marking.MarkingId, proto.Sprites.Count); + + var color = args.Effect.InvertColor + ? InvertMarkingColor(marking) + : GenerateColor(args.Effect.PaintingColor); + + recolored = marking.WithColor(EnsureVisible(color)); + return true; + } + + private Color InvertMarkingColor(Marking marking) + { + if (marking.MarkingColors.Count > 0) + return Invert(marking.MarkingColors[0]); + + return GenerateColor(null); + } + + private Color InvertSkinColor( + Dictionary, OrganProfileData> profiles, + EntityEffectEvent args) + { + foreach (var profile in profiles.Values) + return Invert(profile.SkinColor); + + return GenerateColor(args.Effect.PaintingColor); + } + + private Color GenerateColor(Color? paintingColor = null) + { + if (paintingColor != null) + return EnsureVisible(paintingColor.Value); + + var r = _random.NextByte(255); + var g = _random.NextByte(255); + var b = _random.NextByte(255); + + return new Color(r, g, b); + } + + /// + /// Inverts RGB only. Inverting alpha makes markings fully transparent on the sprite layer. + /// + private static Color Invert(Color color) + { + return new Color(1f - color.R, 1f - color.G, 1f - color.B, color.A); + } + + private static Color EnsureVisible(Color color) + { + if (color.A > 0.01f) + return color; + + return color with { A = 1f }; + } + + private static bool TryGetLayer( + ChemicalMarkingCategory category, + [NotNullWhen(true)] out HumanoidVisualLayers layer) + { + return Enum.TryParse(category.ToString(), out layer); + } +} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/GravityEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/Systems/GravityEntityEffectSystem.cs new file mode 100644 index 00000000000..e84feba879f --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/GravityEntityEffectSystem.cs @@ -0,0 +1,26 @@ +using Content.Server.Singularity.EntitySystems; +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.EntityEffects; +using Robust.Shared.Physics.Components; + +namespace Content.Server.Imperial.ChemistryRework; + + +public sealed partial class GravityEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly GravityWellSystem _gravityWellSystem = default!; + + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var range = MathF.Min((float)(args.Scale * args.Effect.ImpulsePerUnit), args.Effect.MaxRange); + + _gravityWellSystem.GravPulse( + entity, + range, + args.Effect.MinRange, + args.Effect.BaseRadialDeltaV, + args.Effect.BaseTangentialDeltaV + ); + } +} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/ImperialFlashEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/Systems/ImperialFlashEntityEffectSystem.cs new file mode 100644 index 00000000000..574e7825cfa --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/ImperialFlashEntityEffectSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Flash; +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.EntityEffects; +using Robust.Server.GameObjects; + +namespace Content.Server.Imperial.ChemistryRework; + + +public sealed partial class ImperialFlashEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly PointLightSystem _pointLightSystem = default!; + [Dependency] private readonly FlashSystem _flashSystem = default!; + + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var uid = Spawn(args.Effect.FlashEffectPrototype, _transformSystem.GetMapCoordinates(entity)); + var range = 1f; + + _transformSystem.AttachToGridOrMap(uid); + + if (TryComp(uid, out var pointLightComp)) + _pointLightSystem.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp); + + if (args.Effect.SlowOnlyTarget) + { + _flashSystem.Flash( + entity, + null, + null, + args.Effect.MaxDuration, + 1.0f + ); + + return; + } + + _flashSystem.FlashArea( + entity, + null, + args.Effect.MaxRange, + args.Effect.MaxDuration, + args.Effect.SlowTo + ); + } +} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/Luminescence.cs b/Content.Server/Imperial/ChemistryRework/Systems/Luminescence.cs new file mode 100644 index 00000000000..94a5e69caa4 --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/Luminescence.cs @@ -0,0 +1,82 @@ +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.EntityEffects; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Chemistry.ReactionEffects; + + +public sealed partial class LuminescenceEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPointLightSystem _pointLight = default!; + + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var light = _pointLight.EnsureLight(entity); + var tracker = EnsureComp(entity); + tracker.Accumulated += args.Scale; + + _pointLight.SetColor(entity, GetColor(args), light); + _pointLight.SetEnergy(entity, GetEnergy(args, tracker), light); + _pointLight.SetRadius(entity, GetRadius(args, tracker), light); + _pointLight.SetEnabled(entity, true, light); + _pointLight.SetCastShadows(entity, true, light); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var tracker)) + { + if (tracker.NextCheck >= _timing.CurTime) + continue; + + tracker.Accumulated -= 1; + tracker.NextCheck = _timing.CurTime + tracker.GracePeriod; + + if (tracker.Accumulated > 0) + continue; + + _pointLight.RemoveLightDeferred(uid); + RemComp(uid); + } + } + + #region Helpers + + private Color GetColor(EntityEffectEvent args) + { + if (args.Effect.PaintingColor != null) + return args.Effect.PaintingColor.Value; + + return GenerateRandomColor(); + } + + private Color GenerateRandomColor() + { + var r = _random.NextByte(255); + var g = _random.NextByte(255); + var b = _random.NextByte(255); + + return new Color(r, g, b); + } + + private static float GetEnergy(EntityEffectEvent args, LuminescenceTrackerComponent component) + { + var effect = args.Effect; + return Math.Clamp(component.Accumulated * effect.EnergyPerUnit, effect.MinEnergy, effect.MaxEnergy); + } + + private static float GetRadius(EntityEffectEvent args, LuminescenceTrackerComponent component) + { + var effect = args.Effect; + return Math.Clamp(component.Accumulated * effect.RangePerUnit, effect.MinRange, effect.MaxRange); + } + + #endregion +} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/RemoveMarkEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/Systems/RemoveMarkEntityEffectSystem.cs new file mode 100644 index 00000000000..d7e81464b71 --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/RemoveMarkEntityEffectSystem.cs @@ -0,0 +1,41 @@ +using System.Linq; +using Content.Shared.Body; +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.EntityEffects; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.ChemistryRework; + +public sealed partial class RemoveMarkEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (!_visualBody.TryGatherMarkingsData(entity.Owner, null, out _, out _, out var applied)) + return; + + if (applied == null) + return; + + var updated = new Dictionary, Dictionary>>(); + + foreach (var (category, organMarkings) in applied) + { + if (!organMarkings.TryGetValue(args.Effect.MarkingCategory, out var markings)) + continue; + + var clone = organMarkings.ToDictionary( + kvp => kvp.Key, + kvp => new List(kvp.Value)); + + clone[args.Effect.MarkingCategory].RemoveAt(0); + updated[category] = clone; + } + + if (updated.Count > 0) + _visualBody.ApplyMarkings(entity.Owner, updated); + } +} diff --git a/Content.Server/Imperial/ChemistryRework/Systems/ResurrectionEntityEffectSystem.cs b/Content.Server/Imperial/ChemistryRework/Systems/ResurrectionEntityEffectSystem.cs new file mode 100644 index 00000000000..ce00f9dd284 --- /dev/null +++ b/Content.Server/Imperial/ChemistryRework/Systems/ResurrectionEntityEffectSystem.cs @@ -0,0 +1,36 @@ +using Content.Server.Mind; +using Content.Server.Popups; +using Content.Shared.Administration.Systems; +using Content.Shared.Chemistry.ReactionEffects; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.EntityEffects; +using Content.Shared.Popups; + +namespace Content.Server.Imperial.ChemistryRework; + + +public sealed partial class ResurrectionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly RejuvenateSystem _rejuvenateSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; + + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _rejuvenateSystem.PerformRejuvenate(entity); + + _damageableSystem.TryChangeDamage( + entity.Owner, + args.Effect.PenaltyDamage, + true + ); + + if (_mindSystem.TryGetMind(entity, out var mindUid, out var mind)) + _mindSystem.UnVisit(mindUid, mind); + + _popupSystem.PopupEntity(Loc.GetString("reagent-effect-body-resurrection"), entity, PopupType.LargeCaution); + } +} diff --git a/Content.Server/Imperial/CrewSkills/CrewSkillsSystem.cs b/Content.Server/Imperial/CrewSkills/CrewSkillsSystem.cs new file mode 100644 index 00000000000..f08e5a6b1b8 --- /dev/null +++ b/Content.Server/Imperial/CrewSkills/CrewSkillsSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Imperial.CrewSkills; + +namespace Content.Server.Imperial.CrewSkills; + + +public sealed partial class CrewSkillsSystem : SharedCrewSkillsSystem; diff --git a/Content.Server/Imperial/Crook/Systems/MetalDetectorSystem.cs b/Content.Server/Imperial/Crook/Systems/MetalDetectorSystem.cs index 10b692e73dd..eb6d9560a4a 100644 --- a/Content.Server/Imperial/Crook/Systems/MetalDetectorSystem.cs +++ b/Content.Server/Imperial/Crook/Systems/MetalDetectorSystem.cs @@ -25,6 +25,9 @@ using Robust.Shared.Audio; using Robust.Server.Containers; using Robust.Shared.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; +using Content.Shared.Power.Components; namespace Content.Server.Imperial.Crook.Systems { @@ -369,7 +372,7 @@ private void DischargeBatteries(EntityUid entity, MetalDetectorComponent detecto private void DischargeSingleItem(EntityUid item) { if (TryComp(item, out var battery)) - _battery.SetCharge(item, 0, battery); + _battery.SetCharge((item, battery), 0); if (TryComp(item, out var containerManager)) { diff --git a/Content.Server/Imperial/Damage/DamageModifierStorageSystem.cs b/Content.Server/Imperial/Damage/DamageModifierStorageSystem.cs index a02077641de..bbd219d9433 100644 --- a/Content.Server/Imperial/Damage/DamageModifierStorageSystem.cs +++ b/Content.Server/Imperial/Damage/DamageModifierStorageSystem.cs @@ -1,20 +1,16 @@ -using System.Collections.Generic; using Content.Shared.Imperial.Damage.Components; using Content.Shared.Storage; using Content.Shared.Stacks; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; using Content.Shared.FixedPoint; using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Prototypes; +using Content.Shared.Damage.Prototypes; namespace Content.Server.Imperial.Damage { public sealed class DamageModifierStorageSystem : EntitySystem { [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { @@ -60,7 +56,7 @@ private void OnGetMeleeDamage(EntityUid uid, DamageModifierStorageComponent comp if (totalCount == FixedPoint2.Zero) return; - var newDamageDict = new Dictionary(args.Damage.DamageDict); + var newDamageDict = new Dictionary, FixedPoint2>(args.Damage.DamageDict); var damageIncrease = component.DamageIncrease * totalCount; if (newDamageDict.TryGetValue("Blunt", out var currentBlunt)) diff --git a/Content.Server/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteSystem.cs b/Content.Server/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteSystem.cs new file mode 100644 index 00000000000..3cc10b89753 --- /dev/null +++ b/Content.Server/Imperial/DeimonFly/BuckshotRoulette/BuckshotRouletteSystem.cs @@ -0,0 +1,582 @@ +using System.Linq; +using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; +using Content.Server.Storage.EntitySystems; +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared.Audio; +using Content.Shared.Chat; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Examine; +using Content.Shared.Imperial.DeimonFly.BuckshotRoulette; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; +using Content.Shared.Nutrition; +using Content.Shared.Popups; +using Content.Shared.Projectiles; +using Content.Shared.Storage; +using Content.Shared.Tools.Components; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.Wieldable; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.DeimonFly.BuckshotRoulette; + +/// +/// Управляет стрельбой, состоянием ствола, загрузкой кейсов и одноразовыми предметами Buckshot Roulette. +/// +public sealed class BuckshotRouletteSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly GunSystem _gun = default!; + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly StorageSystem _storage = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAmmoCaseMapInit); + SubscribeLocalEvent(OnAmmoCaseAfterInteract); + SubscribeLocalEvent(OnItemCaseMapInit); + SubscribeLocalEvent(OnShotgunExamined); + SubscribeLocalEvent>(OnShotgunGetVerbs); + SubscribeLocalEvent(OnShotgunFired); + SubscribeLocalEvent(OnShotgunAmmoShot); + SubscribeLocalEvent(OnShotgunDropped); + SubscribeLocalEvent(OnShotgunUseInHand, + before: [typeof(SharedWieldableSystem), typeof(SharedGunSystem)]); + SubscribeLocalEvent(OnBeerIngested); + SubscribeLocalEvent(OnToolAfterInteract); + SubscribeLocalEvent(OnToolUseInHand, + before: [typeof(SharedWieldableSystem), typeof(SharedGunSystem)]); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var shotgun)) + { + if (shotgun.BarrelVisualState != BuckshotRouletteBarrelVisualState.Restoring || + shotgun.BarrelRestoreAt is not { } restoreAt || + now < restoreAt) + { + continue; + } + + shotgun.BarrelRestoreAt = null; + shotgun.BarrelVisualState = BuckshotRouletteBarrelVisualState.Intact; + Dirty(uid, shotgun); + } + } + + private void OnShotgunExamined(Entity shotgun, ref ExaminedEvent args) + { + var mode = GetFireModeLocString(shotgun.Comp.FireMode); + args.PushMarkup(Loc.GetString("buckshot-roulette-fire-mode-examine", ("mode", mode))); + } + + private string GetFireModeLocString(BuckshotRouletteFireMode mode) + { + return Loc.GetString(mode == BuckshotRouletteFireMode.Self + ? "buckshot-roulette-fire-mode-self" + : "buckshot-roulette-fire-mode-target"); + } + + private void OnShotgunGetVerbs( + Entity shotgun, + ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + var user = args.User; + args.Verbs.Add(new AlternativeVerb + { + Text = Loc.GetString("buckshot-roulette-fire-mode-toggle-verb"), + Priority = 1, + Act = () => ToggleFireMode(shotgun, user), + }); + } + + private void ToggleFireMode(Entity shotgun, EntityUid user) + { + shotgun.Comp.FireMode = shotgun.Comp.FireMode == BuckshotRouletteFireMode.Target + ? BuckshotRouletteFireMode.Self + : BuckshotRouletteFireMode.Target; + DirtyField(shotgun.AsNullable(), nameof(BuckshotRouletteShotgunComponent.FireMode)); + + var mode = GetFireModeLocString(shotgun.Comp.FireMode); + _popup.PopupClient(Loc.GetString("buckshot-roulette-fire-mode-changed", ("mode", mode)), user, user); + } + + private void OnShotgunFired(Entity shotgun, ref GunShotEvent args) + { + if (shotgun.Comp.FireMode != BuckshotRouletteFireMode.Self || Deleted(args.User) || + !TryComp(shotgun, out var gun)) + { + return; + } + + var doubleDamage = shotgun.Comp.DoubleNextShot; + + // Усиление необходимо погасить до нанесения урона: переход стрелка в крит может + // синхронно выбросить дробовик и запустить обработчик восстановления ствола. + if (doubleDamage) + ConsumeSawEffect(shotgun); + + foreach (var (shellUid, _) in args.Ammo) + { + if (shellUid is not { } shell || !TryComp(shell, out var rouletteShell)) + continue; + + if (rouletteShell.Live) + { + _audio.PlayPredicted(gun.SoundGunshotModified, shotgun, args.User); + var damage = new DamageSpecifier(shotgun.Comp.SelfDamage); + if (doubleDamage) + damage *= shotgun.Comp.SawDamageMultiplier; + + _damageable.TryChangeDamage( + args.User, + damage, + ignoreResistances: true, + origin: shotgun.Owner); + } + else + { + _audio.PlayPredicted(gun.SoundEmpty, shotgun, args.User); + } + + // При самовыстреле штатное создание снаряда отменено, поэтому использованный патрон удаляется здесь. + QueueDel(shell); + } + } + + private void OnShotgunAmmoShot(Entity shotgun, ref AmmoShotEvent args) + { + if (shotgun.Comp.FireMode != BuckshotRouletteFireMode.Target || !shotgun.Comp.DoubleNextShot) + return; + + // У холостого патрона список снарядов пуст: усиление просто расходуется без лишней обработки нулевого урона. + foreach (var projectileUid in args.FiredProjectiles) + { + if (!TryComp(projectileUid, out var projectile)) + continue; + + projectile.Damage *= shotgun.Comp.SawDamageMultiplier; + Dirty(projectileUid, projectile); + } + + ConsumeSawEffect(shotgun); + } + + private void OnShotgunDropped(Entity shotgun, ref DroppedEvent args) + { + TryStartBarrelRestoration(shotgun); + } + + private void TryStartBarrelRestoration(Entity shotgun) + { + if (!shotgun.Comp.BarrelRestorationPending || + shotgun.Comp.BarrelVisualState != BuckshotRouletteBarrelVisualState.Sawed) + { + return; + } + + shotgun.Comp.BarrelRestorationPending = false; + shotgun.Comp.BarrelVisualState = BuckshotRouletteBarrelVisualState.Restoring; + shotgun.Comp.BarrelRestoreAt = _timing.CurTime + shotgun.Comp.BarrelRestoreDuration; + Dirty(shotgun); + } + + private void ConsumeSawEffect(Entity shotgun) + { + shotgun.Comp.DoubleNextShot = false; + shotgun.Comp.BarrelRestorationPending = true; + + // Обычно восстановление запускает DroppedEvent. Если оружие уже успели выбить + // до GunShotEvent, оно больше не находится в контейнере рук и ждать нового события нельзя. + if (!_containers.IsEntityInContainer(shotgun)) + { + TryStartBarrelRestoration(shotgun); + return; + } + + Dirty(shotgun); + } + + private void OnBeerIngested(Entity beer, ref IngestedEvent args) + { + if (beer.Comp.RewardGranted || + !_solution.ResolveSolution(beer.Owner, beer.Comp.SolutionName, ref beer.Comp.Solution, out var solution) || + solution.Volume > 0) + { + return; + } + + beer.Comp.RewardGranted = true; + var permit = EnsureComp(args.Target); + permit.Charges++; + _popup.PopupClient(Loc.GetString("buckshot-roulette-beer-permit-granted"), args.Target, args.Target); + } + + private void OnShotgunUseInHand(Entity shotgun, ref UseInHandEvent args) + { + if (args.Handled || !TryComp(args.User, out var permit)) + return; + + // RemCompDeferred удаляет исчерпанное разрешение в конце тика; эта проверка + // не даёт повторному вводу за тот же тик извлечь дополнительный патрон. + if (permit.Charges <= 0) + { + RemCompDeferred(args.User); + return; + } + + args.Handled = true; + args.ApplyDelay = false; + + if (!TryComp(shotgun, out var provider) || provider.Entities.Count == 0) + { + _popup.PopupClient(Loc.GetString("buckshot-roulette-beer-pull-empty"), args.User, args.User); + return; + } + + var extracted = new List<(EntityUid? Entity, IShootable Shootable)>(); + var takeAmmo = new TakeAmmoEvent(1, extracted, Transform(args.User).Coordinates, args.User); + RaiseLocalEvent(shotgun.Owner, takeAmmo); + + if (extracted.Count == 0 || extracted[0].Entity is not { } shell) + return; + + _transform.SetCoordinates(shell, Transform(args.User).Coordinates); + var ejectSound = TryComp(shell, out var cartridge) + ? cartridge.EjectSound ?? shotgun.Comp.ShellEjectSound + : shotgun.Comp.ShellEjectSound; + + // Это серверное взаимодействие не предсказывается клиентом, + // поэтому звук принудительно отправляется всем поблизости. + _audio.PlayPvs( + ejectSound, + shotgun, + AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f)); + + var cycled = new GunCycledEvent(); + RaiseLocalEvent(shotgun.Owner, ref cycled); + + permit.Charges--; + if (permit.Charges <= 0) + RemCompDeferred(args.User); + + _popup.PopupClient(Loc.GetString("buckshot-roulette-beer-pull-success"), args.User, args.User); + } + + private void OnAmmoCaseMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var storage)) + return; + + var count = _random.Next(ent.Comp.MinimumShells, ent.Comp.MaximumShells + 1); + var liveCount = _random.Next(1, count); + + var liveShells = Enumerable.Repeat(ent.Comp.LiveShell, liveCount); + var blankShells = Enumerable.Repeat(ent.Comp.BlankShell, count - liveCount); + var shells = liveShells.Concat(blankShells).ToList(); + _random.Shuffle(shells); + + var grid = storage.Grid.GetBoundingBox(); + var positions = new List(storage.Grid.GetArea()); + for (var y = grid.Bottom; y <= grid.Top; y++) + { + for (var x = grid.Left; x <= grid.Right; x++) + { + if (storage.Grid.Contains(x, y)) + positions.Add(new Vector2i(x, y)); + } + } + _random.Shuffle(positions); + + for (var i = 0; i < shells.Count; i++) + { + var shell = Spawn(shells[i], Transform(ent).Coordinates); + if (!TryComp(shell, out var item) || + !_storage.InsertAt((ent.Owner, storage), (shell, item), + new ItemStorageLocation(Angle.Zero, positions[i]), out _, playSound: false)) + { + QueueDel(shell); + } + } + } + + private void OnItemCaseMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var storage) || ent.Comp.Items.Count == 0) + return; + + var minimum = Math.Max(1, ent.Comp.MinimumItems); + var maximum = Math.Max(minimum, ent.Comp.MaximumItems); + var count = _random.Next(minimum, maximum + 1); + + for (var i = 0; i < count; i++) + { + var item = Spawn(_random.Pick(ent.Comp.Items), Transform(ent).Coordinates); + if (!_storage.Insert(ent.Owner, item, out _, storageComp: storage, playSound: false)) + QueueDel(item); + } + } + + private void OnAmmoCaseAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { } target || + !TryComp(ent, out var storage) || + !TryComp(target, out _) || + !TryComp(target, out var provider)) + { + return; + } + + var shells = storage.Container.ContainedEntities + .Where(HasComp) + .ToList(); + _random.Shuffle(shells); + + var loaded = 0; + foreach (var shell in shells) + { + if (!_gun.TryBallisticInsert((target, provider), shell, args.User, suppressInsertionSound: true)) + continue; + + loaded++; + } + + if (loaded == 0) + return; + + // Перенос патронов выполняется только на сервере, поэтому один раз отправляем штатный + // звук зарядки всем поблизости, включая игрока, использовавшего кейс. + _audio.PlayPvs(provider.SoundInsert, target); + args.Handled = true; + } + + private void OnToolAfterInteract(Entity tool, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { } target || + !TryComp(target, out var shotgun) || + !TryComp(target, out var provider)) + { + return; + } + + switch (tool.Comp.Effect) + { + case BuckshotRouletteToolEffect.BurnerPhone: + UseBurnerPhone(args.User, provider); + AnnounceUse(args.User, "buckshot-roulette-announcement-phone"); + break; + case BuckshotRouletteToolEffect.MagnifyingGlass: + UseMagnifyingGlass(args.User, provider); + AnnounceUse(args.User, "buckshot-roulette-announcement-magnifier"); + break; + case BuckshotRouletteToolEffect.Inverter: + if (!TryUseInverter(args.User, target, shotgun, provider)) + return; + AnnounceUse(args.User, "buckshot-roulette-announcement-inverter"); + break; + case BuckshotRouletteToolEffect.HandSaw: + if (!TryUseHandSaw(tool.Owner, args.User, target, shotgun)) + return; + AnnounceUse(args.User, "buckshot-roulette-announcement-saw"); + break; + default: + return; + } + + args.Handled = true; + QueueDel(tool); + } + + private void OnToolUseInHand(Entity tool, ref UseInHandEvent args) + { + var loc = tool.Comp.Effect switch + { + BuckshotRouletteToolEffect.Jammer => "buckshot-roulette-announcement-jammer", + BuckshotRouletteToolEffect.Remote => "buckshot-roulette-announcement-remote", + _ => null, + }; + + if (loc == null) + return; + + AnnounceUse(args.User, loc); + args.Handled = true; + QueueDel(tool); + } + + private void UseBurnerPhone(EntityUid user, BallisticAmmoProviderComponent provider) + { + if (provider.Entities.Count <= 2) + { + SendPrivate(user, Loc.GetString("buckshot-roulette-phone-no-information")); + return; + } + + var offset = _random.Next(1, provider.Entities.Count); + var shell = provider.Entities[provider.Entities.Count - 1 - offset]; + if (!TryGetRouletteShell(user, shell, out var rouletteShell)) + return; + + var shellName = Loc.GetString(rouletteShell.Live + ? "buckshot-roulette-shell-type-live" + : "buckshot-roulette-shell-type-blank"); + SendPrivate(user, Loc.GetString("buckshot-roulette-phone-result", + ("position", offset + 1), + ("shell", shellName))); + } + + private void UseMagnifyingGlass(EntityUid user, BallisticAmmoProviderComponent provider) + { + if (provider.Entities.Count == 0) + { + SendPrivate(user, Loc.GetString("buckshot-roulette-magnifier-empty")); + return; + } + + var shell = provider.Entities[^1]; + if (!TryGetRouletteShell(user, shell, out var rouletteShell)) + return; + + SendPrivate(user, Loc.GetString(rouletteShell.Live + ? "buckshot-roulette-magnifier-live" + : "buckshot-roulette-magnifier-blank")); + } + + private bool TryUseInverter( + EntityUid user, + EntityUid shotgunUid, + BuckshotRouletteShotgunComponent shotgun, + BallisticAmmoProviderComponent provider) + { + if (provider.Entities.Count == 0) + { + SendPrivate(user, Loc.GetString("buckshot-roulette-inverter-empty")); + return false; + } + + var expectedCurrent = provider.Entities[^1]; + if (!TryGetRouletteShell(user, expectedCurrent, out var rouletteShell)) + return false; + + var extracted = new List<(EntityUid? Entity, IShootable Shootable)>(); + var takeAmmo = new TakeAmmoEvent(1, extracted, Transform(shotgunUid).Coordinates, user); + RaiseLocalEvent(shotgunUid, takeAmmo); + + if (extracted.Count == 0 || extracted[0].Entity is not { } current) + return false; + + if (current != expectedCurrent) + { + Log.Error($"Expected to extract {ToPrettyString(expectedCurrent)}, " + + $"but got {ToPrettyString(current)} from {ToPrettyString(shotgunUid)}."); + _gun.TryBallisticInsert((shotgunUid, provider), current, user, suppressInsertionSound: true); + return false; + } + + var replacementPrototype = rouletteShell.Live ? shotgun.BlankShell : shotgun.LiveShell; + var replacement = Spawn(replacementPrototype, Transform(shotgunUid).Coordinates); + if (_gun.TryBallisticInsert((shotgunUid, provider), replacement, user, suppressInsertionSound: true)) + { + QueueDel(current); + return true; + } + + QueueDel(replacement); + + // При ошибке конфигурации не теряем исходный патрон и не расходуем инвертор. + if (!_gun.TryBallisticInsert((shotgunUid, provider), current, user, suppressInsertionSound: true)) + { + Log.Error($"Failed to restore {ToPrettyString(current)} into " + + $"{ToPrettyString(shotgunUid)} after inverter failure."); + } + + return false; + } + + private bool TryGetRouletteShell( + EntityUid user, + EntityUid shell, + out BuckshotRouletteShellComponent component) + { + if (TryComp(shell, out var shellComponent)) + { + component = shellComponent; + return true; + } + + component = default!; + Log.Error($"Buckshot Roulette ammo provider contains incompatible shell {ToPrettyString(shell)}."); + SendPrivate(user, Loc.GetString("buckshot-roulette-invalid-shell")); + return false; + } + + private bool TryUseHandSaw( + EntityUid sawUid, + EntityUid user, + EntityUid shotgunUid, + BuckshotRouletteShotgunComponent shotgun) + { + if (shotgun.DoubleNextShot || shotgun.BarrelVisualState != BuckshotRouletteBarrelVisualState.Intact) + { + SendPrivate(user, Loc.GetString("buckshot-roulette-saw-already-active")); + return false; + } + + shotgun.DoubleNextShot = true; + shotgun.BarrelVisualState = BuckshotRouletteBarrelVisualState.Sawed; + Dirty(shotgunUid, shotgun); + + // Обработчик работает только на сервере, поэтому звук отправляется всем игрокам поблизости напрямую. + if (TryComp(sawUid, out var tool) && tool.UseSound != null) + _audio.PlayPvs(tool.UseSound, sawUid); + + return true; + } + + private void AnnounceUse(EntityUid user, string loc) + { + _chat.TrySendInGameICMessage(user, Loc.GetString(loc), InGameICChatType.Emote, false, + checkRadioPrefix: false, ignoreActionBlocker: true); + } + + private void SendPrivate(EntityUid user, string message) + { + if (!_players.TryGetSessionByEntity(user, out var session)) + return; + + _chatManager.ChatMessageToOne(ChatChannel.Local, message, message, EntityUid.Invalid, false, session.Channel); + } +} diff --git a/Content.Server/Imperial/DeimonFly/Roulette/RouletteSelfShotSystem.cs b/Content.Server/Imperial/DeimonFly/Roulette/RouletteSelfShotSystem.cs new file mode 100644 index 00000000000..f9d0c917368 --- /dev/null +++ b/Content.Server/Imperial/DeimonFly/Roulette/RouletteSelfShotSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.Imperial.DeimonFly.BuckshotRoulette; +using Content.Shared.Imperial.DeimonFly.RussianRoulette; +using Content.Shared.Weapons.Ranged.Events; + +namespace Content.Server.Imperial.DeimonFly.Roulette; + +/// +/// Отменяет создание внешнего снаряда для оружия, которое должно выстрелить в своего пользователя. +/// Последствия подтверждённого выстрела обрабатывают системы конкретного оружия. +/// +public sealed class RouletteSelfShotSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + // MetaDataComponent присутствует у любой сущности, поэтому правило действует + // даже на нестандартного стрелка без MobStateComponent или DamageableComponent. + SubscribeLocalEvent(OnBeforeGunShot); + } + + private void OnBeforeGunShot(Entity shooter, ref SelfBeforeGunShotEvent args) + { + if (HasComp(args.Gun) || + TryComp(args.Gun, out var shotgun) && + shotgun.FireMode == BuckshotRouletteFireMode.Self) + { + args.Cancel(); + } + } +} diff --git a/Content.Server/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunSystem.cs b/Content.Server/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunSystem.cs new file mode 100644 index 00000000000..da7322b16fa --- /dev/null +++ b/Content.Server/Imperial/DeimonFly/RussianRoulette/RussianRouletteGunSystem.cs @@ -0,0 +1,90 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Imperial.DeimonFly.RussianRoulette; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Traits.Assorted; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Random; + +namespace Content.Server.Imperial.DeimonFly.RussianRoulette; + +/// +/// Применяет эффект револьвера для русской рулетки к сущности, совершившей выстрел. +/// +public sealed class RussianRouletteGunSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnGunShot); + } + + private void OnMapInit(Entity gun, ref MapInitEvent args) + { + if (!TryComp(gun, out var revolver) || revolver.Capacity <= 0) + return; + + // Клиент и сервер создают унаследованный револьвер пустым. Здесь сервер помещает + // единственный настоящий патрон в случайную камору и синхронизирует барабан целиком. + Array.Fill(revolver.Chambers, null); + var chamber = _random.Next(revolver.Capacity); + var cartridge = Spawn(gun.Comp.Cartridge, Transform(gun).Coordinates); + + revolver.AmmoSlots[chamber] = cartridge; + revolver.Chambers[chamber] = true; + _containers.Insert(cartridge, revolver.AmmoContainer); + Dirty(gun.Owner, revolver); + } + + private void OnGunShot(Entity gun, ref GunShotEvent args) + { + // GunShotEvent вызывается только после подтверждения выстрела и расходования боеприпаса. + if (Deleted(args.User)) + return; + + // Сервер отменяет полёт внешнего снаряда, поэтому сам воспроизводит звук для остальных игроков + // и удаляет уже созданную штатной системой безвредную пулю. + if (TryComp(gun, out var gunComponent)) + _audio.PlayPredicted(gunComponent.SoundGunshotModified, gun, args.User); + + foreach (var (projectile, _) in args.Ammo) + { + if (projectile is { } projectileUid) + QueueDel(projectileUid); + } + + // Добавляем компонент до смертельного урона, чтобы системы воскрешения увидели его на трупе. + // Если выстрел не убил сущность, временную метку ниже обязательно снимаем. + var addedUnrevivable = gun.Comp.PreventRevival && !HasComp(args.User); + if (addedUnrevivable) + EnsureComp(args.User); + + // Копируем значение из прототипа, поскольку события урона могут изменять DamageSpecifier. + var damage = new DamageSpecifier(gun.Comp.Damage); + _damageable.TryChangeDamage( + args.User, + damage, + ignoreResistances: true, + origin: gun.Owner); + + if (addedUnrevivable && + !Deleted(args.User) && + (!TryComp(args.User, out var mobState) || !_mobState.IsDead(args.User, mobState))) + { + RemComp(args.User); + } + } +} diff --git a/Content.Server/Imperial/DeimonFly/Storage/PunishOnStorageTakeSystem.cs b/Content.Server/Imperial/DeimonFly/Storage/PunishOnStorageTakeSystem.cs new file mode 100644 index 00000000000..f2899f1698a --- /dev/null +++ b/Content.Server/Imperial/DeimonFly/Storage/PunishOnStorageTakeSystem.cs @@ -0,0 +1,67 @@ +using System; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Imperial.DeimonFly.Storage; +using Content.Shared.Popups; +using Content.Shared.Storage; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.DeimonFly.Storage; + +/// +/// Система, которая наказывает игрока при попытке забрать определённые предметы из стораджа. +/// +public sealed class PunishOnStorageTakeSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + SubscribeAllEvent(OnStorageInteract); + } + + private void OnStorageInteract(StorageInteractWithItemEvent msg, EntitySessionEventArgs args) + { + // Должен быть игрок, который кликнул по предмету, иначе не срабатываем. + if (args.SenderSession?.AttachedEntity is not { } userUid || Deleted(userUid)) + return; + + if (!TryGetEntity(msg.StorageUid, out EntityUid? storageUid) || storageUid is null) + return; + + if (!TryComp(storageUid, out PunishOnStorageTakeComponent? comp)) + return; + + var now = _timing.CurTime; + if (comp.Cooldown > TimeSpan.Zero && now < comp.LastPunish + comp.Cooldown) + return; + + if (!TryGetEntity(msg.InteractedItemUid, out EntityUid? itemUid) || itemUid is null) + return; + + if (comp.TargetItems.Count > 0) + { + if (!TryComp(itemUid, out var meta) || + meta.EntityPrototype?.ID is not { } protoId || + !comp.TargetItems.Contains(protoId)) + { + return; + } + } + + var changed = _damageable.TryChangeDamage(userUid, comp.Damage, ignoreResistances: true, interruptsDoAfters: false); + + if (comp.Sound != null) + _audio.PlayPvs(comp.Sound, storageUid.Value); + + if (comp.Popup is { } popup) + _popup.PopupEntity(Loc.GetString(popup), storageUid.Value, userUid); + + comp.LastPunish = now; + Dirty(storageUid.Value, comp); + } +} diff --git a/Content.Server/Imperial/Effects/BloodTrail/BloodTrailSystem.cs b/Content.Server/Imperial/Effects/BloodTrail/BloodTrailSystem.cs index 1819a873fc5..b539a18a22a 100644 --- a/Content.Server/Imperial/Effects/BloodTrail/BloodTrailSystem.cs +++ b/Content.Server/Imperial/Effects/BloodTrail/BloodTrailSystem.cs @@ -1,6 +1,8 @@ using Content.Server.Decals; using Content.Shared.Body.Components; using Content.Shared.Damage; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage.Systems; using Content.Shared.Decals; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Components; @@ -20,6 +22,7 @@ public sealed class BloodTrailSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IMapManager _map = default!; [Dependency] private readonly DecalSystem _decal = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; private const float MinDistanceBetweenDecals = 0.3f; private readonly List _recentDecalPositions = new(); @@ -161,11 +164,13 @@ private bool IsTooCloseToRecentDecals(Vector2 position) private Color GetBloodColor(EntityUid victim) { - if (TryComp(victim, out var bloodstream) && - _prototype.TryIndex(bloodstream.BloodReagent, out var reagent)) - return reagent.SubstanceColor; + if (!TryComp(victim, out var bloodstream)) + return Color.DarkRed; - return Color.DarkRed; + if (!_solutionContainerSystem.TryGetSolution(victim, bloodstream.BloodSolutionName, out var _, out var solution)) + return Color.DarkRed; + + return solution.GetColor(_prototype); } private (Vector2 position, Angle rotation) CalculateDecalPositionAndRotation(Vector2 victimWorldPos, EntityUid? damageSource, float spread) diff --git a/Content.Server/Imperial/ElectroMouse/ElectroMouseSystem.cs b/Content.Server/Imperial/ElectroMouse/ElectroMouseSystem.cs index 4d36ed65b88..a08df5e838b 100644 --- a/Content.Server/Imperial/ElectroMouse/ElectroMouseSystem.cs +++ b/Content.Server/Imperial/ElectroMouse/ElectroMouseSystem.cs @@ -39,25 +39,13 @@ using CollisionGroup = Content.Shared.Physics.CollisionGroup; using Content.Server.Chat.Systems; using Content.Shared.Light.Components; +using Content.Shared.Damage.Components; +using Content.Shared.Power.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Body; namespace Content.Server.Imperial.ElectroMouse.EntitySystems; -/* -          _,.. -──- 、, -        , '"          `ヽ. -       //¨7__  /     i  _厂廴 -      / ̄( ノ__/ /{    } 「 (_冫} -    / ̄l_//  /-|  ,!  ム  ̄|_「 \__ -  . イ    ,  /!_∠_ | / /_⊥_,ノ ハ   イ -   / / /  〃ん心 レ'|/ f,心 Y i \__>  -  ∠イ  /   ト弋_ツ     弋_ツ i  |  | \ -  _/ _ノ| ,i ⊂⊃   '   ⊂⊃ ./  !、_ン -   ̄  ∨| ,小、  ` ‐ '    /|/| / -          Y |ヘ> 、 _ ,. イレ|  レ' -       r'.|  |;;;入゙亠―亠' );;;;;!  |、 -      ,ノ:,:|. !|く __ ̄ ̄ ̄__У ノ|:,:,ヽ -     (:.:.:.:ム人!ヘ   ` ´    厂|ノ:.:.:丿 -*/ public sealed partial class ElectroMouseSystem : EntitySystem @@ -182,7 +170,7 @@ public override void Update(float frameTime) if (TryComp(uid, out var damageableComponent)) { var total = FixedPoint2.Zero; - foreach (var value in damageableComponent.Damage.DamageDict.Values) + foreach (var value in _damageable.GetAllDamage(uid).DamageDict.Values) { total += value; } @@ -317,7 +305,7 @@ private void OnEmp(EntityUid uid, ElectroMouseComponent component, ElectroMouseE _popup.PopupEntity("Недостаточно энергии", uid, uid); return; } - _empSystem.EmpPulse(coords, component.EmpRadius, 10000, 120); + _empSystem.EmpPulse(coords, component.EmpRadius, 10000, TimeSpan.FromSeconds(120)); AddEnergy(uid, component, -20); @@ -405,7 +393,7 @@ private void OnInteract(EntityUid uid, ElectroMouseComponent component, UserActi if (HasComp(target)) return; - if (HasComp(target) || HasComp(target) || HasComp(target) || HasComp(target)) + if (HasComp(target) || HasComp(target) || HasComp(target)) { args.Handled = true; @@ -500,13 +488,7 @@ private void AddEnergy(EntityUid uid, ElectroMouseComponent component, FixedPoin private void BeginHarvestDoAfter(EntityUid uid, EntityUid target, ElectroMouseComponent comp) { - if (TryComp(target, out var hitprov) && hitprov.Shots == 0) - return; - if (TryComp(target, out var projprov) && projprov.Shots == 0) - return; - if (HasComp(target) && !comp.CanBattery) - return; - if (HasComp(target) && !comp.CanBattery) + if (TryComp(target, out var hitprov) && hitprov.Shots == 0) return; if (HasComp(target)) @@ -563,13 +545,13 @@ private bool TryDrainPower(EntityUid uid, ElectroMouseComponent component, Entit if (!TryComp(target, out var targetBattery) || !TryComp(target, out var pnb)) return false; - if (targetBattery.CurrentCharge <= targetBattery.MaxCharge / 100 * 20) + if (targetBattery.LastCharge <= targetBattery.MaxCharge / 100 * 20) { _popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium); return false; } - var available = targetBattery.CurrentCharge; + var available = targetBattery.LastCharge; int required; if (HasComp(target)) @@ -581,7 +563,7 @@ private bool TryDrainPower(EntityUid uid, ElectroMouseComponent component, Entit else required = 5000; - _battery.UseCharge(target, required * 200, targetBattery); + _battery.TryUseCharge((target, targetBattery), required * 200); Dirty(target, targetBattery); @@ -618,13 +600,7 @@ private void OnHarvest(EntityUid uid, ElectroMouseComponent component, HarvestEv component.Harvested.Add(target); AddEnergy(uid, component, 5); } - else if (TryComp(target, out var hitprov)) - { - AddEnergy(uid, component, hitprov.Shots); - hitprov.Shots = 0; - Dirty(target, hitprov); - } - else if (TryComp(target, out var projprov)) + else if (TryComp(target, out var projprov)) { AddEnergy(uid, component, projprov.Shots); projprov.Shots = 0; @@ -697,15 +673,15 @@ private void OnHeal(EntityUid uid, ElectroMouseComponent component, ElectroMouse if (TryComp(uid, out var damagecomp) && component.Energy >= 30) { args.Handled = true; - var result = damagecomp.Damage.DamageDict.OrderByDescending(z => z.Value).ToDictionary(a => a, s => s).First().Key.Key.ToString(); + var result = _damageable.GetAllDamage(uid).DamageDict.OrderByDescending(z => z.Value).ToDictionary(a => a, s => s).First().Key.Key.ToString(); if (component.HealingStrength == 10) { - if (result != null && damagecomp.Damage.DamageDict[result] != 0) + if (result != null && _damageable.GetAllDamage(uid).DamageDict[result] != 0) { AddEnergy(uid, component, -30); var newdamage = component.HealingStrength; - if (damagecomp.Damage.DamageDict[result] <= newdamage) - newdamage = (int)damagecomp.Damage.DamageDict[result]; + if (_damageable.GetAllDamage(uid).DamageDict[result] <= newdamage) + newdamage = (int)_damageable.GetAllDamage(uid).DamageDict[result]; DamageSpecifier damage = new() { DamageDict = new() @@ -713,7 +689,7 @@ private void OnHeal(EntityUid uid, ElectroMouseComponent component, ElectroMouse { result, component.HealingStrength * -1 } } }; - _damageable.TryChangeDamage(uid, damage, false, true, damagecomp, origin: uid); + _damageable.TryChangeDamage(uid, damage, false, true, origin: uid); Dirty(uid, damagecomp); } else @@ -721,12 +697,12 @@ private void OnHeal(EntityUid uid, ElectroMouseComponent component, ElectroMouse } else { - if (result != null && damagecomp.Damage.DamageDict[result] != 0) + if (result != null && _damageable.GetAllDamage(uid).DamageDict[result] != 0) { AddEnergy(uid, component, -30); var newdamage = component.HealingStrength; - if (damagecomp.Damage.DamageDict[result] <= newdamage) - newdamage = (int)damagecomp.Damage.DamageDict[result]; + if (_damageable.GetAllDamage(uid).DamageDict[result] <= newdamage) + newdamage = (int)_damageable.GetAllDamage(uid).DamageDict[result]; DamageSpecifier damage = new() { DamageDict = new() @@ -734,7 +710,7 @@ private void OnHeal(EntityUid uid, ElectroMouseComponent component, ElectroMouse { result, component.HealingStrength * -1 } } }; - _damageable.TryChangeDamage(uid, damage, false, true, damagecomp, origin: uid); + _damageable.TryChangeDamage(uid, damage, false, true, origin: uid); Dirty(uid, damagecomp); } else diff --git a/Content.Server/Imperial/EmergencyButton/EmergencyButtonServerSystem.cs b/Content.Server/Imperial/EmergencyButton/EmergencyButtonServerSystem.cs new file mode 100644 index 00000000000..32a02e22f8b --- /dev/null +++ b/Content.Server/Imperial/EmergencyButton/EmergencyButtonServerSystem.cs @@ -0,0 +1,43 @@ +using Content.Server.Pinpointer; +using Content.Server.Radio.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Imperial.EmergencyButton; +using Content.Shared.Imperial.EmergencyButton.Components; +using Content.Shared.Imperial.EmergencyButton.Events; +using Robust.Server.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.EmergencyButton; + +public sealed class EmergencyButtonServerSystem : EntitySystem +{ + [Dependency] private readonly RadioSystem _radio = null!; + [Dependency] private readonly IPrototypeManager _prototype = null!; + [Dependency] private readonly NavMapSystem _navMap = null!; + [Dependency] private readonly AudioSystem _audioSystem = null!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEmergencyButtonPressed); + } + + private void OnEmergencyButtonPressed(Entity entity, ref EmergencyButtonPressedEvent args) + { + var officerName = Identity.Name(args.User, EntityManager); + + var location = _navMap.TryGetNearestBeacon((args.User, null), out var beacon, out _) + ? beacon.Value.Comp.Text! + : Loc.GetString("alert-emergency-button-popup-unknown-location"); + + var message = Loc.GetString("alert-emergency-button-popup-message", + ("officerName", officerName), + ("location", location)); + + _audioSystem.PlayPvs(entity.Comp.UseSound, Transform(entity).Coordinates); + + if (_prototype.TryIndex(entity.Comp.RadioChannel, out var radioChannel)) + _radio.SendRadioMessage(entity, message, radioChannel, entity); + } +} diff --git a/Content.Server/Imperial/EmergencyButton/EmergencyButtonSystem.cs b/Content.Server/Imperial/EmergencyButton/EmergencyButtonSystem.cs deleted file mode 100644 index 427e9655e97..00000000000 --- a/Content.Server/Imperial/EmergencyButton/EmergencyButtonSystem.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Content.Server.Chat.Systems; -using Content.Server.Pinpointer; -using Content.Server.Popups; -using Content.Server.Radio.EntitySystems; -using Content.Server.Station.Systems; -using Content.Shared.Imperial.EmergencyButton.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Popups; -using Content.Shared.Radio; -using Content.Shared.Station; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Server.Imperial.EmergencyButton; - -public sealed class EmergencyButtonSystem : EntitySystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly RadioSystem _radio = default!; - [Dependency] private readonly StationSystem _station = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly NavMapSystem _navMap = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedPopupSystem _sharedPopup = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnUseInHand); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - // Обработка автоматического разпрайминга - var now = _timing.CurTime; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp)) - { - if (comp.NextUnprime is not {} time) - continue; - - if (now >= time) - Unprime((uid, comp)); - } - } - - private void OnUseInHand(EntityUid uid, EmergencyButtonComponent component, UseInHandEvent args) - { - if (args.Handled) - return; - - var user = args.User; - - // Проверяем есть ли заряды - if (component.CurrentCharges <= 0) - { - _popup.PopupEntity(Loc.GetString(component.NoChargesMessage), uid, user); - args.Handled = true; - return; - } - - // Если кнопка не "заправлена" - заправляем её и отменяем действие - if (component.NextConfirm is not {} confirm) - { - Prime((uid, component), user); - args.Handled = true; - return; - } - - // Заправлена, но время подтверждения ещё не прошло - отменяем действие - if (_timing.CurTime < confirm) - { - _sharedPopup.PopupEntity(Loc.GetString(component.ConfirmationMessage), uid, user, PopupType.LargeCaution); - args.Handled = true; - return; - } - - // Заправлена и время подтверждения прошло - выполняем действие - ExecuteEmergencyAction((uid, component), user); - args.Handled = true; - } - - private void ExecuteEmergencyAction(Entity entity, EntityUid user) - { - var (uid, component) = entity; - - // Разпрайм кнопку - Unprime(entity); - - // Тратим заряд - component.CurrentCharges--; - Dirty(uid, component); - - // Получаем имя офицера - var officerName = Identity.Name(user, EntityManager); - - // Получаем местоположение - var location = GetUserLocation(user); - - // Формируем сообщение для рации - var message = Loc.GetString(component.AlertMessage, - ("officerName", officerName), - ("location", location)); - - // Отправляем сообщение в рацию - if (_prototype.TryIndex(component.RadioChannel, out var radioChannel)) - { - _radio.SendRadioMessage(uid, message, radioChannel, uid); - } - - // Показываем сообщение пользователю - _popup.PopupEntity(Loc.GetString(component.UseMessage), uid, user); - } - - private void Prime(Entity entity, EntityUid user) - { - var (uid, comp) = entity; - comp.NextConfirm = _timing.CurTime + comp.ConfirmDelay; - comp.NextUnprime = comp.NextConfirm + comp.PrimeTime; - Dirty(uid, comp); - - _sharedPopup.PopupEntity(Loc.GetString(comp.ConfirmationMessage), uid, user, PopupType.LargeCaution); - } - - private void Unprime(Entity entity) - { - var (uid, comp) = entity; - comp.NextConfirm = null; - comp.NextUnprime = null; - Dirty(uid, comp); - } - - private string GetUserLocation(EntityUid user) - { - // Получаем ближайший видимый конфигурируемый бекон и используем его имя - if (_navMap.TryGetNearestBeacon((user, null), out var beacon, out _)) - return beacon!.Value.Comp.Text!; - - // Бекон не найден - return Loc.GetString("alert-emergency-button-unknown-location"); - } -} diff --git a/Content.Server/Imperial/EnergyCore/Components/CoreCollisionComponent.cs b/Content.Server/Imperial/EnergyCore/Components/CoreCollisionComponent.cs new file mode 100644 index 00000000000..07c49c9c733 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/CoreCollisionComponent.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Audio; + +namespace Content.Server.Imperial.EnergyCore.Components +{ + /// + /// Не касайтесь термоядерных ядер. Это очень вредно для вашего здоровья. + /// + [RegisterComponent] + public sealed partial class CoreCollisionComponent : Component + { + [ViewVariables(VVAccess.ReadOnly)] + public int EntitiesDeleted = 0; + + [ViewVariables(VVAccess.ReadOnly)] + public bool Deletions = false; + + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier DelitionSound = new SoundPathSpecifier("/Audio/Effects/gib3.ogg"); + } +} diff --git a/Content.Server/Imperial/EnergyCore/Components/CoreShielding.cs b/Content.Server/Imperial/EnergyCore/Components/CoreShielding.cs new file mode 100644 index 00000000000..b59f9fd346e --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/CoreShielding.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Components +{ + [RegisterComponent] + public sealed partial class CoreShieldingComponent : Component + { + } +} diff --git a/Content.Server/Imperial/EnergyCore/Components/CoreTechnicalRuleComponent.cs b/Content.Server/Imperial/EnergyCore/Components/CoreTechnicalRuleComponent.cs new file mode 100644 index 00000000000..8649cd56e6b --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/CoreTechnicalRuleComponent.cs @@ -0,0 +1,13 @@ +using Content.Server.Imperial.EnergyCore; + +namespace Content.Server.Imperial.EnergyCore.Components; +/// +/// Необходим для вывода информации касательно энерго ядра в конце раунда в том случае, если оно было перегрето +/// +[RegisterComponent] +[Access(typeof(CoreTechnicalRuleSystem))] +public sealed partial class CoreTechnicalRuleComponent : Component +{ + [DataField] + public LocId EndRoundText = "endround-core-was-compromised"; +} diff --git a/Content.Server/Imperial/EnergyCore/Components/DeauthorizationCodeComponent.cs b/Content.Server/Imperial/EnergyCore/Components/DeauthorizationCodeComponent.cs new file mode 100644 index 00000000000..d147dc78a8c --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/DeauthorizationCodeComponent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Components +{ + [RegisterComponent] + public sealed partial class DeauthorizationCodeComponent : Component + {} +} diff --git a/Content.Server/Imperial/EnergyCore/Components/EnergyCore.GeneratorComponent.cs b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.GeneratorComponent.cs new file mode 100644 index 00000000000..a2889ca5194 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.GeneratorComponent.cs @@ -0,0 +1,23 @@ +namespace Content.Server.Imperial.EnergyCore.Components +{ + /// + /// Генератор энергетического ядра. + /// + [RegisterComponent] + public sealed partial class CoreGeneratorComponent : Component + { + // Ближайшее ядро + [ViewVariables(VVAccess.ReadWrite)] + public EntityUid? NearestCore; + + // Ближайшее ядро: время данное на поиск + [ViewVariables(VVAccess.ReadOnly)] + public TimeSpan SearchTime = TimeSpan.FromSeconds(5); + + [ViewVariables(VVAccess.ReadOnly)] + public float EnergyOutput = 0f; + + [DataField("energyCoefficient")] + public float EnergyCoef = 1.7f; + } +} diff --git a/Content.Server/Imperial/EnergyCore/Components/EnergyCore.PendingDetonationComponent.cs b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.PendingDetonationComponent.cs new file mode 100644 index 00000000000..aac8bda3627 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.PendingDetonationComponent.cs @@ -0,0 +1,40 @@ +using Robust.Shared.Serialization; +using Robust.Shared.Audio; +using Robust.Shared.Timing; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Server.GameTicking.Rules.Components; +using Content.Shared.DoAfter; +using Content.Shared.Imperial.EnergyCore; +using Content.Server.Imperial.EnergyCore; +/// +/// Система энергетического ядра. +/// +namespace Content.Server.Imperial.EnergyCore.Components; + +[RegisterComponent] +[Access(typeof(EnergyCorePendingDetonationSystem))] +public sealed partial class EnergyCorePendingDetonationComponent : Component +{ + // Время до детонации ядра при расплавлении. Этот параметр перезаписывается цваркой + [ViewVariables(VVAccess.ReadOnly)] + public TimeSpan DetonationTime = TimeSpan.Zero; + + [ViewVariables(VVAccess.ReadOnly)] + public TimeSpan MusicTime = TimeSpan.Zero; + + [ViewVariables(VVAccess.ReadOnly)] + public bool PlayedMusic = false; + + // Музыка детонации. + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier MeltdownMusic = new SoundPathSpecifier("/Audio/Imperial/level.ogg"); // Старый добрый level + + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier BackgroundSiren = new SoundPathSpecifier("/Audio/Imperial/EnergyCore/background_siren.ogg"); + + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier CoreAmbience2 = new SoundPathSpecifier("/Audio/Imperial/EnergyCore/CoreAmbience/coreambience_2.ogg"); + + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier CoreAmbience3 = new SoundPathSpecifier("/Audio/Imperial/EnergyCore/CoreAmbience/coreambience_3.ogg"); +} diff --git a/Content.Server/Imperial/EnergyCore/Components/EnergyCore.ScreenStatusComponent.cs b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.ScreenStatusComponent.cs new file mode 100644 index 00000000000..8cea82a728b --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/EnergyCore.ScreenStatusComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Imperial.EnergyCore; + +namespace Content.Server.Imperial.EnergyCore.Components +{ + /// + /// Система статуса ядра, определение ключевых параметров через Examine ивент. + /// + [RegisterComponent] + public sealed partial class CoreStatusScreenComponent : Component + { + [ViewVariables(VVAccess.ReadOnly)] + public CoreStatus ScreenStatus = CoreStatus.OFFLINE; + + [ViewVariables(VVAccess.ReadOnly)] + public byte SpriteStatus = 0; + + // Ближайшее ядро + [ViewVariables(VVAccess.ReadWrite)] + public EntityUid? CheckedCore; + + // Ближайшее ядро: время данное на поиск + [ViewVariables(VVAccess.ReadOnly)] + public TimeSpan SearchTime = TimeSpan.FromSeconds(5); + } +} diff --git a/Content.Server/Imperial/EnergyCore/Components/EnergyCoreComponent.cs b/Content.Server/Imperial/EnergyCore/Components/EnergyCoreComponent.cs new file mode 100644 index 00000000000..ed936748f4f --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/EnergyCoreComponent.cs @@ -0,0 +1,124 @@ +using Robust.Shared.Audio; +using Color = Robust.Shared.Maths.Color; +using Content.Shared.Imperial.EnergyCore; +using Content.Server.Imperial.EnergyCore; +/// +/// Система энергетического ядра. +/// +namespace Content.Server.Imperial.EnergyCore.Components; + +[RegisterComponent] +[Access(typeof(EnergyCoreSystem))] +public sealed partial class EnergyCoreComponent : Component +{ + /// + /// Температура ядра + /// + [ViewVariables(VVAccess.ReadWrite)] + public float CoreTemp = 0f; + + // Ближайший терминал + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? Controller; + + // Ближайший терминал: время данное на поиск + [DataField] + public TimeSpan SearchTime = TimeSpan.FromSeconds(5); + + // Минимальная температура, которая может быть у ядра + [ViewVariables(VVAccess.ReadOnly)] + public float MinCoreTemp = 0f; + + // Автосистема, определяется через терминал + [ViewVariables(VVAccess.ReadOnly)] + public bool AutoSystemActive = false; + + // Изменение температуры (в сек.) + [ViewVariables(VVAccess.ReadOnly)] + public float TempChangeMultiplier = 150f; + + // Изменение температуры (в сек.) (ивент расплавления) + [ViewVariables(VVAccess.ReadOnly)] + public float TempChangeMultiplierMeltdown = 4000f; + + // Изменение температуры (в сек.) (инициализация защитного протокола) + [ViewVariables(VVAccess.ReadOnly)] + public float TempChangeMultiplierProtocol = 4500f; + + // Изменение температуры, нужен для расчета в положительную или отрицательную сторону + [ViewVariables(VVAccess.ReadOnly)] + public float UpdateTemp = 0f; + + // Изменение температуры, будет ли после порога 600000 активирован протокол защиты + [ViewVariables(VVAccess.ReadOnly)] + public bool IsSafeProtocolActive = true; + + // Было ли анансировано, что протокол был отключен + [ViewVariables(VVAccess.ReadOnly)] + public bool AnnouncedProtocol = false; + + // Объявление при критически высокой температуре + [ViewVariables(VVAccess.ReadOnly)] + public bool AnnounceReady = true; + + // Определение enum для света + [ViewVariables(VVAccess.ReadOnly)] + public CoreStatusColorVisual CoreColorEnum = CoreStatusColorVisual.OFFLINE; + + // Техническое определение статуса энерго ядра + [ViewVariables(VVAccess.ReadOnly)] + public CoreStatus Status = CoreStatus.OFFLINE; + + // Статус изменения температуры. Нагревание, охлаждание, авто-режим + [ViewVariables(VVAccess.ReadOnly)] + public CoreTempChangeLevel TempRiseStatus = CoreTempChangeLevel.COOLING; + + // Свет источяемый ядром + [DataField] + public Color CoreColor = Color.FromHex("#74aeff"); + + // Сила света + [DataField] + public float CoreColorEnergy = 10f; + + // Радиус света + [DataField] + public float CoreColorRadius = 10f; + + // Предупреждение о критическом перегреве + [ViewVariables(VVAccess.ReadOnly)] + public SoundSpecifier CriticalHighAnnounce = new SoundPathSpecifier("/Audio/Imperial/EnergyCore/core-criticalhigh.ogg"); + + // Базовый эмбиент ядра + [DataField] + public SoundSpecifier CoreAmbience1 = new SoundPathSpecifier("/Audio/Imperial/EnergyCore/CoreAmbience/coreambience_1.ogg"); + + // Датафилды цветов + + [DataField] + public Color OfflineColor = Color.FromHex("#74aeff"); + + [DataField] + public Color IdleColor = Color.FromHex("#74aeff"); + + [DataField] + public Color StableColor = Color.FromHex("#d80000ff"); + + [DataField] + public Color OptimalColor = Color.FromHex("#ff0000ff"); + + [DataField] + public Color ModerateColor = Color.FromHex("#fff700ff"); + + [DataField] + public Color HighColor = Color.FromHex("#fbff11ff"); + + [DataField] + public Color CriticalHighColor = Color.FromHex("#fdff8fff"); + + [DataField] + public Color CompromisedColor = Color.FromHex("#ffffffff"); + + [DataField] + public Color ProtocolColor = Color.FromHex("#fff700ff"); +} diff --git a/Content.Server/Imperial/EnergyCore/Components/StationFragmentCodes.cs b/Content.Server/Imperial/EnergyCore/Components/StationFragmentCodes.cs new file mode 100644 index 00000000000..7ace8031490 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Components/StationFragmentCodes.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Components +{ + [RegisterComponent] + public sealed partial class StationFragmentCodesComponent : Component + { + } + +} diff --git a/Content.Server/Imperial/EnergyCore/EnergyCoreCommands.cs b/Content.Server/Imperial/EnergyCore/EnergyCoreCommands.cs new file mode 100644 index 00000000000..4d9b92dd29f --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/EnergyCoreCommands.cs @@ -0,0 +1,101 @@ +using Content.Shared.Administration; +using Robust.Shared.Console; +using Content.Server.Administration; +using Robust.Shared.GameObjects; +using Content.Server.Imperial.EnergyCore.Events; +using Content.Server.Imperial.EnergyCore; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; + +namespace Content.Server.Imperial.EnergyCore.Commands +{ + [AdminCommand(AdminFlags.Admin)] + public sealed class RestoreCoreCommand : LocalizedCommands + { + [Dependency] private readonly IEntityManager _entManager = default!; + public override string Command => "restorecore"; + public override string Description => Loc.GetString("restorecore-desc"); + public override string Help => Loc.GetString("restorecore-help"); + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + switch (args.Length) + { + case 1: + return CompletionResult.FromHintOptions(CompletionHelper.Components(args[0], _entManager), Loc.GetString("core-hint")); + case 2: + return CompletionResult.FromHintOptions(CompletionHelper.Components(args[1], _entManager), Loc.GetString("terminal-hint")); + case 3: + return CompletionResult.FromHintOptions(CompletionHelper.Booleans, Loc.GetString("restorecore-hint-bool")); + } + return CompletionResult.Empty; + } + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 3) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var entityIdNet)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + var entityId = _entManager.GetEntity(entityIdNet); + + if (!NetEntity.TryParse(args[1], out var entityIdtNet)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + var entityIdt = _entManager.GetEntity(entityIdtNet); + + if (!bool.TryParse(args[2], out var announce)) + { + shell.WriteLine(Loc.GetString("shell-argument-must-be-boolean")); + return; + } + + var terminal = _entManager.System(); + terminal.ResetTerminal(entityIdt); + var core = _entManager.System(); + core.RestoreCore(entityId, announce); + } + } + + [AdminCommand(AdminFlags.Admin)] + public sealed class CorearmCommand : LocalizedCommands + { + [Dependency] private readonly IEntityManager _entManager = default!; + public override string Command => "corearm"; + public override string Description => Loc.GetString("corearm-desc"); + public override string Help => Loc.GetString("corearm-help"); + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length != 1) + return CompletionResult.Empty; + return CompletionResult.FromHintOptions(CompletionHelper.Components(args[0], _entManager), Loc.GetString("core-hint")); + } + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 1) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var entityIdNet)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + var entityId = _entManager.GetEntity(entityIdNet); + var core = _entManager.System(); + core.Corearm(entityId); + } + } +} + diff --git a/Content.Server/Imperial/EnergyCore/Events/CommandEvents.cs b/Content.Server/Imperial/EnergyCore/Events/CommandEvents.cs new file mode 100644 index 00000000000..c5d44e2fd4b --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Events/CommandEvents.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Events +{ + // Команды + public sealed class Corearm : EntityEventArgs + {} + public sealed class CoreRecovery : EntityEventArgs + { + public bool Announce; + } +} diff --git a/Content.Server/Imperial/EnergyCore/Events/EnergyCoreEvents.cs b/Content.Server/Imperial/EnergyCore/Events/EnergyCoreEvents.cs new file mode 100644 index 00000000000..e9080001236 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Events/EnergyCoreEvents.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Events +{ + // События ядра + public sealed class CoreCompromisedEvent : EntityEventArgs + {} + public sealed class CoreDetonatedEvent : EntityEventArgs + { + public EntityUid? OwningStation; + } +} diff --git a/Content.Server/Imperial/EnergyCore/Events/InitEvents.cs b/Content.Server/Imperial/EnergyCore/Events/InitEvents.cs new file mode 100644 index 00000000000..720d9fd93af --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Events/InitEvents.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Imperial.EnergyCore.Events +{ + // Инициализация новых ядер или терминалов + public sealed class CoreInitEvent : EntityEventArgs + {} + public sealed class CoreTerminalInitEvent : EntityEventArgs + {} +} diff --git a/Content.Server/Imperial/EnergyCore/Systems/CoreCollisionSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/CoreCollisionSystem.cs new file mode 100644 index 00000000000..c11d172b4ba --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/CoreCollisionSystem.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Physics.Events; +using Robust.Shared.Player; +using Content.Shared.Singularity.Components; +using Content.Shared.Examine; +using Content.Server.Administration.Managers; +using Content.Server.Administration; +using Content.Shared.Administration.Logs; +using Content.Server.Administration.Logs; +using Content.Shared.Database; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Events; + +namespace Content.Server.Imperial.EnergyCore; + +public sealed class CoreCollisionSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnStartCollide); + } + private void OnExamined(EntityUid uid, CoreCollisionComponent col, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (col.Deletions) + args.PushMarkup(Loc.GetString("energycore-was-collision-true")); + else + args.PushMarkup(Loc.GetString("energycore-was-collision-false")); + + var count = col.EntitiesDeleted; + args.PushMarkup(Loc.GetString("energycore-collision-deletions", ("count", count))); + } + private void OnStartCollide(EntityUid uid, CoreCollisionComponent col, ref StartCollideEvent args) + { + var target = args.OtherEntity; + if (HasComp(target)) + return; + if (HasComp(target)) + return; + + DeleteEntity(target, col); + _audio.PlayPvs(col.DelitionSound, uid); + } + + private void DeleteEntity(EntityUid target, CoreCollisionComponent col) + { + col.EntitiesDeleted = col.EntitiesDeleted + 1; + col.Deletions = true; + _adminLogger.Add(LogType.Gib, + LogImpact.Extreme, $"Объект {target} был уничтожен об ядро."); + Del(target); + } +} + diff --git a/Content.Server/Imperial/EnergyCore/Systems/CoreTechnicalRuleSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/CoreTechnicalRuleSystem.cs new file mode 100644 index 00000000000..7d5620d8d42 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/CoreTechnicalRuleSystem.cs @@ -0,0 +1,50 @@ +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Components; +using Content.Shared.GameTicking.Components; +using Robust.Shared.Prototypes; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Events; + +namespace Content.Server.Imperial.EnergyCore; + +public sealed class CoreTechnicalRuleSystem : GameRuleSystem +{ + public static readonly EntProtoId CoreTechnicalRule = "CoreTechnical"; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCoreDetonated); + } + + protected override void Started(EntityUid uid, + CoreTechnicalRuleComponent component, + GameRuleComponent gameRule, + GameRuleStartedEvent args) + { + } + + protected override void AppendRoundEndText(EntityUid uid, + CoreTechnicalRuleComponent component, + GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) + { + var coremain = Loc.GetString("core-technical-main"); + args.AddLine(coremain); + + var text = Loc.GetString(component.EndRoundText); + args.AddLine(text); + } + private void OnCoreDetonated(CoreDetonatedEvent ev) + { + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var core, out _)) + { + if (GameTicker.IsGameRuleActive(CoreTechnicalRule)) + { + GameTicker.EndGameRule(uid); + } + } + } +} diff --git a/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.AccessComputerSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.AccessComputerSystem.cs new file mode 100644 index 00000000000..2213f5ad7c8 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.AccessComputerSystem.cs @@ -0,0 +1,140 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.GameObjects; +using Robust.Server.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.Timing; +using Content.Shared.Power.Components; +using Content.Server.Power.Components; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Imperial.EnergyCore; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Events; +using Content.Server.Imperial.EnergyCore.Helpers; + +namespace Content.Server.Imperial.EnergyCore; + +public sealed class CoreAccessComputerSystem : SharedCoreAccessComputerSystem +{ + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly CoreSearchSystem _coreHelper = default!; + [Dependency] private readonly EnergyCoreSystem _core = default!; + public HashSet CoreTerminalHash = new(); + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnItemSlotChanged); + SubscribeLocalEvent(OnNewCoreInit); + } + private void OnInit(EntityUid uid, CoreAccessComputerComponent terminal, ComponentInit args) + { + CoreTerminalHash.Add(uid); + RaiseLocalEvent(new CoreTerminalInitEvent()); + _itemSlots.AddItemSlot(uid, SharedCoreAccessComponent.DeCodeSlotId, terminal.DeCodeSlot); + terminal.SearchTime = terminal.SearchTime + _timing.CurTime; + + RejoinCore(uid, terminal); + UpdateUi(uid, terminal); + } + private void OnShutdown(EntityUid uid, CoreAccessComputerComponent terminal, ComponentShutdown args) + => CoreTerminalHash.Remove(uid); + private void RejoinCore(EntityUid uid, CoreAccessComputerComponent terminal) + => terminal.ControledCore = _coreHelper.FindNearestEnergyCore(uid, _core.CoreHash, 30f); + private void OnNewCoreInit(CoreInitEvent ev) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var terminal)) + { + RejoinCore(uid, terminal); + } + } + private void OnItemSlotChanged(EntityUid uid, CoreAccessComputerComponent terminal, ContainerModifiedMessage args) + { + if (!terminal.Initialized) + return; + + if (args.Container.ID != terminal.DeCodeSlot.ID) + return; + + GetCheckTime(terminal); + } + private void GetCheckTime(CoreAccessComputerComponent terminal) + { + terminal.Time = _timing.CurTime + terminal.TimeToCheck; + } + private void CompleteProtocolDeactivation(EntityUid uid, CoreAccessComputerComponent terminal) + { + terminal.SaveProtocolWasDeactivated = true; + terminal.TerminalStatus = 2; + } + private void UpdateVisual(EntityUid uid, CoreAccessComputerComponent terminal) + { + if (TryComp(uid, out var appearance)) + { + _appearance.SetData(uid, CoreStatusScreenVisual.Core_Screen_Visual, terminal.TerminalStatus, appearance); + } + } + private void UpdateFinalCoef(EntityUid uid, CoreAccessComputerComponent value) + { + value.FinalTempChangeCoef = value.Reactivity * value.Halflife; // Халфлайф 3 не будет + } + private void GetCurrTemp(EntityUid uid, CoreAccessComputerComponent terminal) + { + var nearestUid = terminal.ControledCore; + if (nearestUid == null || !TryComp(nearestUid, out EnergyCoreComponent? nearest)) + { + return; + } + var (coreTemp, coreStatus) = GetCoreInfo(nearest); + terminal.CurrCoreTemp = coreTemp; + terminal.Status = coreStatus; + } + private (float coreTemp, CoreStatus coreStatus) GetCoreInfo(EnergyCoreComponent terminal) + { + var coreTemp = terminal.CoreTemp; + var coreStatus = terminal.Status; + + return (coreTemp, coreStatus); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var terminal)) + { + UpdateFinalCoef(uid, terminal); + if (_timing.CurTime >= terminal.Time && terminal.DeCodeSlot.HasItem) + { + UpdateVisual(uid, terminal); + if (!terminal.DeactivationCompleted) + { + terminal.DeactivationCompleted = true; + CompleteProtocolDeactivation(uid, terminal); + } + } + GetCurrTemp(uid, terminal); + } + } + #region public API + public void ResetTerminal(EntityUid terminalUid, CoreAccessComputerComponent? terminal = null) + { + if (!Resolve(terminalUid, ref terminal)) + return; + + GetCheckTime(terminal); + terminal.DeactivationCompleted = false; + terminal.SaveProtocolWasDeactivated = false; + terminal.TerminalStatus = 1; + UpdateVisual(terminalUid, terminal); + } + #endregion +} diff --git a/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.GeneratorSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.GeneratorSystem.cs new file mode 100644 index 00000000000..fd846c36f91 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.GeneratorSystem.cs @@ -0,0 +1,104 @@ +using Robust.Shared.Timing; +using Content.Server.Power.Components; +using Content.Shared.Imperial.EnergyCore; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Events; +using Content.Server.Imperial.EnergyCore.Helpers; +using Content.Shared.Examine; + +namespace Content.Server.Imperial.EnergyCore +{ + public sealed class CoreGeneratorSystem : EntitySystem + { + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly CoreSearchSystem _coreHelper = default!; + [Dependency] private readonly EnergyCoreSystem _core = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnNewCoreInit); + } + private void OnInit(EntityUid uid, CoreGeneratorComponent generator, ComponentInit args) + { + RejoinCore(uid, generator); + generator.SearchTime = generator.SearchTime + _timing.CurTime; + } + private void OnExamined(EntityUid uid, CoreGeneratorComponent generator, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + var nearestUid = generator.NearestCore; + if (nearestUid == null || !TryComp(nearestUid, out EnergyCoreComponent? nearest)) + { + args.PushMarkup(Loc.GetString("energycore-dont-any-near")); + return; + } + var (coreTemp, tempRiseStatus) = GetCoreInfo(nearest); + + args.PushMarkup(Loc.GetString($"energycore-current-temp-change-{tempRiseStatus.ToString().ToLower()}")); + + var energyOutput = generator.EnergyOutput; + args.PushMarkup(Loc.GetString("energycore-generator-current-energy-output", ("energyOutput", energyOutput))); + } + private void RejoinCore(EntityUid uid, CoreGeneratorComponent generator) + => generator.NearestCore = _coreHelper.FindNearestEnergyCore(uid, _core.CoreHash, 10f); + private void OnNewCoreInit(CoreInitEvent ev) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var generator)) + { + RejoinCore(uid, generator); + } + } + private void SetEnergyOutput(EntityUid uid, CoreGeneratorComponent generator, PowerSupplierComponent power) + { + var nearestUid = generator.NearestCore; + + if (nearestUid == null || !TryComp(nearestUid, out EnergyCoreComponent? nearest)) + { + generator.EnergyOutput = 0f; + power.MaxSupply = 0f; + return; + } + + var (coreTemp, tempRiseStatus) = GetCoreInfo(nearest); + + if (coreTemp > 0f) + { + if (coreTemp > 500000f) // Ибо нехер забивать на ядро + generator.EnergyOutput = coreTemp / 10f; + else + generator.EnergyOutput = coreTemp / generator.EnergyCoef; + power.MaxSupply = generator.EnergyOutput; + } + else + { + generator.EnergyOutput = 0f; + power.MaxSupply = 0f; + } + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var comp, out var powr, out _)) + { + SetEnergyOutput(uid, comp, powr); + } + } + + private (float coreTemp, CoreTempChangeLevel tempRiseStatus) GetCoreInfo(EnergyCoreComponent generator) + { + var coreTemp = generator.CoreTemp; + var tempRiseStatus = generator.TempRiseStatus; + + return (coreTemp, tempRiseStatus); + } + } +} + diff --git a/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.PendingDetonationSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.PendingDetonationSystem.cs new file mode 100644 index 00000000000..34ab78c8821 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.PendingDetonationSystem.cs @@ -0,0 +1,136 @@ +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.GameObjects; +using Robust.Shared.Physics.Dynamics; +using Robust.Shared.Physics.Events; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; +using Robust.Shared.Configuration; +using Content.Server.Radio.EntitySystems; +using Content.Server.Chat.Systems; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Content.Shared.GameTicking.Components; +using Content.Server.Audio; +using Content.Server.Explosion.EntitySystems; +using Content.Server.AlertLevel; +using Content.Server.Station.Systems; +using Content.Shared.Audio; +using Content.Shared.Imperial.ICCVar; +using Content.Shared.Imperial.EnergyCore; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Events; + +namespace Content.Server.Imperial.EnergyCore; + +public sealed class EnergyCorePendingDetonationSystem : EntitySystem +{ + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly AlertLevelSystem _alertLevel = default!; + [Dependency] private readonly ExplosionSystem _explosion = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; + [Dependency] private readonly ServerGlobalSoundSystem _sound = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + } + + private void OnStartup(EntityUid uid, EnergyCorePendingDetonationComponent component, ComponentStartup args) + { + // Определение времени до детонации через цварку + var detonationTime = _cfg.GetCVar(ICCVars.CoreDetonationTime); + component.DetonationTime = TimeSpan.FromSeconds(detonationTime); + + // Ставим новое имя и описание сущности + _metaData.SetEntityName(uid, Loc.GetString("energycore-meltdown-name")); + _metaData.SetEntityDescription(uid, Loc.GetString("energycore-meltdown-desc")); + + var ev = new CoreCompromisedEvent(); + RaiseLocalEvent(ev); + + GetDelayTime(uid, component); + AnnounceCatastroph(uid, component); + PlayBackgroundSiren(uid, component); + } + private void GetDelayTime(EntityUid uid, EnergyCorePendingDetonationComponent component) + { + // К полученному от цварки времени добавляем текущее время + component.DetonationTime = _timing.CurTime + component.DetonationTime; + GetMusicTime(uid, component); + } + private void GetMusicTime(EntityUid uid, EnergyCorePendingDetonationComponent component) + { + var sound = _audio.ResolveSound(component.MeltdownMusic); + var audioLength = _audio.GetAudioLength(sound).TotalSeconds; + component.MusicTime = component.DetonationTime - TimeSpan.FromSeconds(audioLength); + } + private void AnnounceCatastroph(EntityUid uid, EnergyCorePendingDetonationComponent component) + { + var station = _stationSystem.GetOwningStation(uid); + if (station != null) + { + var alertLevel = _cfg.GetCVar(ICCVars.AlertLevelOnMeltdown); + _alertLevel.SetLevel(station.Value, alertLevel, true, true, true, false); + } + if (HasComp(uid)) + _ambientSound.SetSound(uid, component.CoreAmbience2); + } + private void PlayBackgroundSiren(EntityUid uid, EnergyCorePendingDetonationComponent corecomp) + { + _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(corecomp.BackgroundSiren), new AudioParams { Volume = -3f }); + + } + private void PlayMeltdownMusic(EntityUid uid, EnergyCorePendingDetonationComponent corecomp) + { + if (!corecomp.PlayedMusic) + { + corecomp.PlayedMusic = true; + _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(corecomp.MeltdownMusic), new AudioParams { Volume = -2f }); + + var station = _stationSystem.GetOwningStation(uid); + if (station != null) + { + _chatSystem.DispatchStationAnnouncement(station.Value, + Loc.GetString("energycore-less-2min-to-boom"), + Loc.GetString("energy-department"), + playDefaultSound: true, + colorOverride: Color.Red); + } + if (HasComp(uid)) + _ambientSound.SetSound(uid, corecomp.CoreAmbience3); + } + } + private void ExplodeCore(EntityUid uid, EnergyCorePendingDetonationComponent component, + TransformComponent transform) + { + _explosion.QueueExplosion(uid, ExplosionSystem.DefaultExplosionPrototypeId, 5000000, 5, 100); + RaiseLocalEvent(new CoreDetonatedEvent() + { + OwningStation = transform.GridUid, + }); + Del(uid); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var trans)) + { + if (_timing.CurTime >= comp.MusicTime) + PlayMeltdownMusic(uid, comp); + if (_timing.CurTime >= comp.DetonationTime) + ExplodeCore(uid, comp, trans); + } + } +} diff --git a/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.ScreenStatusSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.ScreenStatusSystem.cs new file mode 100644 index 00000000000..e474622d8e8 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/EnergyCore.ScreenStatusSystem.cs @@ -0,0 +1,109 @@ +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; +using Content.Shared.Imperial.EnergyCore; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.Power.Components; +using Content.Server.Imperial.EnergyCore.Events; +using Content.Server.Imperial.EnergyCore.Helpers; +using Content.Shared.Examine; + +namespace Content.Server.Imperial.EnergyCore +{ + public sealed class CoreStatusScreenSystem : EntitySystem + { + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly CoreSearchSystem _coreHelper = default!; + [Dependency] private readonly EnergyCoreSystem _core = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnNewCoreInit); + } + private void OnInit(EntityUid uid, CoreStatusScreenComponent screen, ComponentInit args) + { + RejoinCore(uid, screen); + screen.SearchTime = screen.SearchTime + _timing.CurTime; + } + private void OnExamined(EntityUid uid, CoreStatusScreenComponent screen, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + var nearestUid = screen.CheckedCore; + if (nearestUid == null || !TryComp(nearestUid, out EnergyCoreComponent? nearest)) + { + return; + } + + var (coreTemp, coreStatus, tempRiseStatus, safeProtocol, coreTempMult) = GetCoreInfo(nearest); + + args.PushMarkup(Loc.GetString($"energycore-current-temp-change-{tempRiseStatus.ToString().ToLower()}")); + + var protocol = Loc.GetString($"energycore-current-protocol-is-safe-{safeProtocol.ToString().ToLower()}"); + args.PushMarkup(protocol); + + var status = Loc.GetString($"energycore-screen-status-{coreStatus.ToString().ToLower()}"); + args.PushMarkup(status); + + args.PushMarkup(Loc.GetString("energycore-current-coef", ("coefficient", coreTempMult))); + args.PushMarkup(Loc.GetString("energycore-current-temp", ("coreTemp", coreTemp))); + } + private void RejoinCore(EntityUid uid, CoreStatusScreenComponent screen) + => screen.CheckedCore = _coreHelper.FindNearestEnergyCore(uid, _core.CoreHash, float.MaxValue); + private void OnNewCoreInit(CoreInitEvent ev) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var screen)) + { + RejoinCore(uid, screen); + } + } + private void UpdateScreenVisual(EntityUid uid, CoreStatusScreenComponent screen) + { + var nearestUid = screen.CheckedCore; + + if (nearestUid == null || !TryComp(nearestUid, out EnergyCoreComponent? nearest)) + { + return; + } + var (coreTemp, coreStatus, tempRiseStatus, safeProtocol, coreTempMult) = GetCoreInfo(nearest); + screen.SpriteStatus = (byte)coreStatus; + if (TryComp(uid, out var appearance)) + { + _appearance.SetData(uid, CoreStatusScreenVisual.Core_Screen_Visual, screen.SpriteStatus, appearance); + } + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var comp, out _)) + { + UpdateScreenVisual(uid, comp); + } + } + private (float coreTemp, CoreStatus coreStatus, CoreTempChangeLevel tempRiseStatus, bool safeProtocol, float coreTempMult) GetCoreInfo(EnergyCoreComponent core) + { + var coreTemp = core.CoreTemp; + var coreStatus = core.Status; + var tempRiseStatus = core.TempRiseStatus; + var safeProtocol = core.IsSafeProtocolActive; + var coreTempMult = core.TempChangeMultiplier; + + return (coreTemp, coreStatus, tempRiseStatus, safeProtocol, coreTempMult); + } + } +} + diff --git a/Content.Server/Imperial/EnergyCore/Systems/EnergyCoreSystem.cs b/Content.Server/Imperial/EnergyCore/Systems/EnergyCoreSystem.cs new file mode 100644 index 00000000000..b002dfd81f4 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/EnergyCoreSystem.cs @@ -0,0 +1,374 @@ +using Robust.Shared.Prototypes; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Physics.Dynamics; +using Robust.Shared.Physics.Events; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; +using Content.Server.GameTicking; +using Color = Robust.Shared.Maths.Color; +using Content.Server.AlertLevel; +using Content.Server.Station.Systems; +using Content.Shared.Audio; +using Content.Server.Audio; +using Content.Server.Radio.EntitySystems; +using Content.Server.Chat.Systems; +using Content.Shared.Imperial.EnergyCore; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Helpers; +using Content.Server.Imperial.EnergyCore.Events; + +namespace Content.Server.Imperial.EnergyCore; + +public sealed partial class EnergyCoreSystem : EntitySystem +{ + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly AlertLevelSystem _alertLevel = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ServerGlobalSoundSystem _sound = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly SharedPointLightSystem _pointLight = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly CoreSearchSystem _coreHelper = default!; + [Dependency] private readonly CoreAccessComputerSystem _coreTerminal = default!; + public static readonly EntProtoId CoreTechnicalRule = "CoreTechnical"; + public HashSet CoreHash = new(); + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnNewCoreTerminalInit); + } + private void UpdateDataFromTerminal(EntityUid uid, EnergyCoreComponent core) + { + var nearestUid = core.Controller; + if (nearestUid == null || !TryComp(nearestUid, out CoreAccessComputerComponent? nearest)) + { + return; + } + var (safeProtocol, safeProtocolCompleted, tempRiseTerminal, finalTempChangeCoef) = GetTerminalProtocolStatus(nearest); + + core.TempChangeMultiplier = finalTempChangeCoef; + core.TempRiseStatus = tempRiseTerminal; + } + private void UpdateLightVisual(EntityUid uid, Color coreColor, float coreColorRadius, float coreColorEnergy) + { + if (TryComp(uid, out var light)) + { + _pointLight.SetColor(uid, coreColor, light); + _pointLight.SetRadius(uid, coreColorRadius); + _pointLight.SetEnergy(uid, coreColorEnergy); + } + } + private void UpdateCoreVisual(EntityUid uid, EnergyCoreComponent core) + { + if (TryComp(uid, out var appearance)) + { //Byte + _appearance.SetData(uid, CoreStatusVisual.Core_Visual, (byte)core.Status, appearance); + } + } + private void UpdateCoreTemp(EnergyCoreComponent core, float frameTime) // YandereDev ahh moment + { + core.UpdateTemp = frameTime * core.TempChangeMultiplier; + + switch (core.TempRiseStatus) + { + case CoreTempChangeLevel.COOLING: // Охлаждение + core.CoreTemp -= core.UpdateTemp; + if (core.CoreTemp < core.MinCoreTemp) + core.CoreTemp = core.MinCoreTemp; + break; + + case CoreTempChangeLevel.AUTO: // Авто режим активен + if (core.CoreTemp < 150000) + core.CoreTemp += core.UpdateTemp; + else + core.CoreTemp -= core.UpdateTemp; + break; + + case CoreTempChangeLevel.HEATING: // Нагревание + core.CoreTemp += core.UpdateTemp; + break; + + default: + core.TempRiseStatus = CoreTempChangeLevel.COOLING; + break; + } + + if (core.IsSafeProtocolActive) + { + if (core.Status == CoreStatus.SAFEPROTOCOL) // Если температура больше 600.000 и протокол безопасности не был отключен + { + core.UpdateTemp = frameTime * core.TempChangeMultiplierProtocol; + core.CoreTemp -= core.UpdateTemp; + } + } + if (core.Status == CoreStatus.CATASTROPHIC) // При катастрофическом статусе контроль над ядром полностью потерян и температура ядра НЕ может быть снижена + { + core.UpdateTemp = frameTime * core.TempChangeMultiplierMeltdown; + core.CoreTemp += core.UpdateTemp; + } + } + private void UpdateProtocolStatus(EntityUid uid, EnergyCoreComponent core) + { + var nearestUid = core.Controller; + if (nearestUid == null || !TryComp(nearestUid, out CoreAccessComputerComponent? nearest)) + { + return; + } + var (safeProtocol, safeProtocolCompleted, tempRiseTerminal, finalTempChangeCoef) = GetTerminalProtocolStatus(nearest); + + if (safeProtocol) + core.IsSafeProtocolActive = false; + else + return; + + if (core.AnnouncedProtocol) return; + + core.AnnouncedProtocol = true; + + var station = _stationSystem.GetOwningStation(uid); + if (station == null) return; + + _alertLevel.SetLevel(station.Value, "red", true, true, true); + _chatSystem.DispatchStationAnnouncement(station.Value, + Loc.GetString("energycore-protocol-deactivated"), Loc.GetString("energy-department"), + playDefaultSound: false, colorOverride: Color.Red); + } + private void OnInit(EntityUid uid, EnergyCoreComponent core, ComponentInit args) + { + CoreHash.Add(uid); + RaiseLocalEvent(new CoreInitEvent()); + RejoinController(uid, core); + + if (HasComp(uid)) + _ambientSound.SetSound(uid, core.CoreAmbience1); + } + private void OnShutdown(EntityUid uid, EnergyCoreComponent core, ComponentShutdown args) => CoreHash.Remove(uid); + private void RejoinController(EntityUid uid, EnergyCoreComponent core) + => core.Controller = _coreHelper.FindNearestEnergyCore(uid, _coreTerminal.CoreTerminalHash, 30f); + private void OnNewCoreTerminalInit(CoreTerminalInitEvent ev) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var core)) + { + RejoinController(uid, core); + } + } + private void OnMeltdown(EntityUid uid, EnergyCoreComponent core) // За ивент расплавления ядра отвечает отдельная система, чтобы не превращать эту в свалку + { + if (!HasComp(uid) && core.Status == CoreStatus.CATASTROPHIC) + { + _gameTicker.AddGameRule(CoreTechnicalRule); + EnsureComp(uid); + } + else + return; + } + private void RefreshCoreStatus(EntityUid uid, EnergyCoreComponent core) + { + switch (core.Status) + { + case CoreStatus.OFFLINE: // Статус ядра: Оффлайн + UpdateLightVisual(uid, core.OfflineColor, 1f, 1f); + if (core.CoreTemp > 1f) + { + core.Status = CoreStatus.IDLE; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.IDLE: // Статус ядра: Простаивание + UpdateLightVisual(uid, core.IdleColor, 10f, 2f); + if (core.CoreTemp > 30000f) + { + core.Status = CoreStatus.STABLE; + UpdateCoreVisual(uid, core); + } + if (core.CoreTemp <= 0f) + { + core.Status = CoreStatus.OFFLINE; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.STABLE: // Статус ядра: Стабильный + UpdateLightVisual(uid, core.StableColor, 11f, 3f); + if (core.CoreTemp > 100000f) + { + core.Status = CoreStatus.OPTIMAL; + UpdateCoreVisual(uid, core); + } + if (core.CoreTemp < 30000f) + { + core.Status = CoreStatus.IDLE; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.OPTIMAL: // Статус ядра: Оптимальный + UpdateLightVisual(uid, core.OptimalColor, 11f, 3f); + if (core.CoreTemp > 300000f) + { + core.Status = CoreStatus.MODERATE; + UpdateCoreVisual(uid, core); + } + if (core.CoreTemp < 100000f) + { + core.Status = CoreStatus.STABLE; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.MODERATE: // Статус ядра: Приемлимый (повышенный оптимальный) + UpdateLightVisual(uid, core.ModerateColor, 12f, 4f); + if (core.CoreTemp > 600000f) + { + if (core.IsSafeProtocolActive) + { + core.Status = CoreStatus.SAFEPROTOCOL; + UpdateCoreVisual(uid, core); + } + else + { + core.Status = CoreStatus.HIGH; + UpdateCoreVisual(uid, core); + } + } + if (core.CoreTemp < 300000f) + { + core.Status = CoreStatus.OPTIMAL; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.HIGH: // Статус ядра: Высокая температура + UpdateLightVisual(uid, core.HighColor, 12f, 4f); + if (core.CoreTemp > 800000f) + { + core.Status = CoreStatus.CRITICALHIGH; + UpdateCoreVisual(uid, core); + SendCriticalAnnounce(uid, core); // Сообщение + } + if (core.CoreTemp < 600000f) + { + core.Status = CoreStatus.MODERATE; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.CRITICALHIGH: // Статус ядра: Критически высокая температура + UpdateLightVisual(uid, core.CriticalHighColor, 15f, 7f); + if (core.CoreTemp > 1000000f) + { + core.Status = CoreStatus.CATASTROPHIC; + UpdateCoreVisual(uid, core); + } + if (core.CoreTemp < 800000f) + { + core.Status = CoreStatus.HIGH; + UpdateCoreVisual(uid, core); + } + break; + + case CoreStatus.CATASTROPHIC: // Статус ядра: Катастрофически высокая температура (расплавление) + UpdateLightVisual(uid, core.CompromisedColor, 20f, 12f); + OnMeltdown(uid, core); // Ивент расплавления ядра + break; + + case CoreStatus.SAFEPROTOCOL: // Статус ядра: Протокол безопасности активен + UpdateLightVisual(uid, core.ProtocolColor, 12f, 4f); + if (core.CoreTemp < 500000f) + core.Status = CoreStatus.MODERATE; + break; + + default: + core.Status = CoreStatus.OFFLINE; // Базовый статус ядра: Оффлайн + break; + } + } + private void SendCriticalAnnounce(EntityUid uid, EnergyCoreComponent core) + { + if (!core.AnnounceReady) return; + + var station = _stationSystem.GetOwningStation(uid); + + if (station == null) return; + _chatSystem.DispatchStationAnnouncement(station.Value, + Loc.GetString("energycore-critical-high-announce"), Loc.GetString("energy-department"), + playDefaultSound: false, colorOverride: Color.DarkSalmon); + _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(core.CriticalHighAnnounce)); + core.AnnounceReady = false; + } + private (bool safeProtocol, bool safeProtocolCompleted, CoreTempChangeLevel tempRiseTerminal, float finalTempChangeCoef) GetTerminalProtocolStatus(CoreAccessComputerComponent terminal) + { + var safeProtocol = terminal.SaveProtocolWasDeactivated; + var safeProtocolCompleted = terminal.DeactivationCompleted; + var tempRiseTerminal = terminal.TempRiseTerminal; + var finalTempChangeCoef = terminal.FinalTempChangeCoef; + return (safeProtocol, safeProtocolCompleted, tempRiseTerminal, finalTempChangeCoef); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var cormp, out _)) + { + UpdateCoreTemp(cormp, frameTime); + RefreshCoreStatus(uid, cormp); + UpdateProtocolStatus(uid, cormp); + UpdateDataFromTerminal(uid, cormp); + } + } + #region public API + + public void RestoreCore(EntityUid uid, bool announce, EnergyCoreComponent? core = null) + { + if (!Resolve(uid, ref core)) + return; + + _metaData.SetEntityName(uid, Loc.GetString("energycore-name")); + _metaData.SetEntityDescription(uid, Loc.GetString("energycore-desc")); + _ambientSound.SetSound(uid, core.CoreAmbience1); + + core.CoreTemp = 0; + core.Status = CoreStatus.OFFLINE; + + if (!core.IsSafeProtocolActive && core.AnnouncedProtocol) + { + core.IsSafeProtocolActive = true; + core.AnnouncedProtocol = false; + } + + if (TryComp(uid, out EnergyCorePendingDetonationComponent? pending)) + RemComp(uid, pending); + + if (announce) + { + var station = _stationSystem.GetOwningStation(uid); + if (station != null) + { + _alertLevel.SetLevel(station.Value, "green", true, true, true); + _chatSystem.DispatchStationAnnouncement(station.Value, + Loc.GetString("energycore-have-a-good-day"), Loc.GetString("energy-department"), + playDefaultSound: false, colorOverride: Color.DarkSalmon); + } + } + UpdateCoreVisual(uid, core); + } + public void Corearm(EntityUid uid, EnergyCoreComponent? core = null) + { + if (!Resolve(uid, ref core)) + return; + + core.IsSafeProtocolActive = false; + core.CoreTemp = 1010000f; + } + #endregion +} diff --git a/Content.Server/Imperial/EnergyCore/Systems/NukeopsRuleSystemImperial.cs b/Content.Server/Imperial/EnergyCore/Systems/NukeopsRuleSystemImperial.cs new file mode 100644 index 00000000000..162200cefe7 --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/Systems/NukeopsRuleSystemImperial.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Content.Shared.GameTicking.Components; +using Content.Server.RoundEnd; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Imperial.EnergyCore; +using Content.Server.Imperial.EnergyCore.Events; +using Content.Server.Imperial.EnergyCore.Components; + +namespace Content.Server.GameTicking.Rules; + +// Imperial EnergyCore ивент подрыва ядра +public sealed partial class NukeopsRuleSystem : GameRuleSystem +{ + private void InitializeImperial() + { + SubscribeLocalEvent(OnCoreDetonated); + } + private void OnCoreDetonated(CoreDetonatedEvent ev) + { + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) + { + nukeops.WinConditions.Add(WinCondition.CoreExploded); // Винтайп подрыва энерго ядра + SetWinType((uid, nukeops), WinType.OpsMajor); + + foreach (var cond in nukeops.WinConditions) + { + if (cond.ToString().ToLower() == "CoreExploded") + { + if (!GameTicker.IsGameRuleActive("Nukeops")) return; + GameTicker.EndGameRule(uid); + _roundEndSystem.EndRound(); + break; + } + } + } + } +} diff --git a/Content.Server/Imperial/EnergyCore/license.txt b/Content.Server/Imperial/EnergyCore/license.txt new file mode 100644 index 00000000000..7bdaea39b5d --- /dev/null +++ b/Content.Server/Imperial/EnergyCore/license.txt @@ -0,0 +1,2 @@ +license: This content is under ICLA. Copyright holder: texttipo (discord) (Read more on: https://wiki.imperialspace.net/ru/icla) +copyright: "texttipo (discord)" diff --git a/Content.Server/Imperial/Entry.cs b/Content.Server/Imperial/Entry.cs index 11deeec3140..8071bb7ba84 100644 --- a/Content.Server/Imperial/Entry.cs +++ b/Content.Server/Imperial/Entry.cs @@ -8,15 +8,17 @@ public sealed partial class ImperialEntry public static void Init() { IoCManager.Resolve().Initialize(); + MedievalInit(); } public static void PostInit() { - + MedievalPostInit(); } - public static void IoCRegister() + public static void IoCRegister(IDependencyCollection deps) { - IoCManager.Register(); + deps.Register(); + MedievalIoCRegister(deps); } } diff --git a/Content.Server/Imperial/ErtCall/ErtCallCommand.cs b/Content.Server/Imperial/ErtCall/ErtCallCommand.cs index 7c26d26ed09..9a4752a1dd0 100644 --- a/Content.Server/Imperial/ErtCall/ErtCallCommand.cs +++ b/Content.Server/Imperial/ErtCall/ErtCallCommand.cs @@ -8,6 +8,7 @@ using Robust.Shared.Audio.Systems; using Content.Server.Imperial.ErtCall; using Content.Server.Chat.Systems; +using Robust.Shared.Utility; namespace Content.Server.Imperial.ErtCall; @@ -15,12 +16,17 @@ namespace Content.Server.Imperial.ErtCall; [AdminCommand(AdminFlags.Admin)] public sealed class CallErt : LocalizedCommands { - public string Description => Loc.GetString("callertcommand-desc"); - public string Help => Loc.GetString("callertcommand-help"); [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; public override string Command => "callert"; + public override string Description => Loc.GetString("callertcommand-desc"); + public override string Help => Loc.GetString("callertcommand-help"); + + + private static readonly ResPath NoErtSoundPath = new("/Audio/Imperial/ErtCall/noert.ogg"); + private static readonly ResPath YesErtSoundPath = new("/Audio/Imperial/ErtCall/yesert.ogg"); + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) { @@ -43,7 +49,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length == 0) { shell.WriteError(Loc.GetString("callertcommand-error-args0")); - _entity.System().PlayGlobal("/Audio/Imperial/ErtCall/noert.ogg", Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f)); + _entity.System().PlayGlobal(new SoundPathSpecifier(NoErtSoundPath), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f)); return; } if (args.Length > 1) @@ -61,7 +67,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) } if (ertSpawnSystem.SpawnErt(proto)) { - _entity.System().PlayGlobal("/Audio/Imperial/ErtCall/yesert.ogg", Filter.Broadcast(), true, AudioParams.Default.WithVolume(-5f)); + _entity.System().PlayGlobal(new SoundPathSpecifier(YesErtSoundPath), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-5f)); shell.WriteLine(Loc.GetString("callertcommand-preset-loaded", ("protoid", protoId))); return; } diff --git a/Content.Server/Imperial/ErtCall/ErtCallPresetPrototype.cs b/Content.Server/Imperial/ErtCall/ErtCallPresetPrototype.cs index 0958c71982d..4db6dd4c6c1 100644 --- a/Content.Server/Imperial/ErtCall/ErtCallPresetPrototype.cs +++ b/Content.Server/Imperial/ErtCall/ErtCallPresetPrototype.cs @@ -1,10 +1,11 @@ using Robust.Shared.Prototypes; namespace Content.Server.Imperial.ErtCall; -[Serializable, Prototype("ertCall")] -public sealed class ErtCallPresetPrototype : IPrototype + +[Prototype("ertCall")] +public sealed partial class ErtCallPresetPrototype : IPrototype { - [IdDataField] public string ID { get; } = default!; + [IdDataField] public string ID { get; private set; } = default!; [DataField("path")] public string Path { get; set; } = string.Empty; diff --git a/Content.Server/Imperial/EverythingStamp/EverythingStampSystem.cs b/Content.Server/Imperial/EverythingStamp/EverythingStampSystem.cs index 60bbced7c77..f17260ae7d1 100644 --- a/Content.Server/Imperial/EverythingStamp/EverythingStampSystem.cs +++ b/Content.Server/Imperial/EverythingStamp/EverythingStampSystem.cs @@ -3,16 +3,15 @@ using Content.Shared.Paper; using Robust.Shared.Audio.Systems; using Content.Shared.Popups; -using Microsoft.CodeAnalysis.Elfie.Diagnostics; namespace Content.Server.Paper { public sealed class EverythingStampSystem : EntitySystem { [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] protected readonly ILocalizationManager Loc = default!; - [Dependency] private readonly IEntityManager _entities = default!; - [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() { base.Initialize(); @@ -35,7 +34,7 @@ private void OnInHandActivation(Entity entity, ref Use }); positionOfPreviousMode = positionOfPreviousMode == entity.Comp.CollectedStamps.Count - 1 ? -1 : positionOfPreviousMode; entity.Comp.CurrentStampName = entity.Comp.CollectedStamps[positionOfPreviousMode + 1].StampedName; - Popup.PopupEntity(Loc.GetString("everything-stamp-chosen-stamp") + " " + Loc.GetString(entity.Comp.CollectedStamps[positionOfPreviousMode + 1].StampedName), entity, args.User); + _popup.PopupEntity(Loc.GetString("everything-stamp-chosen-stamp") + " " + Loc.GetString(entity.Comp.CollectedStamps[positionOfPreviousMode + 1].StampedName), entity, args.User); if (TryComp(entity, out StampComponent? stamp)) { stamp.StampedName = entity.Comp.CollectedStamps[positionOfPreviousMode + 1].StampedName; @@ -50,11 +49,11 @@ private void OnInteractUsing(EntityUid uid, EverythingStampComponent everythingS if (!TryCopyStamp(uid, GetStampInfo(stampComp), stampComp.StampState, everythingStampComp)) { _audio.PlayPvs(stampComp.Sound, uid); - Popup.PopupEntity(Loc.GetString("everything-stamp-new-stamp-added"), uid, args.User); + _popup.PopupEntity(Loc.GetString("everything-stamp-new-stamp-added"), uid, args.User); } else { - Popup.PopupEntity(Loc.GetString("everything-stamp-new-stamp-already-added"), uid, args.User); + _popup.PopupEntity(Loc.GetString("everything-stamp-new-stamp-already-added"), uid, args.User); } } private static StampDisplayInfo GetStampInfo(StampComponent stamp) diff --git a/Content.Server/Imperial/ExplosiveClothing/ClothingFactionExplosionSystem.cs b/Content.Server/Imperial/ExplosiveClothing/ClothingFactionExplosionSystem.cs index a72fbd77acf..c7db8f74778 100644 --- a/Content.Server/Imperial/ExplosiveClothing/ClothingFactionExplosionSystem.cs +++ b/Content.Server/Imperial/ExplosiveClothing/ClothingFactionExplosionSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Chat.Managers; using Content.Shared.NPC.Components; using Content.Shared.Hands; +using Content.Shared.Chat; namespace Content.Server.Clothing.Systems; @@ -58,8 +59,8 @@ private bool SearchFriendlyFaction(EntityUid uid, ClothingFactionExplosionCompon } private void OnGotEquipped(EntityUid uid, ClothingFactionExplosionComponent component, GotEquippedEvent args) { - component.LastUser = args.Equipee; - SearchFriendlyFaction(args.Equipee, component); + component.LastUser = args.EquipTarget; + SearchFriendlyFaction(args.EquipTarget, component); if (!component.OwnerIsFriendly) { component.TimerOn = true; diff --git a/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.ResultOn.cs b/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.ResultOn.cs index 1931abaad90..4cb84c46c72 100644 --- a/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.ResultOn.cs +++ b/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.ResultOn.cs @@ -3,13 +3,14 @@ using Content.Server.Explosion.EntitySystems; using Content.Shared.Body.Systems; using Content.Server.Imperial.ExplosiveProjectile.Components; +using Content.Shared.Gibbing; namespace Content.Server.Imperial.ExplosiveProjectile { [UsedImplicitly] public sealed class ExplosiveProjectileResultOnSystem : EntitySystem { - [Dependency] private readonly SharedBodySystem _body = default!; + [Dependency] private readonly GibbingSystem _gibbingSystem = default!; [Dependency] private readonly ExplosionSystem _explosion = default!; [Dependency] private readonly IGameTiming _timing = default!; @@ -21,7 +22,7 @@ public override void Initialize() private void GibEntity(EntityUid uid) { _explosion.QueueExplosion(uid, ExplosionSystem.DefaultExplosionPrototypeId, 1, 1, 1); - _body.GibBody(uid, splatModifier: 5f); + _gibbingSystem.Gib(uid); RemComp(uid); } private void GetDelayTime(EntityUid uid, ExplosiveProjectileResultOnComponent component, ComponentStartup args) diff --git a/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.cs b/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.cs index 207bf4d3619..65724d748c6 100644 --- a/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.cs +++ b/Content.Server/Imperial/ExplosiveProjectile/ExplosiveProjectileSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Standing; using Content.Shared.StatusEffect; using Content.Server.Imperial.ExplosiveProjectile.Components; +using Content.Shared.Body; namespace Content.Server.Imperial.ExplosiveProjectile { diff --git a/Content.Server/Imperial/FactionChange/FactionChangeCommand.cs b/Content.Server/Imperial/FactionChange/FactionChangeCommand.cs index 385647eca1d..b151ad13fa0 100644 --- a/Content.Server/Imperial/FactionChange/FactionChangeCommand.cs +++ b/Content.Server/Imperial/FactionChange/FactionChangeCommand.cs @@ -17,7 +17,7 @@ public sealed class FactionChangeCommand : LocalizedCommands public override string Command => "changefaction"; public override string Description => Loc.GetString("changefaction-desc"); - public string Help => Loc.GetString("changefaction-help"); + public override string Help => Loc.GetString("changefaction-help"); public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) { diff --git a/Content.Server/Imperial/GasMaskFullProtection/GasMaskFullProtectionSystem.cs b/Content.Server/Imperial/GasMaskFullProtection/GasMaskFullProtectionSystem.cs index a1da43f3f1b..3e653c4c05e 100644 --- a/Content.Server/Imperial/GasMaskFullProtection/GasMaskFullProtectionSystem.cs +++ b/Content.Server/Imperial/GasMaskFullProtection/GasMaskFullProtectionSystem.cs @@ -1,6 +1,6 @@ using System.Linq; using Content.Server.Radiation.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Inventory.Events; @@ -21,18 +21,18 @@ public override void Initialize() private void OnEquipee(EntityUid uid, GasMaskFullProtectionComponent component, GotEquippedEvent args) { - EnsureComp(args.Equipee).GasMasks.Add(uid); + EnsureComp(args.EquipTarget).GasMasks.Add(uid); } private void OnUnequipee(EntityUid uid, GasMaskFullProtectionComponent component, GotUnequippedEvent args) { - if (!TryComp(args.Equipee, out var gasMaskFullProtectionUserComponent)) return; + if (!TryComp(args.EquipTarget, out var gasMaskFullProtectionUserComponent)) return; gasMaskFullProtectionUserComponent.GasMasks.Remove(uid); if (gasMaskFullProtectionUserComponent.GasMasks.Any()) return; - RemComp(args.Equipee); + RemComp(args.EquipTarget); } private void OnBeforeDamageChanged(EntityUid uid, GasMaskFullProtectionUserComponent component, ref BeforeDamageChangedEvent args) diff --git a/Content.Server/Imperial/Gibtonite/GibtoniteGatherProtectionSystem.cs b/Content.Server/Imperial/Gibtonite/GibtoniteGatherProtectionSystem.cs new file mode 100644 index 00000000000..7c8c287cff8 --- /dev/null +++ b/Content.Server/Imperial/Gibtonite/GibtoniteGatherProtectionSystem.cs @@ -0,0 +1,35 @@ +using Content.Server.Gatherable.Components; +using Content.Shared.Destructible; +using Content.Shared.Trigger.Components; +using Content.Shared.Trigger.Components.Effects; + +namespace Content.Server.Imperial.Gibtonite; + +/// +/// Imperial added Gatherable to AsteroidRock for plasma cutters. Gibtonite inherits it and +/// would be one-shot deleted on hit; cancel that path so the fuse can prime from damage. +/// Explosions still delete via QueueDel, not DestroyEntity. +/// +public sealed class GibtoniteGatherProtectionSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGatherableMapInit); + SubscribeLocalEvent(OnDestructionAttempt); + } + + private void OnGatherableMapInit(Entity ent, ref MapInitEvent args) + { + if (!HasComp(ent) || !HasComp(ent)) + return; + + EnsureComp(ent); + } + + private void OnDestructionAttempt(Entity ent, ref DestructionAttemptEvent args) + { + args.Cancel(); + } +} diff --git a/Content.Server/Imperial/Gibtonite/PreventInstantGatherComponent.cs b/Content.Server/Imperial/Gibtonite/PreventInstantGatherComponent.cs new file mode 100644 index 00000000000..208f423e96f --- /dev/null +++ b/Content.Server/Imperial/Gibtonite/PreventInstantGatherComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Imperial.Gibtonite; + +/// +/// Blocks DestroyEntity so Gatherable cannot one-shot delete the entity. +/// Gibtonite must take damage and prime its fuse instead. +/// +[RegisterComponent] +[Access(typeof(GibtoniteGatherProtectionSystem))] +public sealed partial class PreventInstantGatherComponent : Component; diff --git a/Content.Server/Imperial/HandledCuffer/HandledCuffer.cs b/Content.Server/Imperial/HandledCuffer/HandledCuffer.cs index 282a09a97c7..cff4a21e571 100644 --- a/Content.Server/Imperial/HandledCuffer/HandledCuffer.cs +++ b/Content.Server/Imperial/HandledCuffer/HandledCuffer.cs @@ -24,21 +24,21 @@ private void OnInteract(EntityUid uid, HandledCufferComponent component, AfterIn if (args.Target is not { Valid: true } target) return; - if(!HasComp(target) && !HasComp(target)) + if (!HasComp(target) && !HasComp(target)) return; if (!args.CanReach) return; - if(!TryComp(args.Target, out var cuffableComp)) + if (!TryComp(args.Target, out var cuffableComp)) return; - if(!cuffableComp.CanStillInteract) + if (!cuffableComp.CanStillInteract) return; - var cuffs = EntityManager.SpawnEntity(component.SpawnedPrototype, Transform(uid).Coordinates); + var cuffs = Spawn(component.SpawnedPrototype, Transform(uid).Coordinates); - if(!TryComp(cuffs, out var cuffsComp)) + if (!TryComp(cuffs, out var cuffsComp)) return; _cuffs.TryAddNewCuffs(target, args.User, cuffs, cuffableComp); diff --git a/Content.Server/Imperial/Helpers/EnergyCoreHelpers/CoreSearchSystem.cs b/Content.Server/Imperial/Helpers/EnergyCoreHelpers/CoreSearchSystem.cs new file mode 100644 index 00000000000..84ecd234774 --- /dev/null +++ b/Content.Server/Imperial/Helpers/EnergyCoreHelpers/CoreSearchSystem.cs @@ -0,0 +1,114 @@ +using System.Linq; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Content.Shared.Imperial.EnergyCore.Components; +using Content.Server.Imperial.EnergyCore.Components; + +namespace Content.Server.Imperial.EnergyCore.Helpers; +public sealed class CoreSearchSystem : EntitySystem +{ + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + public EntityUid? FindNearestEnergyCore(EntityUid core, float distance) + { + var transformCompConsole = Transform(core); + var mapId = transformCompConsole.MapID; + var pos = _transformSystem.GetMapCoordinates(transformCompConsole).Position; + + EntityUid? nearest = null; + var minDist = distance; + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out _, out var transComp)) + { + if (transComp.MapID != mapId) + continue; + + var corepos = _transformSystem.GetMapCoordinates(uid).Position; + var dist = (corepos - pos).LengthSquared(); + if (dist > minDist) + continue; + + minDist = dist; + nearest = uid; + } + return nearest; + } + public EntityUid? FindNearestProtocolTerminal(EntityUid terminal, float distance) + { + var transformCompConsole = Transform(terminal); + var mapId = transformCompConsole.MapID; + var pos = _transformSystem.GetMapCoordinates(transformCompConsole).Position; + + EntityUid? nearest = null; + var minDist = distance; + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out _, out var transComp)) + { + if (transComp.MapID != mapId) + continue; + + var corepos = _transformSystem.GetMapCoordinates(uid).Position; + var dist = (corepos - pos).LengthSquared(); + if (dist > minDist) + continue; + + minDist = dist; + nearest = uid; + } + return nearest; + } + // Поиск по хэшу + public EntityUid? FindNearestEnergyCore(EntityUid targetEntity, HashSet entities, float maxDistance) + { + if (!entities.Any()) + return null; + + var xformQuery = EntityManager.System(); + var targetXform = Transform(targetEntity); + var targetCoords = targetXform.Coordinates; + + EntityUid? closest = null; + var minDistance = maxDistance; + + foreach (var entity in entities) + { + var entityXform = Transform(entity); + var distance = (entityXform.Coordinates.Position - targetCoords.Position).Length(); + + if (distance < minDistance) + { + minDistance = distance; + closest = entity; + } + } + + return closest; + } + public EntityUid? FindNearestProtocolTerminal(EntityUid targetEntity, HashSet entities, float maxDistance) + { + if (!entities.Any()) + return null; + + var xformQuery = EntityManager.System(); + var targetXform = Transform(targetEntity); + var targetCoords = targetXform.Coordinates; + + EntityUid? closest = null; + var minDistance = maxDistance; + + foreach (var entity in entities) + { + var entityXform = Transform(entity); + var distance = (entityXform.Coordinates.Position - targetCoords.Position).Length(); + + if (distance < minDistance) + { + minDistance = distance; + closest = entity; + } + } + + return closest; + } +} diff --git a/Content.Server/Imperial/ImperialBorgs/BorgHypoSystem.cs b/Content.Server/Imperial/ImperialBorgs/BorgHypoSystem.cs index b9142986134..56928b42015 100644 --- a/Content.Server/Imperial/ImperialBorgs/BorgHypoSystem.cs +++ b/Content.Server/Imperial/ImperialBorgs/BorgHypoSystem.cs @@ -24,7 +24,7 @@ public override void Initialize() SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnReagentAction); SubscribeNetworkEvent(OnReagentChange); - SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnUseInHand, before: [ typeof(InjectorSystem) ] ); } private void OnGetActions(EntityUid uid, BorgHypoComponent component, GetItemActionsEvent args) @@ -127,7 +127,8 @@ private void SwitchReagent(EntityUid uid, BorgHypoComponent component, ReagentPr { return; } - solution.Value.Comp.Solution.RemoveAllSolution(); + + _solutionSystem.RemoveAllSolution(solution.Value); var generated = solutionRegenerationComponent.Generated; diff --git a/Content.Server/Imperial/ImperialBorgs/MimicrySystem.cs b/Content.Server/Imperial/ImperialBorgs/MimicrySystem.cs index a55eddb0f49..d57c4e2693c 100644 --- a/Content.Server/Imperial/ImperialBorgs/MimicrySystem.cs +++ b/Content.Server/Imperial/ImperialBorgs/MimicrySystem.cs @@ -1,9 +1,6 @@ using Content.Shared.Borgs; using Robust.Server.GameObjects; -using Robust.Shared.Prototypes; using Content.Server.Actions; -using System; -using Content.Shared.Actions; using Content.Shared.Popups; using Content.Shared.Mobs; using Robust.Shared.Player; @@ -12,8 +9,6 @@ namespace Content.Server.Borgs { public sealed class MimicrySystem : EntitySystem { - [Dependency] private readonly IComponentFactory _factory = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; diff --git a/Content.Server/Imperial/ImperialLightning/Commands/SetNonCancelableLightningTargetCommand.cs b/Content.Server/Imperial/ImperialLightning/Commands/SetNonCancelableLightningTargetCommand.cs index d402ed2ef7d..fbb2f12d3c7 100644 --- a/Content.Server/Imperial/ImperialLightning/Commands/SetNonCancelableLightningTargetCommand.cs +++ b/Content.Server/Imperial/ImperialLightning/Commands/SetNonCancelableLightningTargetCommand.cs @@ -6,6 +6,7 @@ using Content.Shared.Imperial.ImperialStore; using Content.Shared.Damage; using Content.Shared.Mobs.Components; +using Content.Shared.Damage.Components; namespace Content.Server.Imperial.ImperialLightning; diff --git a/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerSpeciesCondition.cs b/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerSpeciesCondition.cs index d129db4dac5..1b20df4258a 100644 --- a/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerSpeciesCondition.cs +++ b/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerSpeciesCondition.cs @@ -27,7 +27,7 @@ public override bool Condition(ImperialListingConditionArgs args) { var ent = args.EntityManager; - if (!ent.TryGetComponent(args.Buyer, out var appearance)) + if (!ent.TryGetComponent(args.Buyer, out var appearance)) return true; // inanimate or non-humanoid entities should be handled elsewhere, main example being surplus crates if (Blacklist != null) diff --git a/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerWhitelistCondition.cs b/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerWhitelistCondition.cs index b54653053da..56ffaf8925d 100644 --- a/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerWhitelistCondition.cs +++ b/Content.Server/Imperial/ImperialStore/Conditions/ImperialBuyerWhitelistCondition.cs @@ -26,7 +26,7 @@ public override bool Condition(ImperialListingConditionArgs args) var whitelistSystem = ent.System(); if (whitelistSystem.IsWhitelistFail(Whitelist, args.Buyer) || - whitelistSystem.IsBlacklistPass(Blacklist, args.Buyer)) + whitelistSystem.IsWhitelistPass(Blacklist, args.Buyer)) return false; return true; diff --git a/Content.Server/Imperial/ImperialStore/Conditions/ImperialStoreWhitelistCondition.cs b/Content.Server/Imperial/ImperialStore/Conditions/ImperialStoreWhitelistCondition.cs index 9cbf5141573..3b9ab75b7f7 100644 --- a/Content.Server/Imperial/ImperialStore/Conditions/ImperialStoreWhitelistCondition.cs +++ b/Content.Server/Imperial/ImperialStore/Conditions/ImperialStoreWhitelistCondition.cs @@ -11,15 +11,16 @@ public sealed partial class ImperialStoreWhitelistCondition : ImperialListingCon /// /// A whitelist of tags or components. /// - [DataField("whitelist")] + [DataField] public EntityWhitelist? Whitelist; /// /// A blacklist of tags or components. /// - [DataField("blacklist")] + [DataField] public EntityWhitelist? Blacklist; + public override bool Condition(ImperialListingConditionArgs args) { if (args.StoreEntity == null) @@ -29,7 +30,7 @@ public override bool Condition(ImperialListingConditionArgs args) var whitelistSystem = ent.System(); if (whitelistSystem.IsWhitelistFail(Whitelist, args.StoreEntity.Value) || - whitelistSystem.IsBlacklistPass(Blacklist, args.StoreEntity.Value)) + whitelistSystem.IsWhitelistPass(Blacklist, args.StoreEntity.Value)) return false; return true; diff --git a/Content.Server/Imperial/ImperialStore/ImperialStoreSystem.Ui.cs b/Content.Server/Imperial/ImperialStore/ImperialStoreSystem.Ui.cs index 590d2674490..a91d27f6574 100644 --- a/Content.Server/Imperial/ImperialStore/ImperialStoreSystem.Ui.cs +++ b/Content.Server/Imperial/ImperialStore/ImperialStoreSystem.Ui.cs @@ -314,7 +314,7 @@ private void OnRequestWithdraw(EntityUid uid, ImperialStoreComponent component, { var cashId = proto.Cash[value]; var amountToSpawn = (int)MathF.Floor((float)(amountRemaining / value)); - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition((EntProtoId)cashId, amountToSpawn, coordinates); _hands.PickupOrDrop(buyer, ents.First()); amountRemaining -= value * amountToSpawn; @@ -355,7 +355,7 @@ private void OnRequestRefund(EntityUid uid, ImperialStoreComponent component, Im if (action != null) _actionContainer.RemoveAction((action.Value.Owner, action.Value.Comp)); - EntityManager.DeleteEntity(purchase); + Del(purchase); } component.BoughtEntities.Clear(); diff --git a/Content.Server/Imperial/KAKTYC/TitorClock/TitorClockComponent.cs b/Content.Server/Imperial/KAKTYC/TitorClock/TitorClockComponent.cs new file mode 100644 index 00000000000..cf1d4090129 --- /dev/null +++ b/Content.Server/Imperial/KAKTYC/TitorClock/TitorClockComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Server.Imperial.KAKTYC.TitorClock; + +[RegisterComponent] +[Access(typeof(TitorClockSystem))] +public sealed partial class TitorClockComponent : Component +{ + [DataField] + public float TitorNumber = 0; + [DataField] + public float UniversalNumber = 1; + [DataField] + public float CoefficentNumber = 0.7f; + [DataField] + public float CoefficentNumberTwo = 1; +} diff --git a/Content.Server/Imperial/KAKTYC/TitorClock/TitorClocksSystem.cs b/Content.Server/Imperial/KAKTYC/TitorClock/TitorClocksSystem.cs new file mode 100644 index 00000000000..010b513f970 --- /dev/null +++ b/Content.Server/Imperial/KAKTYC/TitorClock/TitorClocksSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Examine; + +namespace Content.Server.Imperial.KAKTYC.TitorClock; + +public sealed class TitorClockSystem : EntitySystem +{ + [Dependency] private readonly SharedTransformSystem _transform = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(GeneratorExamined); + } + private void GeneratorExamined(EntityUid uid, TitorClockComponent component, ExaminedEvent args) + { + var mapID = (float)_transform.GetMapId(uid); + component.UniversalNumber = mapID * component.CoefficentNumberTwo; + var number = component.UniversalNumber / mapID * component.CoefficentNumber; + component.TitorNumber = number; + args.PushMarkup(Loc.GetString($"titor-clock-line", ("number", component.TitorNumber))); + } +} diff --git a/Content.Server/Imperial/Lavaland/MaterialEnergy/MaterialEnergySystem.cs b/Content.Server/Imperial/Lavaland/MaterialEnergy/MaterialEnergySystem.cs index e64997ffb3a..d5d1693e934 100644 --- a/Content.Server/Imperial/Lavaland/MaterialEnergy/MaterialEnergySystem.cs +++ b/Content.Server/Imperial/Lavaland/MaterialEnergy/MaterialEnergySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Interaction; using Content.Shared.Materials; using Robust.Shared.Audio.Systems; +using Content.Shared.Power.Components; namespace Content.Server.Imperial.Lavaland.MaterialEnergy { @@ -55,7 +56,7 @@ private bool TryAddBatteryCharge(EntityUid batteryUid, EntityUid materialUid, in if (!TryComp(batteryUid, out var battery)) return false; - var chargeNeeded = battery.MaxCharge - battery.CurrentCharge; + var chargeNeeded = battery.MaxCharge - battery.LastCharge; if (chargeNeeded <= 0) return false; @@ -66,7 +67,7 @@ private bool TryAddBatteryCharge(EntityUid batteryUid, EntityUid materialUid, in return false; // Adding energy - _batterySystem.SetCharge(batteryUid, battery.CurrentCharge + materialToConsume, battery); + _batterySystem.SetCharge((batteryUid, battery), battery.LastCharge + materialToConsume); // Remove the used material var itemsToConsume = (int)Math.Ceiling((float)materialToConsume / materialPerItem); diff --git a/Content.Server/Imperial/Medieval/Achievements/AchievementSystem.cs b/Content.Server/Imperial/Medieval/Achievements/AchievementSystem.cs index 771041a035d..cd3b4ebfd14 100644 --- a/Content.Server/Imperial/Medieval/Achievements/AchievementSystem.cs +++ b/Content.Server/Imperial/Medieval/Achievements/AchievementSystem.cs @@ -74,7 +74,7 @@ private async void OnPlayerChange(object? sender, SessionStatusEventArgs args) } } - TryGrantAchievement(guid, AchievementFirstJoin, args.Session); + _ = TryGrantAchievement(guid, AchievementFirstJoin, args.Session); break; case SessionStatus.Disconnected: diff --git a/Content.Server/Imperial/Medieval/Achievements/Jobs/JobAchievementManager.cs b/Content.Server/Imperial/Medieval/Achievements/Jobs/JobAchievementManager.cs index 8a2a519d3d2..94c7ffcd6b4 100644 --- a/Content.Server/Imperial/Medieval/Achievements/Jobs/JobAchievementManager.cs +++ b/Content.Server/Imperial/Medieval/Achievements/Jobs/JobAchievementManager.cs @@ -8,7 +8,6 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Serilog; namespace Content.Server.Imperial.Medieval.Achievements.Jobs; diff --git a/Content.Server/Imperial/Medieval/Administration/Commands/NrpPardonCommand.cs b/Content.Server/Imperial/Medieval/Administration/Commands/NrpPardonCommand.cs index f78445709a9..8ef95199066 100644 --- a/Content.Server/Imperial/Medieval/Administration/Commands/NrpPardonCommand.cs +++ b/Content.Server/Imperial/Medieval/Administration/Commands/NrpPardonCommand.cs @@ -38,7 +38,6 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) } string target; - string reason; switch (args.Length) { diff --git a/Content.Server/Imperial/Medieval/Administration/Commands/NrpWarnCommand.cs b/Content.Server/Imperial/Medieval/Administration/Commands/NrpWarnCommand.cs index 62e588dc483..6c02e1ee050 100644 --- a/Content.Server/Imperial/Medieval/Administration/Commands/NrpWarnCommand.cs +++ b/Content.Server/Imperial/Medieval/Administration/Commands/NrpWarnCommand.cs @@ -1,4 +1,4 @@ -using Content.Server.Administration.Logs; +using Content.Server.Administration.Logs; using Content.Server.EUI; using Content.Shared.Administration; using Robust.Shared.Console; @@ -70,6 +70,6 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) await _nrpSystem.AddPlayerNrpViolation(targetUid); var violations = await _nrpSystem.GetPlayerNrpViolations(targetUid, 3); _nrpSystem.OnViolation(targetUid, targetName, reason, violations, player.UserId); - _nrpSystem.AddResolveToStats(player.Name, true, located.UserId); + _ = _nrpSystem.AddResolveToStats(player.Name, true, located.UserId); } } diff --git a/Content.Server/Imperial/Medieval/Administration/Commands/OpenNrpPanelCommand.cs b/Content.Server/Imperial/Medieval/Administration/Commands/OpenNrpPanelCommand.cs index c903ca91216..6e016107ae4 100644 --- a/Content.Server/Imperial/Medieval/Administration/Commands/OpenNrpPanelCommand.cs +++ b/Content.Server/Imperial/Medieval/Administration/Commands/OpenNrpPanelCommand.cs @@ -12,7 +12,6 @@ using Content.Server.Imperial.Medieval.Administration.Nrp; using System.Linq; using Content.Server.Administration.Managers; -using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Imperial.Medieval.Administration.Nrp; diff --git a/Content.Server/Imperial/Medieval/Administration/Nrp/NrpMessagesSystem.cs b/Content.Server/Imperial/Medieval/Administration/Nrp/NrpMessagesSystem.cs index 68da595dac3..b927d21f10d 100644 --- a/Content.Server/Imperial/Medieval/Administration/Nrp/NrpMessagesSystem.cs +++ b/Content.Server/Imperial/Medieval/Administration/Nrp/NrpMessagesSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Eventing.Reader; +using System.Diagnostics.Eventing.Reader; using System.Linq; using System.Text.RegularExpressions; using Content.Server.Administration.Managers; @@ -28,6 +28,7 @@ using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Roles.Jobs; +using Content.Shared.Chat; namespace Content.Server.Imperial.Medieval.Administration.Nrp; @@ -256,6 +257,18 @@ private async void _onViolation(int violationCount, NetUserId targetId, NetUserI } } + private uint GetBanMinutesByViolationCount(int violationCount) + { + return violationCount switch + { + 3 => 5, // 5 минута + 4 => 10, // 10 минут + 5 => 30, // 30 минут + >= 6 => 60, // 1 час (максимум) + _ => 1 // На всякий случай + }; + } + public override void Initialize() @@ -349,7 +362,7 @@ public Dictionary GetBannedWords(string input, Dictionary bannedWords, string openingTag, string closingTag) + private string WrapBannedWordsInTag(string input, Dictionary bannedWords, string openingTag = "[color=red]", string closingTag = "[/color]") { if (string.IsNullOrEmpty(input) || bannedWords.Count == 0) return input; @@ -373,11 +386,10 @@ private string WrapBannedWordsInTag(string input, Dictionary banne if (patterns.Any()) { var combinedPattern = string.Join("|", patterns); - result = Regex.Replace( + var regex = new Regex(combinedPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + result = regex.Replace( result, - combinedPattern, - match => $"{openingTag}{match.Value}{closingTag}", - RegexOptions.IgnoreCase); + match => $"{openingTag}{match.Value}{closingTag}"); } return result; diff --git a/Content.Server/Imperial/Medieval/Administration/Nrp/NrpPanelEui.cs b/Content.Server/Imperial/Medieval/Administration/Nrp/NrpPanelEui.cs index 798a854d2cd..992169ee3d6 100644 --- a/Content.Server/Imperial/Medieval/Administration/Nrp/NrpPanelEui.cs +++ b/Content.Server/Imperial/Medieval/Administration/Nrp/NrpPanelEui.cs @@ -70,8 +70,11 @@ public override async void HandleMessage(EuiMessageBase msg) SendMessage(new NrpMessagesResponse(messages)); break; case ResolveNrpMessageMsg resolve: - if (!resolve.Message.TryResolve()) + if (!_nrpSystem.ContainsMessage(resolve.Message)) + { + SendMessage(new RemoveNrpMessageMsg(resolve.Message)); return; + } var isNrp = resolve.IsNrp; if (isNrp) @@ -82,7 +85,7 @@ public override async void HandleMessage(EuiMessageBase msg) _nrpSystem.OnViolation(resolve.Message, violations, Player.UserId); } _nrpSystem.RemoveMessage(resolve.Message); - _nrpSystem.AddResolveToStats(Player.Name, !isNrp, Player.UserId); + _ = _nrpSystem.AddResolveToStats(Player.Name, !isNrp, Player.UserId); break; case NrpStatsRequest: var roundStats = _nrpSystem.GetRoundStats(); diff --git a/Content.Server/Imperial/Medieval/Barrier/MagicBarrierPointSystem.cs b/Content.Server/Imperial/Medieval/Barrier/MagicBarrierPointSystem.cs index 1ef3da9d9c1..d913372bb64 100644 --- a/Content.Server/Imperial/Medieval/Barrier/MagicBarrierPointSystem.cs +++ b/Content.Server/Imperial/Medieval/Barrier/MagicBarrierPointSystem.cs @@ -23,6 +23,7 @@ using Content.Server.Imperial.Medieval.GameTicking.Rules; using Content.Shared.GameTicking; using Content.Server.Cult.Components; +using Content.Shared.Damage.Systems; namespace Content.Server.MagicBarrier { @@ -424,7 +425,6 @@ public override void Update(float frameTime) // var choosenSpawner = _random.Pick(cursespawners); // var cursexform = Transform(choosenSpawner.Owner); // var cursecoords = cursexform.Coordinates; - // Spawn("MedievalSpawnNecroSenderPreset", cursecoords); // _chat.DispatchGlobalAnnouncement("Посланник темного повелителя замечен на этих землях.", playSound: true, colorOverride: Color.DeepPink, sender: "Барьер"); //} @@ -435,8 +435,6 @@ public override void Update(float frameTime) // var cursexform = Transform(choosenSpawner.Owner); // var cursecoords = cursexform.Coordinates; // for (int i = 0; i < 100; i++) - // Spawn("MedievalSpawnNecroFighterPreset", cursecoords); - // Spawn("MedievalSpawnNecroLeaderPreset", cursecoords); // _chat.DispatchGlobalAnnouncement("Бойтесь, ОНИ идут... Объединение - единственный шанс на спасение.", playSound: true, colorOverride: Color.DeepPink, sender: "Барьер"); //} diff --git a/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Bee.cs b/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Bee.cs index f6614515039..552803b6bc7 100644 --- a/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Bee.cs +++ b/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Bee.cs @@ -1,5 +1,6 @@ using Content.Shared.Damage; using Content.Shared.Imperial.Medieval.Bee.Components; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Bee.Systems; diff --git a/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Trap.cs b/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Trap.cs index 38afa498232..5d0141540e3 100644 --- a/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Trap.cs +++ b/Content.Server/Imperial/Medieval/Bee/Systems/MedievalBeeSystem.Trap.cs @@ -21,7 +21,7 @@ private void TrapCollide(EntityUid uid, MedievalBeeTrapComponent component, Star if (HasComp(args.OtherEntity)) return; - if (!HasComp(args.OtherEntity)) + if (!HasComp(args.OtherEntity)) return; if (!_stun.TryAddParalyzeDuration(args.OtherEntity, component.StunTime)) diff --git a/Content.Server/Imperial/Medieval/BellMap/BellMapSystems.cs b/Content.Server/Imperial/Medieval/BellMap/BellMapSystems.cs index 19a4501715f..6128d81fc09 100644 --- a/Content.Server/Imperial/Medieval/BellMap/BellMapSystems.cs +++ b/Content.Server/Imperial/Medieval/BellMap/BellMapSystems.cs @@ -1,8 +1,8 @@ -using Content.Server.Mind; using Content.Server.Popups; using Content.Shared.Coordinates; using Content.Shared.Imperial.BellMap.Components; using Content.Shared.Interaction; +using Content.Shared.Objectives.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -12,7 +12,7 @@ namespace Content.Server.Imperial.BellMap.Systems; public sealed partial class BellMapSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly MindSystem _mindSystem = default!; + [Dependency] private readonly TargetSystem _targetSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; @@ -37,7 +37,7 @@ public void OnUse(EntityUid uid, BellMapComponent comp, InteractHandEvent ev) } var mapId = _transformSystem.GetMapId(ev.Target); - var all = _mindSystem.GetAliveHumans(); + var all = _targetSystem.GetAliveHumans(); foreach (var mind in all) { if (mind.Comp.CurrentEntity == null) continue; // Skip if mind doesn't have an entity. diff --git a/Content.Server/Imperial/Medieval/BloodRegenBed/BloodRegenBedSystem.cs b/Content.Server/Imperial/Medieval/BloodRegenBed/BloodRegenBedSystem.cs index da0c3385a59..32c560cf735 100644 --- a/Content.Server/Imperial/Medieval/BloodRegenBed/BloodRegenBedSystem.cs +++ b/Content.Server/Imperial/Medieval/BloodRegenBed/BloodRegenBedSystem.cs @@ -76,8 +76,7 @@ public override void Update(float frameTime) { continue; } - - if (_bloodstreamSystem.GetBloodLevelPercentage(buckledEntity) >= 1.0f) + if (_bloodstreamSystem.GetBloodLevel(buckledEntity) >= 1.0f) { continue; } diff --git a/Content.Server/Imperial/Medieval/BloodSplatter/BloodSplatterOnHitSystem.cs b/Content.Server/Imperial/Medieval/BloodSplatter/BloodSplatterOnHitSystem.cs index e79397d0c4d..eba02d509d8 100644 --- a/Content.Server/Imperial/Medieval/BloodSplatter/BloodSplatterOnHitSystem.cs +++ b/Content.Server/Imperial/Medieval/BloodSplatter/BloodSplatterOnHitSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Mobs.Components; using Robust.Shared.Player; using Robust.Shared.Random; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.BloodSplatter; @@ -26,7 +27,7 @@ public override void Initialize() private void OnPlayerAttached(PlayerAttachedEvent args) { - if (HasComp(args.Entity)) + if (HasComp(args.Entity)) EnsureComp(args.Entity); } diff --git a/Content.Server/Imperial/Medieval/Boss/Components/DamageOnContactComponent.cs b/Content.Server/Imperial/Medieval/Boss/Components/DamageOnContactComponent.cs index a87cc649ef0..6b0f66e3fe2 100644 --- a/Content.Server/Imperial/Medieval/Boss/Components/DamageOnContactComponent.cs +++ b/Content.Server/Imperial/Medieval/Boss/Components/DamageOnContactComponent.cs @@ -2,8 +2,10 @@ namespace Content.Server.Imperial.Medieval.Boss; +/// +/// [RegisterComponent] -public sealed partial class DamageOnContactComponent : Component +public sealed partial class BossDamageOnContactComponent : Component { [DataField(required: true)] public DamageSpecifier Damage; diff --git a/Content.Server/Imperial/Medieval/Boss/StartTestBossfightCommand.cs b/Content.Server/Imperial/Medieval/Boss/StartTestBossfightCommand.cs index f0ffea96a13..eeeff8e0bde 100644 --- a/Content.Server/Imperial/Medieval/Boss/StartTestBossfightCommand.cs +++ b/Content.Server/Imperial/Medieval/Boss/StartTestBossfightCommand.cs @@ -19,7 +19,7 @@ public sealed class StartTestBossfightCommand : LocalizedEntityCommands public override async void Execute(IConsoleShell shell, string argStr, string[] args) { - var players = _entMan.AllEntities().Select(x => x.Owner).ToList(); + var players = _entMan.AllEntities().Select(x => x.Owner).ToList(); var boss = _entMan.AllEntities().First(); _entMan.System().StartBossfight(players, boss); diff --git a/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.Attacks.cs b/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.Attacks.cs index 8019cce7d73..dc746567143 100644 --- a/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.Attacks.cs +++ b/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.Attacks.cs @@ -22,6 +22,7 @@ using Robust.Shared.Physics.Events; using Robust.Shared.Random; using Robust.Shared.Utility; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Boss; @@ -45,7 +46,7 @@ private void InitializeAttacks() SubscribeLocalEvent(OnHealInit); SubscribeLocalEvent(OnBossHealDoAfter); SubscribeLocalEvent(OnAwakeSpawn); - SubscribeLocalEvent(OnDamageOnContactCollide); + SubscribeLocalEvent(OnDamageOnContactCollide); SubscribeLocalEvent(OnChargingStartup); SubscribeLocalEvent(OnChargingDoAfter); SubscribeLocalEvent(OnBHellInit); @@ -113,7 +114,7 @@ private void OnBossHealDoAfter(EntityUid uid, TimedBossHealComponent component, private void OnAwakeSpawn(EntityUid uid, PhysAwakeOnSpawnComponent component, ComponentInit args) => _physics.WakeBody(uid); - private void OnDamageOnContactCollide(EntityUid uid, DamageOnContactComponent component, ref StartCollideEvent args) + private void OnDamageOnContactCollide(EntityUid uid, BossDamageOnContactComponent component, ref StartCollideEvent args) => _damage.TryChangeDamage(args.OtherEntity, component.Damage); private void OnChargingStartup(EntityUid uid, ChargingRuneExplosionComponent component, ComponentInit args) @@ -365,7 +366,7 @@ private void UpdateRunes() comp.NextCheck = _timing.CurTime + TimeSpan.FromSeconds(1f); var runes = EntityManager.AllEntities().Where(x => TryComp(x, out var attack) && attack.Boss == uid); - var activeRunes = runes.Where(x => _physics.GetContactingEntities(x).Where(y => HasComp(y)).ToList().Any()); + var activeRunes = runes.Where(x => _physics.GetContactingEntities(x).Where(y => HasComp(y)).ToList().Any()); if (activeRunes.Count() >= runes.Count()) { @@ -403,7 +404,7 @@ private void UpdateBHell() var coords = Transform(uid).Coordinates; var targetCoords = new EntityCoordinates(coords.EntityId, coords.Position + comp.TargetPos); - _gun.AttemptShoot(uid, comp.Weapon, Comp(comp.Weapon), targetCoords); + _gun.AttemptShoot(uid, (comp.Weapon, Comp(comp.Weapon)), targetCoords); _audio.PlayPvs(Comp(comp.Weapon).SoundGunshot, uid); comp.CurShot++; comp.NextShot = _timing.CurTime + TimeSpan.FromSeconds(comp.Delay); diff --git a/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.cs b/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.cs index 6436ff54e97..42a4f0fbcc3 100644 --- a/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.cs +++ b/Content.Server/Imperial/Medieval/Boss/Systems/BossSystem.cs @@ -170,7 +170,7 @@ public void SendPlayersBack() if (list.Count == 0) { - foreach (var item in EntityManager.AllEntities().Where(x => players.Contains(x.Owner))) + foreach (var item in EntityManager.AllEntities().Where(x => players.Contains(x.Owner))) list.Add(Transform(item.Owner).Coordinates); } diff --git a/Content.Server/Imperial/Medieval/BountyBoard/BountyBoardSystem.cs b/Content.Server/Imperial/Medieval/BountyBoard/BountyBoardSystem.cs index bbb27ed8324..9cc3a2e8319 100644 --- a/Content.Server/Imperial/Medieval/BountyBoard/BountyBoardSystem.cs +++ b/Content.Server/Imperial/Medieval/BountyBoard/BountyBoardSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Server.Imperial.Medieval.Achievements; using Content.Server.Chat.Managers; using Content.Server.Mind; @@ -18,6 +18,7 @@ using Content.Shared.Storage; using Robust.Server.Player; using Robust.Shared.Collections; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -157,7 +158,7 @@ private void OnTargetInteracted(Entity ent, ref Intera var xform = Transform(args.Target); - _stackSystem.SpawnMultiple(contractComponent.CurrencyProtoId.Id, contractComponent.Payout, xform.Coordinates); + _stackSystem.SpawnMultipleAtPosition((EntProtoId)contractComponent.CurrencyProtoId.Id, contractComponent.Payout, xform.Coordinates); if (HasComp(args.User)) { @@ -174,7 +175,7 @@ private void OnTargetInteracted(Entity ent, ref Intera EntityUid target; - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out _, out _)) { diff --git a/Content.Server/Imperial/Medieval/Cannon/CannonSystem.cs b/Content.Server/Imperial/Medieval/Cannon/CannonSystem.cs index c21a4baee82..1ae4d555915 100644 --- a/Content.Server/Imperial/Medieval/Cannon/CannonSystem.cs +++ b/Content.Server/Imperial/Medieval/Cannon/CannonSystem.cs @@ -341,7 +341,7 @@ private bool TryFire(Entity ent, EntityUid? user) var fromCoordinates = Transform(ent.Owner).Coordinates; var toCoordinates = new EntityCoordinates(ent.Owner, gun.DefaultDirection); - _gunSystem.Shoot(ent.Owner, gun, payload, fromCoordinates, toCoordinates, out _, user); + _gunSystem.Shoot((ent.Owner, gun), payload, fromCoordinates, toCoordinates, out _, user); SpawnShotSmoke(ent, gun); // GunSystem uses PlayPredicted for gunshots, which excludes the initiating user on the server. diff --git a/Content.Server/Imperial/Medieval/Chemistry/MortarSystem.cs b/Content.Server/Imperial/Medieval/Chemistry/MortarSystem.cs index 17f2aa895a2..7c648e1b1aa 100644 --- a/Content.Server/Imperial/Medieval/Chemistry/MortarSystem.cs +++ b/Content.Server/Imperial/Medieval/Chemistry/MortarSystem.cs @@ -46,9 +46,9 @@ public void Finished(EntityUid uid, MortarComponent component, MortarDoAfterEven Solution transfer; if (extractable.JuiceSolution == null) { - if (extractable.GrindableSolution == null) + if (extractable.GrindableSolutionName == null) continue; - if (!_solution.TryGetSolution(item, extractable.GrindableSolution, out _, out var itemsolution)) + if (!_solution.TryGetSolution(item, extractable.GrindableSolutionName, out _, out var itemsolution)) continue; transfer = itemsolution; } diff --git a/Content.Server/Imperial/Medieval/Chemistry/Randomization/ChemistryRandomizationSystem.cs b/Content.Server/Imperial/Medieval/Chemistry/Randomization/ChemistryRandomizationSystem.cs index dd0fdebf24c..6595ba7967c 100644 --- a/Content.Server/Imperial/Medieval/Chemistry/Randomization/ChemistryRandomizationSystem.cs +++ b/Content.Server/Imperial/Medieval/Chemistry/Randomization/ChemistryRandomizationSystem.cs @@ -24,6 +24,7 @@ public sealed partial class ChemistryRandomizationSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly MetaDataSystem _meta = default!; [Dependency] private readonly IEntitySystemManager _ent = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; public override void Initialize() { @@ -48,7 +49,7 @@ public void OnExamine(EntityUid uid, SolutionContainerManagerComponent component var addedeffects = new List(); foreach (var key in component.Containers) { - if (!contman.TryGetContainer($"solution@{key}", out var container)) + if (!_containers.TryGetContainer(uid, $"solution@{key}", out var container, contman)) continue; var solution = EnsureComp(((ContainerSlot)container).ContainedEntity!.Value).Solution; foreach (var reagent in solution.Contents) @@ -60,9 +61,9 @@ public void OnExamine(EntityUid uid, SolutionContainerManagerComponent component if (proto.Metabolisms == null) continue; - foreach (var (_, effectentry) in proto.Metabolisms) + foreach (var (_, effectentry) in proto.Metabolisms.Metabolisms) { - var entry = effectentry.MakeGuideEntry(_prototype, _ent); + var entry = effectentry.MakeGuideEntry(_prototype, _ent, proto); var effects = string.Empty; foreach (var effect in entry.EffectDescriptions) { diff --git a/Content.Server/Imperial/Medieval/CombatStance/CombatStancePointTestSystem.cs b/Content.Server/Imperial/Medieval/CombatStance/CombatStancePointTestSystem.cs index 8d60a1ec04a..3511ff4b00b 100644 --- a/Content.Server/Imperial/Medieval/CombatStance/CombatStancePointTestSystem.cs +++ b/Content.Server/Imperial/Medieval/CombatStance/CombatStancePointTestSystem.cs @@ -26,6 +26,7 @@ using Content.Shared.Damage.Events; using Content.Shared.Explosion; using Content.Server.Popups; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.CombatStance; @@ -92,17 +93,15 @@ private void ModifyDamage(EntityUid uid, CombatStanceComponent component, Damage { if (!component.HasDefence) return; - var newdict = new Dictionary(); - foreach (var (key, value) in args.Damage.DamageDict) + var keys = new List>(args.Damage.DamageDict.Keys); + foreach (var key in keys) { - Console.WriteLine(value.Float()); + var value = args.Damage.DamageDict[key]; + Log.Debug(value.Float().ToString()); if (value > 0) - newdict[key] = FixedPoint2.New(value.Float() * 0.5f); - else - newdict[key] = value; - Console.WriteLine(newdict[key].Float()); + args.Damage.DamageDict[key] = FixedPoint2.New(value.Float() * 0.5f); + Log.Debug(args.Damage.DamageDict[key].Float().ToString()); } - args.Damage.DamageDict = newdict; } public void MemberRemoved(EntityUid uid, string faction, FactionMemberGroup group) { diff --git a/Content.Server/Imperial/Medieval/Courier/CourierSystem.cs b/Content.Server/Imperial/Medieval/Courier/CourierSystem.cs index b1cee9af7c0..d4d78058e2b 100644 --- a/Content.Server/Imperial/Medieval/Courier/CourierSystem.cs +++ b/Content.Server/Imperial/Medieval/Courier/CourierSystem.cs @@ -187,7 +187,7 @@ private void OnRequestWithdraw(EntityUid uid, CourierPitComponent component, Cou if (amountToSpawn <= 0) continue; - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates); if (ents.FirstOrDefault() is {} ent) _hands.PickupOrDrop(user, ent); @@ -312,33 +312,13 @@ private void OpenLetterRecipientUi(EntityUid uid, LetterComponent component, Ent private HumanoidCharacterProfile? BuildRecipientProfile(EntityUid recipient) { - if (!TryComp(recipient, out var humanoid)) + // if (!TryComp(recipient, out var humanoid)) + // return null; + // ... WithHairStyleName / WithFacialHairStyleName path ... + if (!TryComp(recipient, out var humanoid)) return null; - var appearance = new HumanoidCharacterAppearance - { - EyeColor = humanoid.EyeColor, - SkinColor = humanoid.SkinColor, - Markings = humanoid.MarkingSet.GetForwardEnumerator().ToList(), - }; - - if (humanoid.MarkingSet.TryGetCategory(MarkingCategories.Hair, out var hairMarkings) && - hairMarkings.Count > 0) - { - var hair = hairMarkings[0]; - appearance = appearance.WithHairStyleName(hair.MarkingId); - if (hair.MarkingColors.Count > 0) - appearance = appearance.WithHairColor(hair.MarkingColors[0]); - } - - if (humanoid.MarkingSet.TryGetCategory(MarkingCategories.FacialHair, out var facialHairMarkings) && - facialHairMarkings.Count > 0) - { - var facialHair = facialHairMarkings[0]; - appearance = appearance.WithFacialHairStyleName(facialHair.MarkingId); - if (facialHair.MarkingColors.Count > 0) - appearance = appearance.WithFacialHairColor(facialHair.MarkingColors[0]); - } + var appearance = HumanoidCharacterAppearance.DefaultWithSpecies(humanoid.Species, humanoid.Sex); return new HumanoidCharacterProfile() .WithCharacterAppearance(appearance) diff --git a/Content.Server/Imperial/Medieval/Courier/VeryFragileSystem.cs b/Content.Server/Imperial/Medieval/Courier/VeryFragileSystem.cs index 05e294302df..96b28ef431a 100644 --- a/Content.Server/Imperial/Medieval/Courier/VeryFragileSystem.cs +++ b/Content.Server/Imperial/Medieval/Courier/VeryFragileSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Robust.Shared.Containers; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Courier; @@ -49,7 +50,7 @@ private void OnFragileEquippedHand(Entity ent, ref GotEqui private void OnFragileEquipped(Entity ent, ref GotEquippedEvent args) { - SetCarrier(ent, args.Equipee); + SetCarrier(ent, args.EquipTarget); } private void OnFragileInsertedIntoContainer(Entity ent, ref EntGotInsertedIntoContainerMessage args) diff --git a/Content.Server/Imperial/Medieval/CritEmotes/CritEmotesSystem.cs b/Content.Server/Imperial/Medieval/CritEmotes/CritEmotesSystem.cs index 6aec0c63693..76ce633e1f0 100644 --- a/Content.Server/Imperial/Medieval/CritEmotes/CritEmotesSystem.cs +++ b/Content.Server/Imperial/Medieval/CritEmotes/CritEmotesSystem.cs @@ -5,6 +5,8 @@ using Content.Shared.Mobs.Components; using Robust.Shared.Random; using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Robust.Server.Audio; using Robust.Shared.Audio; @@ -16,6 +18,7 @@ public sealed class CritEmotesSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public override void Update(float frameTime) { @@ -28,7 +31,7 @@ public override void Update(float frameTime) { crit.NextEmoteUpdate = _gameTiming.CurTime + TimeSpan.FromSeconds(_random.NextFloat(4f, 7f)); - if (damageable.Damage.GetTotal() < crit.MinDamage || mob.CurrentState == Shared.Mobs.MobState.Dead) + if (_damageable.GetTotalDamage((uid, damageable)) < crit.MinDamage || mob.CurrentState == Shared.Mobs.MobState.Dead) continue; _chat.TryEmoteWithChat(uid, _random.Pick(crit.Emotes), ChatTransmitRange.HideChat, ignoreActionBlocker: true); @@ -37,7 +40,7 @@ public override void Update(float frameTime) { crit.NextHeartbeatUpdate = _gameTiming.CurTime + TimeSpan.FromSeconds(_random.NextFloat(1.4f, 2.1f)); - if (damageable.Damage.GetTotal() < crit.MinDamage || mob.CurrentState == Shared.Mobs.MobState.Dead) + if (_damageable.GetTotalDamage((uid, damageable)) < crit.MinDamage || mob.CurrentState == Shared.Mobs.MobState.Dead) continue; _audio.PlayGlobal(new SoundPathSpecifier("/Audio/Imperial/Medieval/heartbeat.ogg"), uid); diff --git a/Content.Server/Imperial/Medieval/Cult/Bloodspells/CultCastSystem.cs b/Content.Server/Imperial/Medieval/Cult/Bloodspells/CultCastSystem.cs index 14ad17c0da1..224f0478e32 100644 --- a/Content.Server/Imperial/Medieval/Cult/Bloodspells/CultCastSystem.cs +++ b/Content.Server/Imperial/Medieval/Cult/Bloodspells/CultCastSystem.cs @@ -17,6 +17,8 @@ using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; +using Content.Shared.Chat; namespace Content.Server.Imperial.Medieval.Cult.Bloodspells; diff --git a/Content.Server/Imperial/Medieval/Cult/Bloodspells/light/DeathCurseSystem.cs b/Content.Server/Imperial/Medieval/Cult/Bloodspells/light/DeathCurseSystem.cs index 86a05bd1b09..d2ea923f0bf 100644 --- a/Content.Server/Imperial/Medieval/Cult/Bloodspells/light/DeathCurseSystem.cs +++ b/Content.Server/Imperial/Medieval/Cult/Bloodspells/light/DeathCurseSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Mobs.Components; using Content.Shared.Popups; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Cult.Bloodspells.Light; diff --git a/Content.Server/Imperial/Medieval/Cult/CultSystem.cs b/Content.Server/Imperial/Medieval/Cult/CultSystem.cs index cb64414fb21..37f72cf943f 100644 --- a/Content.Server/Imperial/Medieval/Cult/CultSystem.cs +++ b/Content.Server/Imperial/Medieval/Cult/CultSystem.cs @@ -45,6 +45,9 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Storage; using Content.Shared.Tag; +using Content.Shared.Administration.Systems; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Cult { @@ -237,10 +240,14 @@ public override void Update(float frameTime) _damageableSystem.TryChangeDamage(cursed.Owner, cursed.LostDamage, true, false); _popupSystem.PopupEntity("Еще немного, и связь с культом разорвется. Терпеть осталось недолго.", cursed.Owner, cursed.Owner, PopupType.SmallCaution); } - if (cursed.CurseLevel > 60f && TryComp(cursed.Owner, out var damage) && damage.TotalDamage < 100 && damage.TotalDamage > 5) + if (cursed.CurseLevel > 60f && TryComp(cursed.Owner, out var damage)) { + var total = _damageableSystem.GetTotalDamage((cursed.Owner, damage)); + if (total < 100 && total > 5) + { _damageableSystem.TryChangeDamage(cursed.Owner, -cursed.RegenDamage, true, false); _popupSystem.PopupEntity("Связь с культом восстанавливает твои раны", cursed.Owner, cursed.Owner, PopupType.Small); + } } } @@ -378,8 +385,6 @@ public void OnActivated(EntityUid uid, CultCheckPictureComponent comp, ActivateI { var axform = Transform(altar.Owner); var acoords = axform.Coordinates; - - //if (!isDead) Spawn("MedievalCultCrystallRed", acoords); if (isDead && TryComp(victim, out var ssdfreeComp) && _playerManager.TryGetSessionByEntity(victim, out var session)) _ssdFreeSystem.GoToSSD(victim, session.UserId, false, ssdfreeComp); if (!isDead) _adminLog.Add(LogType.Action, LogImpact.Low, $"Кристал {Spawn("MedievalCultCrystallRed", acoords)} призван от привязки {blood.Owner} {ToPrettyString(altar.Owner):altar}"); diff --git a/Content.Server/Imperial/Medieval/DarkMage/DarkMageSystem.cs b/Content.Server/Imperial/Medieval/DarkMage/DarkMageSystem.cs index dc55c399014..9401327660d 100644 --- a/Content.Server/Imperial/Medieval/DarkMage/DarkMageSystem.cs +++ b/Content.Server/Imperial/Medieval/DarkMage/DarkMageSystem.cs @@ -132,7 +132,7 @@ public override void Update(float frameTime) var position = _transform.GetMapCoordinates(uid); var entitiesNearby = _lookup.GetEntitiesInRange(position, darkMageComponent.SearchRadius) - .Where(e => e != uid && TryComp(e, out var mindE) && mindE.HasMind && TryComp(e, out var eComp) && eComp.CurrentState == MobState.Alive && HasComp(e)) // Много + .Where(e => e != uid && TryComp(e, out var mindE) && mindE.HasMind && TryComp(e, out var eComp) && eComp.CurrentState == MobState.Alive && HasComp(e)) // Много .ToList(); if (entitiesNearby.Count >= 2 && !darkMageComponent.IsMoved) diff --git a/Content.Server/Imperial/Medieval/DoOnUse/MedievalDoAfterSystem.cs b/Content.Server/Imperial/Medieval/DoOnUse/MedievalDoAfterSystem.cs index 78140bdce0e..a797fae51e1 100644 --- a/Content.Server/Imperial/Medieval/DoOnUse/MedievalDoAfterSystem.cs +++ b/Content.Server/Imperial/Medieval/DoOnUse/MedievalDoAfterSystem.cs @@ -5,6 +5,8 @@ using Content.Shared.Imperial.Medieval.DoOnUse.DoAfter; using Content.Shared.Verbs; using Robust.Shared.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Imperial.Medieval.DoOnUse.DoAfter; diff --git a/Content.Server/Imperial/Medieval/Dungeons/SpikeTrapSystem.cs b/Content.Server/Imperial/Medieval/Dungeons/SpikeTrapSystem.cs index 6afa3dd63e0..cc994dc94fe 100644 --- a/Content.Server/Imperial/Medieval/Dungeons/SpikeTrapSystem.cs +++ b/Content.Server/Imperial/Medieval/Dungeons/SpikeTrapSystem.cs @@ -11,6 +11,7 @@ using Content.Server.MagicBarrier.Components; using Content.Server.Imperial.Medieval.GameTicking.Rules; using Content.Shared.Imperial.Medieval.GameTicking.Rules; +using Content.Shared.Damage.Systems; namespace Content.Server.MagicPotionsMaker { diff --git a/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssence.cs b/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssence.cs deleted file mode 100644 index 01441def2d3..00000000000 --- a/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssence.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Content.Server.Imperial.ImperialStore; -using Content.Server.Imperial.Medieval.Magic.BindStoreOnEquip; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Imperial.Medieval.EntityEffects; - - -public sealed partial class AddMagicEssence : EntityEffect -{ - private IRobustRandom? _random; - private ImperialStoreSystem? _storeSys; - - [DataField] - public Dictionary AddedEssences = []; - - [DataField] - public Dictionary BonusEssences = []; - - [DataField] - public float EssenceAddProbability; - - [DataField] - public float BonusAddProbability; - - - public override void Effect(EntityEffectBaseArgs args) - { - _random ??= IoCManager.Resolve(); - _storeSys ??= args.EntityManager.System(); - - if (args is not MagicEntityEffectsArgs magicEntityEffectsArgs) - return; - - var enumerator = args.EntityManager.EntityQueryEnumerator(); - - while (enumerator.MoveNext(out var spellBookUid, out var bindStoreOnEquipComponent)) - { - if (bindStoreOnEquipComponent.BindedEntity != magicEntityEffectsArgs.Performer) - continue; - - if (_random.Prob(EssenceAddProbability)) - _storeSys.TryAddCurrency(AddedEssences, spellBookUid); - - if (_random.Prob(BonusAddProbability)) - _storeSys.TryAddBonus(BonusEssences, spellBookUid); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => ""; -} diff --git a/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssenceEntityEffectSystem.cs b/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssenceEntityEffectSystem.cs new file mode 100644 index 00000000000..13b091a405a --- /dev/null +++ b/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddMagicEssenceEntityEffectSystem.cs @@ -0,0 +1,33 @@ +using Content.Server.Imperial.ImperialStore; +using Content.Server.Imperial.Medieval.Magic.BindStoreOnEquip; +using Content.Shared.EntityEffects; +using Content.Shared.Imperial.Medieval.EntityEffects; +using Robust.Shared.Random; + +namespace Content.Server.Imperial.Medieval.EntityEffects; + +public sealed partial class GAddMagicEssenceEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var storeSys = _entityManager.System(); + var enumerator = _entityManager.EntityQueryEnumerator(); + + while (enumerator.MoveNext(out var spellBookUid, out var bindStoreOnEquipComponent)) + { + if (bindStoreOnEquipComponent.BindedEntity != args.User) + continue; + + if (_random.Prob(args.Effect.EssenceAddProbability)) + storeSys.TryAddCurrency(args.Effect.AddedEssences, spellBookUid); + + if (_random.Prob(args.Effect.BonusAddProbability)) + storeSys.TryAddBonus(args.Effect.BonusEssences, spellBookUid); + + return; + } + } +} diff --git a/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddReagentToSolution.cs b/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddReagentToSolution.cs deleted file mode 100644 index 4e1e4c403d5..00000000000 --- a/Content.Server/Imperial/Medieval/EntityEffects/Effects/AddReagentToSolution.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.Body.Components; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; - -namespace Content.Server.Imperial.Medieval.EntityEffects; - - -public sealed partial class AddReagentToTarget : EntityEffect -{ - /// - /// The reagent ID to remove. Only one of this and should be active. - /// - [DataField(required: true)] - public List Reagents = new(); - - public override void Effect(EntityEffectBaseArgs args) - { - var solutionContainerSystem = args.EntityManager.System(); - - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out var bloodstreamComponent)) return; - if (!solutionContainerSystem.TryGetSolution(args.TargetEntity, bloodstreamComponent.ChemicalSolutionName, out var injectorSolution)) return; - - foreach (var reagent in Reagents) - injectorSolution.Value.Comp.Solution.AddReagent(reagent.Reagent, reagent.Quantity); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => ""; -} diff --git a/Content.Server/Imperial/Medieval/EntityEffects/license.txt b/Content.Server/Imperial/Medieval/EntityEffects/license.txt deleted file mode 100644 index 7c6998665e0..00000000000 --- a/Content.Server/Imperial/Medieval/EntityEffects/license.txt +++ /dev/null @@ -1,2 +0,0 @@ -license: This content is under ICLA. Copyright holder: orix0689 (discord) (Read more on: https://wiki.imperialspace.net/ru/icla) -copyright: "orix0689 (discord)" \ No newline at end of file diff --git a/Content.Server/Imperial/Medieval/Entry.Medieval.cs b/Content.Server/Imperial/Medieval/Entry.Medieval.cs new file mode 100644 index 00000000000..e8304685433 --- /dev/null +++ b/Content.Server/Imperial/Medieval/Entry.Medieval.cs @@ -0,0 +1,36 @@ +using Content.Server.Imperial.Medieval.Achievements.Jobs; +using Content.Server.Imperial.Medieval.Afk; +using Content.Server.Imperial.Medieval.Flavors; +using Content.Server.Imperial.Medieval.JoinQueue; +using Content.Server.Imperial.PVS; +using Content.Shared.Imperial.Medieval.Flavors; + +namespace Content.Server.Imperial.Entry; + +/// + +/// +public sealed partial class ImperialEntry +{ + private static void MedievalInit() + { + IoCManager.Resolve().Init(); + IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + } + + private static void MedievalPostInit() + { + } + + private static void MedievalIoCRegister(IDependencyCollection deps) + { + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + } +} diff --git a/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.Wanted.cs b/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.Wanted.cs index ff26324f97c..0229008c1aa 100644 --- a/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.Wanted.cs +++ b/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.Wanted.cs @@ -69,44 +69,10 @@ public void AddWanted(EntityUid uid, string job, string performer, string detail private HumanoidCharacterProfile? BuildProfile(EntityUid uid) { - if (!TryComp(uid, out var humanoid)) + if (!TryComp(uid, out var humanoid)) return null; - HumanoidCharacterAppearance hca = new(); - - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.FacialHair, out var facialHair)) - if (facialHair.TryGetValue(0, out var marking)) - { - hca = hca.WithFacialHairStyleName(marking.MarkingId); - hca = hca.WithFacialHairColor(marking.MarkingColors.First()); - } - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Hair, out var hair)) - if (hair.TryGetValue(0, out var marking)) - { - hca = hca.WithHairStyleName(marking.MarkingId); - hca = hca.WithHairColor(marking.MarkingColors.First()); - } - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Head, out var head)) - hca = hca.WithMarkings(head); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.HeadSide, out var headSide)) - hca = hca.WithMarkings(headSide); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.HeadTop, out var headTop)) - hca = hca.WithMarkings(headTop); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Snout, out var snout)) - hca = hca.WithMarkings(snout); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Chest, out var chest)) - hca = hca.WithMarkings(chest); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Arms, out var arms)) - hca = hca.WithMarkings(arms); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Legs, out var legs)) - hca = hca.WithMarkings(legs); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Tail, out var tail)) - hca = hca.WithMarkings(tail); - if (humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Overlay, out var overlay)) - hca = hca.WithMarkings(overlay); - - hca = hca.WithSkinColor(humanoid.SkinColor); - hca = hca.WithEyeColor(humanoid.EyeColor); + var hca = HumanoidCharacterAppearance.DefaultWithSpecies(humanoid.Species, humanoid.Sex); return new HumanoidCharacterProfile(). WithCharacterAppearance(hca). diff --git a/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.cs b/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.cs index f5ad4885968..827c0386652 100644 --- a/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.cs +++ b/Content.Server/Imperial/Medieval/Factions/Systems/MedievalFactionsSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Imperial.Medieval.Factions.Components; +using Content.Shared.Imperial.Medieval.Factions.Components; using Content.Shared.Actions; using Robust.Shared.Player; using Content.Server.Administration; @@ -13,6 +13,7 @@ using Content.Shared.Buckle.Components; using Content.Shared.Mobs; using Robust.Shared.Utility; +using Content.Shared.Chat; namespace Content.Server.Imperial.Medieval.Factions; @@ -42,7 +43,7 @@ public override void Initialize() public void OnCloackMessageAction(EntityUid uid, CloackMessageComponent comp, CloackMessageActionEvent args) { if (!_sharedPlayerManager.TryGetSessionByEntity(uid, out var session)) return; - _quickDialog.OpenDialog(session, "Весть", "Сообщение", (string message) => + _quickDialog.OpenDialog(session, "", "", (string message) => { var query = EntityQueryEnumerator(); while (query.MoveNext(out var cloackOwner, out var cloack)) diff --git a/Content.Server/Imperial/Medieval/Farmer/FarmerBoostSystem.cs b/Content.Server/Imperial/Medieval/Farmer/FarmerBoostSystem.cs index 235fc257cb8..4a16a065627 100644 --- a/Content.Server/Imperial/Medieval/Farmer/FarmerBoostSystem.cs +++ b/Content.Server/Imperial/Medieval/Farmer/FarmerBoostSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Nutrition.Components; using Content.Shared.StatusEffect; +using Content.Shared.Nutrition.Components; namespace Content.Server.Imperial.Medieval.Farmer; public sealed class FarmerBoostSystem : EntitySystem @@ -32,7 +33,8 @@ public override void Initialize() } private void OnMarkerInit(EntityUid uid, AddFarmerBoostOnInitComponent comp, MapInitEvent args) - => _lookup.GetEntitiesInRange(Transform(uid).Coordinates, comp.Range).ToList() + => + _lookup.GetEntitiesInRange(Transform(uid).Coordinates, comp.Range).ToList() .ForEach(x => EnsureComp(x)); private void OnContPickup(EntityUid uid, LastPickedUpContainerComponent comp, GotEquippedHandEvent args) diff --git a/Content.Server/Imperial/Medieval/Fishing/FishSystem.SizeRarity.cs b/Content.Server/Imperial/Medieval/Fishing/FishSystem.SizeRarity.cs index 4987a3f4300..1f1e47054b6 100644 --- a/Content.Server/Imperial/Medieval/Fishing/FishSystem.SizeRarity.cs +++ b/Content.Server/Imperial/Medieval/Fishing/FishSystem.SizeRarity.cs @@ -1,3 +1,4 @@ +using Content.Shared.Store.Components; using System.Numerics; using Content.Server.Construction; using Content.Server.Store.Components; diff --git a/Content.Server/Imperial/Medieval/Fishing/FishingSystem.cs b/Content.Server/Imperial/Medieval/Fishing/FishingSystem.cs index d2ff60a99e9..0c353fc53c2 100644 --- a/Content.Server/Imperial/Medieval/Fishing/FishingSystem.cs +++ b/Content.Server/Imperial/Medieval/Fishing/FishingSystem.cs @@ -28,6 +28,7 @@ using Robust.Shared.Timing; using System.Numerics; using Content.Shared.Coordinates; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Fishing; @@ -595,7 +596,7 @@ private void ConsumeBait(Entity rod) if (TryComp(baitUid, out var baitStack) && baitStack.Count != 1) { - _stack.Use(baitUid, 1, baitStack); + _stack.ReduceCount(baitUid, 1); return; } diff --git a/Content.Server/Imperial/Medieval/Flavors/ServerFlavorManager.cs b/Content.Server/Imperial/Medieval/Flavors/ServerFlavorManager.cs index f6af9d9eeb2..d9cd9279ebb 100644 --- a/Content.Server/Imperial/Medieval/Flavors/ServerFlavorManager.cs +++ b/Content.Server/Imperial/Medieval/Flavors/ServerFlavorManager.cs @@ -4,10 +4,8 @@ using Robust.Shared.Player; using Content.Shared.Imperial.Medieval.Flavors; using System.IO; -using SixLabors.ImageSharp; using Content.Server.Preferences.Managers; using Robust.Shared.Utility; -using Content.Shared.Mind.Components; using Robust.Server.Player; using Content.Shared.DetailExaminable; using Content.Shared.Players.PlayTimeTracking; @@ -45,11 +43,15 @@ public void UpdateImage(MsgUpdateFlavorImage msg) { try { - using (var img = Image.Load(stream)) - { - if (!ImageAllowed(img.Width, img.Height)) - return; - } + // validate via Robust/ImageSharp is Client-only package + // basic size gate without decoding full image metadata. + // using (var img = Image.Load(stream)) + // { + // if (!ImageAllowed(img.Width, img.Height)) + // return; + // } + if (msg.Image.Length is < 32 or > 2_000_000) + return; } catch { diff --git a/Content.Server/Imperial/Medieval/Flavors/ServerFlavorSystem.cs b/Content.Server/Imperial/Medieval/Flavors/ServerFlavorSystem.cs index 566f388daba..e9e4d49fac0 100644 --- a/Content.Server/Imperial/Medieval/Flavors/ServerFlavorSystem.cs +++ b/Content.Server/Imperial/Medieval/Flavors/ServerFlavorSystem.cs @@ -20,9 +20,14 @@ public override void Initialize() } public void CompInit(EntityUid uid, FlavorImageComponent component, ComponentInit args) { - MindAdded(uid, component, new(new(), new())); + TrySetImagePath(uid, component); } public void MindAdded(EntityUid uid, FlavorImageComponent component, MindAddedMessage args) + { + TrySetImagePath(uid, component); + } + + private void TrySetImagePath(EntityUid uid, FlavorImageComponent component) { if (component.ImagePath != null) return; diff --git a/Content.Server/Imperial/Medieval/FleeOperator/SelfHealthFractionGreaterPrecondition.cs b/Content.Server/Imperial/Medieval/FleeOperator/SelfHealthFractionGreaterPrecondition.cs index ddb8e8b4777..7f9d4fde67d 100644 --- a/Content.Server/Imperial/Medieval/FleeOperator/SelfHealthFractionGreaterPrecondition.cs +++ b/Content.Server/Imperial/Medieval/FleeOperator/SelfHealthFractionGreaterPrecondition.cs @@ -4,6 +4,7 @@ using Content.Shared.Damage.Components; using Content.Shared.Mobs.Systems; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.hl2.Mobs; @@ -42,7 +43,7 @@ public override bool IsMet(NPCBlackboard blackboard) return false; var thresholdSys = _entManager.System(); - var totalDamage = damageable.TotalDamage; + var totalDamage = _entManager.System().GetTotalDamage((owner, damageable)); if (!thresholdSys.TryGetIncapPercentage(owner, totalDamage, out var incapPct)) return false; diff --git a/Content.Server/Imperial/Medieval/Forged/ForgedStomachFilterSystem.cs b/Content.Server/Imperial/Medieval/Forged/ForgedStomachFilterSystem.cs index 9b71caf5b5f..1a1a08b3640 100644 --- a/Content.Server/Imperial/Medieval/Forged/ForgedStomachFilterSystem.cs +++ b/Content.Server/Imperial/Medieval/Forged/ForgedStomachFilterSystem.cs @@ -32,7 +32,7 @@ private void OnSolutionChanged(Entity ent, ref SolutionCo if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto)) continue; - if (proto.Metabolisms == null || !proto.Metabolisms.ContainsKey("Food")) + if (proto.Metabolisms == null || !proto.Metabolisms.Metabolisms.ContainsKey("Digestion")) { solution.RemoveReagent(reagent, quantity); wasChanged = true; diff --git a/Content.Server/Imperial/Medieval/GameTicking/Rules/FactionGoals/FactionGoalsRuleSystem.cs b/Content.Server/Imperial/Medieval/GameTicking/Rules/FactionGoals/FactionGoalsRuleSystem.cs index ceb7493d353..9c078ce20ab 100644 --- a/Content.Server/Imperial/Medieval/GameTicking/Rules/FactionGoals/FactionGoalsRuleSystem.cs +++ b/Content.Server/Imperial/Medieval/GameTicking/Rules/FactionGoals/FactionGoalsRuleSystem.cs @@ -12,6 +12,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.GameTicking.Rules; diff --git a/Content.Server/Imperial/Medieval/GameTicking/Rules/SkeletonInvasion/SkeletonInvasionRuleSystem.cs b/Content.Server/Imperial/Medieval/GameTicking/Rules/SkeletonInvasion/SkeletonInvasionRuleSystem.cs index d1ffde43c54..6db04541268 100644 --- a/Content.Server/Imperial/Medieval/GameTicking/Rules/SkeletonInvasion/SkeletonInvasionRuleSystem.cs +++ b/Content.Server/Imperial/Medieval/GameTicking/Rules/SkeletonInvasion/SkeletonInvasionRuleSystem.cs @@ -168,7 +168,7 @@ private void OnSkullStandCompleted(SkullBossStandCompletedEvent args) return; var xform = Transform(args.Stand); - var players = EntityManager.AllEntities().Where(x => !HasComp(x.Owner) && Transform(x).MapUid == Transform(args.Stand).MapUid); + var players = EntityManager.AllEntities().Where(x => !HasComp(x.Owner) && Transform(x).MapUid == Transform(args.Stand).MapUid); var bossfightPlayers = new List(); foreach (var item in players) diff --git a/Content.Server/Imperial/Medieval/GameTicking/Rules/StatCounter/RoundStatCounterRuleSystem.cs b/Content.Server/Imperial/Medieval/GameTicking/Rules/StatCounter/RoundStatCounterRuleSystem.cs index 6c532dc8f19..582d61c8b56 100644 --- a/Content.Server/Imperial/Medieval/GameTicking/Rules/StatCounter/RoundStatCounterRuleSystem.cs +++ b/Content.Server/Imperial/Medieval/GameTicking/Rules/StatCounter/RoundStatCounterRuleSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Damage; using Content.Shared.Speech; using Content.Shared.Imperial.Medieval.GameTicking.Rules; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.GameTicking.Rules; diff --git a/Content.Server/Imperial/Medieval/Illitid/IllitidSystem.cs b/Content.Server/Imperial/Medieval/Illitid/IllitidSystem.cs index 87ed407cf62..62d33285b78 100644 --- a/Content.Server/Imperial/Medieval/Illitid/IllitidSystem.cs +++ b/Content.Server/Imperial/Medieval/Illitid/IllitidSystem.cs @@ -261,7 +261,7 @@ private void CheckThought(EntityUid source, string thought, bool log = true) $"Thought from {ToPrettyString(source):user} as Illitid"); } - _nrpMessages.CheckMessage(source, thought); + _ = _nrpMessages.CheckMessage(source, thought); } private bool SendThoughtInRange(EntityUid source, float range, string thought) diff --git a/Content.Server/Imperial/Medieval/JobSpawn/TDMRuleComponent.cs b/Content.Server/Imperial/Medieval/JobSpawn/TDMRuleComponent.cs index c0f07d28483..d861276eb05 100644 --- a/Content.Server/Imperial/Medieval/JobSpawn/TDMRuleComponent.cs +++ b/Content.Server/Imperial/Medieval/JobSpawn/TDMRuleComponent.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Audio; +using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Utility; using Robust.Shared.Prototypes; @@ -41,77 +41,77 @@ public sealed partial class TDMRuleComponent : Component [DataField] public ResPath[] Sector0 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector1 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector2 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector3 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector4 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector5 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector6 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector7 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector7Cave = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector9 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; [DataField] public ResPath[] Sector10 = new ResPath[] //Imperal additional maps { - //new ResPath("/Maps/Imperial/Medieval/dotamapV0.4.yml"), + new ResPath("/Maps/Imperial/Medieval/Sector0V03.yml") }; diff --git a/Content.Server/Imperial/Medieval/Language/Systems/LanguageSystem.Translator.cs b/Content.Server/Imperial/Medieval/Language/Systems/LanguageSystem.Translator.cs index f9b28fe9098..790ebe1fde4 100644 --- a/Content.Server/Imperial/Medieval/Language/Systems/LanguageSystem.Translator.cs +++ b/Content.Server/Imperial/Medieval/Language/Systems/LanguageSystem.Translator.cs @@ -1,8 +1,7 @@ using Content.Server.Popups; -using Content.Server.PowerCell; +using Content.Shared.PowerCell; using Content.Shared.Interaction; using Content.Shared.Imperial.Medieval.Language; -using Content.Shared.PowerCell; using Content.Shared.Interaction.Events; using Content.Shared.Hands; diff --git a/Content.Server/Imperial/Medieval/Language/Types/CollectiveMind.cs b/Content.Server/Imperial/Medieval/Language/Types/CollectiveMind.cs index 841aab86167..b5d2861ab2d 100644 --- a/Content.Server/Imperial/Medieval/Language/Types/CollectiveMind.cs +++ b/Content.Server/Imperial/Medieval/Language/Types/CollectiveMind.cs @@ -71,7 +71,7 @@ public void Speak(EntityUid uid, string message, string name, SpeechVerbPrototyp var chatMan = IoCManager.Resolve(); success = false; - chat.TryProccessRadioMessage(uid, message, out message, out _); + chat.TryProcessRadioMessage(uid, message, out message, out _); resultMessage = message; if (string.IsNullOrEmpty(message)) @@ -167,7 +167,7 @@ public void Whisper(EntityUid uid, string message, string name, string nameIdent var chatMan = IoCManager.Resolve(); success = false; - chat.TryProccessRadioMessage(uid, message, out message, out _); + chat.TryProcessRadioMessage(uid, message, out message, out _); resultMessage = message; resultObfMessage = message; diff --git a/Content.Server/Imperial/Medieval/Language/Types/Emotes.cs b/Content.Server/Imperial/Medieval/Language/Types/Emotes.cs index b62c00f7e87..15136c74c0d 100644 --- a/Content.Server/Imperial/Medieval/Language/Types/Emotes.cs +++ b/Content.Server/Imperial/Medieval/Language/Types/Emotes.cs @@ -63,7 +63,7 @@ public void Speak(EntityUid uid, string message, string name, SpeechVerbPrototyp var chatMan = IoCManager.Resolve(); success = false; - chat.TryProccessRadioMessage(uid, message, out message, out _); + chat.TryProcessRadioMessage(uid, message, out message, out _); string coloredMessage = lang.AccentuateMessage(uid, Language, message); // We dont want to generate emotes @@ -147,7 +147,7 @@ public void Whisper(EntityUid uid, string message, string name, string nameIdent var chatMan = IoCManager.Resolve(); success = false; - chat.TryProccessRadioMessage(uid, message, out message, out _); + chat.TryProcessRadioMessage(uid, message, out message, out _); string coloredMessage = lang.AccentuateMessage(uid, Language, message); string coloredLanguageMessage = random.Pick(Replacement); resultMessage = FormattedMessage.EscapeText(coloredMessage); diff --git a/Content.Server/Imperial/Medieval/LastWords/LastWordsSystem.cs b/Content.Server/Imperial/Medieval/LastWords/LastWordsSystem.cs index bedd35af2e6..0c0dc3015c6 100644 --- a/Content.Server/Imperial/Medieval/LastWords/LastWordsSystem.cs +++ b/Content.Server/Imperial/Medieval/LastWords/LastWordsSystem.cs @@ -23,7 +23,7 @@ public override void Initialize() private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent args) { - if (!HasComp(ent) || !HasComp(ent)) + if (!HasComp(ent) || !HasComp(ent)) return; if (string.IsNullOrWhiteSpace(args.Message)) diff --git a/Content.Server/Imperial/Medieval/Lycantropy/Systems/LycantropySystem.cs b/Content.Server/Imperial/Medieval/Lycantropy/Systems/LycantropySystem.cs index 5a83808c7bc..e0faa47c4e7 100644 --- a/Content.Server/Imperial/Medieval/Lycantropy/Systems/LycantropySystem.cs +++ b/Content.Server/Imperial/Medieval/Lycantropy/Systems/LycantropySystem.cs @@ -44,6 +44,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Lycantropy; @@ -246,7 +247,7 @@ private void OnWerewolfHit(EntityUid uid, WerewolfComponent comp, MeleeHitEvent { foreach (var item in args.HitEntities) { - if (!HasComp(item)) + if (!HasComp(item)) continue; if (!_mobState.IsCritical(item)) diff --git a/Content.Server/Imperial/Medieval/MArtifacts/ArtifactAbilitiesSystem.cs b/Content.Server/Imperial/Medieval/MArtifacts/ArtifactAbilitiesSystem.cs index d15d56efa68..3f72235124b 100644 --- a/Content.Server/Imperial/Medieval/MArtifacts/ArtifactAbilitiesSystem.cs +++ b/Content.Server/Imperial/Medieval/MArtifacts/ArtifactAbilitiesSystem.cs @@ -32,7 +32,9 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using Content.Shared.Wieldable.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Chemistry.Components; namespace Content.Server.Imperial.Medieval.Artifacts; public sealed class ArtifacAbilitiestSystem : EntitySystem @@ -107,7 +109,7 @@ private void MidasHit(EntityUid _, ArtifactMidasComponent component, MeleeHitEve { foreach (var target in args.HitEntities) { - if (!HasComp(target)) + if (!HasComp(target)) continue; if (component.Blacklist.Contains(target)) continue; @@ -125,17 +127,17 @@ private void PoisonInit(EntityUid _, ArtifactPoisonComponent component, MeleeHit { if (!HasComp(target)) continue; - _blood.TryAddToChemicals(target, new(new List() + _blood.TryAddToBloodstream(target, new Solution(new List() { new("Amatoxin", FixedPoint2.New(component.Amount)) - }, new())); + })); } } private void LightHit(EntityUid _, ArtifactLightWeaponComponent component, MeleeHitEvent args) { foreach (var target in args.HitEntities) { - if (!HasComp(target)) + if (!HasComp(target)) continue; _flash.Flash(target, args.User, args.Weapon, TimeSpan.FromSeconds(component.FlashTime * 1000), 1f, true, true); } @@ -194,7 +196,7 @@ private void VampirismHit(EntityUid ouruid, ArtifactVampirismOnHitComponent comp var passed = false; foreach (var target in args.HitEntities) { - if (!HasComp(target)) + if (!HasComp(target)) continue; passed = true; break; @@ -217,7 +219,7 @@ private void VampirismHit(EntityUid ouruid, ArtifactVampirismOnHitComponent comp { heald.DamageDict[type.ID] = heal; } - _damage.TryChangeDamage(args.User, heald, true, false, null, uid); + _damage.TryChangeDamage(args.User, heald, true, false, uid); if (TryComp(args.User, out var nocturn)) nocturn.BloodLevel += component.BloodRestore; } diff --git a/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.EntityAiming.cs b/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.EntityAiming.cs index 07da5576d1f..637ec1ee567 100644 --- a/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.EntityAiming.cs +++ b/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.EntityAiming.cs @@ -212,7 +212,7 @@ protected override void CastLightningSpell(MedievalLightningSpellData args) args.Seed = args.Seed.HasValue ? args.Seed : args.Seed * i; if (target.HasValue) - args.LightningCollideEffects.ForEach(effect => effect.Effect(new EntityEffectBaseArgs(target.Value, EntityManager))); + _entityEffects.ApplyEffects(target.Value, args.LightningCollideEffects, user: target.Value); SpawnLightning(performerCoords, targetCoords.Position, args); diff --git a/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.cs b/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.cs index afe67c7ed3b..382e1af2b22 100644 --- a/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/MedievalMagicSystem.cs @@ -7,8 +7,9 @@ using Content.Server.Imperial.TargetOverlay; using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Imperial.Medieval.Language; +using Content.Shared.Chat; +using Content.Shared.EntityEffects; using Content.Shared.Imperial.Medieval.Magic; -using Content.Shared.Tag; using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -18,7 +19,6 @@ namespace Content.Server.Imperial.Medieval.Magic; /// -/// Server part of the /// /// Responsible for the words spoken when casting spells and for the spawn of the projectile /// @@ -36,7 +36,8 @@ public sealed partial class MedievalMagicSystem : SharedMedievalMagicSystem [Dependency] private readonly TargetOverlaySystem _targetOverlaySystem = default!; [Dependency] private readonly ImperialLightningSystem _lightningSystem = default!; [Dependency] private readonly MapSystem _mapSystem = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly Content.Shared.Tag.TagSystem _tagSystem = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; [Dependency] private readonly IPrototypeManager _proto = default!; private static readonly Gauge SpellCastedMetrics = Metrics.CreateGauge( @@ -136,14 +137,14 @@ protected override void AddToStack(EntityUid uid, Dictionary InGameICChatType.Speak, - SpellSpeechType.Emote => InGameICChatType.Emote, - SpellSpeechType.Whisper => InGameICChatType.Whisper, - _ => InGameICChatType.Speak + SpellSpeechType.Speak => Content.Shared.Chat.InGameICChatType.Speak, + SpellSpeechType.Emote => Content.Shared.Chat.InGameICChatType.Emote, + SpellSpeechType.Whisper => Content.Shared.Chat.InGameICChatType.Whisper, + _ => Content.Shared.Chat.InGameICChatType.Speak }; } diff --git a/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/CastTraining/CastTrainingSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/CastTraining/CastTrainingSystem.cs index 24eef5d8af7..24b62f33b2e 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/CastTraining/CastTrainingSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/CastTraining/CastTrainingSystem.cs @@ -1,8 +1,6 @@ using Content.Shared.Imperial.Medieval.Magic; using Robust.Shared.Random; using Content.Shared.EntityEffects; -using System.Linq; - namespace Content.Server.Imperial.Medieval.Magic.MagicTraining.CastTraining; @@ -13,6 +11,8 @@ namespace Content.Server.Imperial.Medieval.Magic.MagicTraining.CastTraining; public sealed partial class CastTrainingSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffectsSystem = default!; + public override void Initialize() { @@ -25,14 +25,9 @@ private void OnSpellCast(EntityUid uid, CastTrainingComponent component, Medieva { foreach (var trainingResult in component.TrainingResults) { - var magicEntityEffectsArgs = new MagicEntityEffectsArgs(args.Performer, args.Performer, uid, EntityManager); - var canApplyTrainingResult = trainingResult.Conditions?.Aggregate(true, (acc, cond) => acc && cond.Condition(magicEntityEffectsArgs)) ?? true; - - if ( - canApplyTrainingResult && - _random.Prob(trainingResult.Probability) - ) - trainingResult.Effect(magicEntityEffectsArgs); + if (!_random.Prob(trainingResult.Probability)) continue; + + _entityEffectsSystem.TryApplyEffect(args.Performer, trainingResult, user: args.Performer); } } } diff --git a/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/DamageTraining/DamageTrainingSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/DamageTraining/DamageTrainingSystem.cs index 546a362be06..ac4500cfcdc 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/DamageTraining/DamageTrainingSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/MagicTraining/DamageTraining/DamageTrainingSystem.cs @@ -13,7 +13,7 @@ namespace Content.Server.Imperial.Medieval.Magic.MagicTraining.DamageTraining; public sealed partial class DamageTrainingSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; - + [Dependency] private readonly SharedEntityEffectsSystem _entityEffectsSystem = default!; public override void Initialize() { @@ -35,19 +35,11 @@ private void OnProjectileSpawn(EntityUid uid, DamageTrainingComponent component, private void OnProjectileHit(EntityUid uid, MedievalAddEssenceOnProjectileHitComponent component, ProjectileHitEvent args) { - var magicEntityEffectsArgs = new MagicEntityEffectsArgs(args.Target, component.Performer, component.Action, EntityManager); - foreach (var trainingResult in component.TrainingResults) { - var canApplyTrainingResult = trainingResult.Conditions?.Aggregate(true, (acc, cond) => acc && cond.Condition(magicEntityEffectsArgs)) ?? true; - - if ( - canApplyTrainingResult && - _random.Prob(trainingResult.Probability) - ) - { - trainingResult.Effect(magicEntityEffectsArgs); - } + if (!_random.Prob(trainingResult.Probability)) continue; + + _entityEffectsSystem.TryApplyEffect(args.Target, trainingResult, user: component.Performer); } } } diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/Brarrier/BrarrierComponent.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/Brarrier/BrarrierComponent.cs index 353868f91de..ed6d494e101 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/Brarrier/BrarrierComponent.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/Brarrier/BrarrierComponent.cs @@ -10,7 +10,7 @@ namespace Content.Server.Imperial.Medieval.Magic; /// /// Allows the entity to create a barrier in 4 directions from itself. /// -/// WARNING, you must spawn the entity with the call + /// /// /// Yes, this is shit code. diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalRandomOffsetSpawn/RandomOffsetSpawnSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalRandomOffsetSpawn/RandomOffsetSpawnSystem.cs index 5980118fe9a..75d48e9d6e7 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalRandomOffsetSpawn/RandomOffsetSpawnSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalRandomOffsetSpawn/RandomOffsetSpawnSystem.cs @@ -44,7 +44,6 @@ private void SpawnOnTrigger(EntityUid uid, RandomOffsetSpawnComponent component) { var coords = _transform.GetMapCoordinates(uid); Vector2 randomVector; - Vector2 randomDirectionFromAngle; MapCoordinates resultCoords; bool needNewCoords; for (var i = 0; i < component.Quantity; i++) diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnComponent.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnComponent.cs index 853d1d77075..5d1c2fb57ab 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnComponent.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnComponent.cs @@ -4,7 +4,7 @@ namespace Content.Server.Imperial.Medieval.Magic.MedievalShootAfterSpawn; /// -/// Shoots after calling at selected targets + /// [RegisterComponent] public sealed partial class MedievalShootAfterSpawnComponent : Component diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnSystem.cs index ef0bc1baecc..bd280db141f 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalShootAfterSpawn/MedievalShootAfterSpawnSystem.cs @@ -33,8 +33,7 @@ public override void Update(float frameTime) _gunSystem.AttemptShoot( uid, - uid, - gunComponent, + (uid, gunComponent), CoordsHelper.GetCoords(component.Target, EntityManager) ); } diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractComponent.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractComponent.cs index 2ad41bc2da3..c9dd9f859fc 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractComponent.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractComponent.cs @@ -15,10 +15,10 @@ public sealed partial class MedievalSpellOnInteractComponent : Component /// List of effects that should be applied. /// [DataField] - public List Effects = new(); + public EntityEffect[] Effects = []; [DataField] - public List SelfUseEffects = new(); + public EntityEffect[] SelfUseEffects = []; [DataField] public SoundSpecifier? SoundOnInteract; diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractSystem.cs index f825a8f388b..a980c5fd6d9 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellOnInteract/MedievalSpellOnInteractSystem.cs @@ -8,6 +8,7 @@ namespace Content.Server.Imperial.Medieval.Magic.MedievalSpellOnInteract; public sealed partial class MedievalSpellOnInteractSystem : EntitySystem { [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffectsSystem = default!; public override void Initialize() @@ -25,14 +26,12 @@ public void OnInteract(EntityUid uid, MedievalSpellOnInteractComponent component component.UseCount += 1; var effects = args.User == args.Target - ? component.SelfUseEffects.Count == 0 + ? component.SelfUseEffects.Length == 0 ? component.Effects : component.SelfUseEffects : component.Effects; - foreach (var effect in effects) - effect.Effect(new EntityEffectBaseArgs(args.Target.Value, EntityManager)); - + _entityEffectsSystem.ApplyEffects(args.Target.Value, effects, user: args.User); _audioSystem.PlayPvs(component.SoundOnInteract, args.User); if (component.UseCount >= component.RemainingUses) diff --git a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellTeleportEffect/MedievalSpellTeleportEffectSystem.cs b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellTeleportEffect/MedievalSpellTeleportEffectSystem.cs index 4c010f76d21..adb50c57671 100644 --- a/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellTeleportEffect/MedievalSpellTeleportEffectSystem.cs +++ b/Content.Server/Imperial/Medieval/Magic/Other/Spells/MedievalSpellTeleportEffect/MedievalSpellTeleportEffectSystem.cs @@ -10,9 +10,6 @@ namespace Content.Server.Imperial.Medieval.Magic.MedievalSpellTeleportEffect; /// public sealed partial class MedievalSpellTeleportEffectSystem : SharedMedievalSpellTeleportEffectSystem { - [Dependency] private readonly ExamineSystem _examineSystem = default!; - - public override void Initialize() { base.Initialize(); diff --git a/Content.Server/Imperial/Medieval/MedievalSelfHealSystem.cs b/Content.Server/Imperial/Medieval/MedievalSelfHealSystem.cs index 051b2d5146a..4412a9d0595 100644 --- a/Content.Server/Imperial/Medieval/MedievalSelfHealSystem.cs +++ b/Content.Server/Imperial/Medieval/MedievalSelfHealSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Mobs; using Content.Shared.Damage; using Content.Shared.Imperial.Medieval.Magic.Mana; +using Content.Shared.Damage.Systems; namespace Content.Server.MagicPotionsMaker { diff --git a/Content.Server/Imperial/Medieval/Melee/ShieldRegenSystem.cs b/Content.Server/Imperial/Medieval/Melee/ShieldRegenSystem.cs index e35f058883a..082219a09c5 100644 --- a/Content.Server/Imperial/Medieval/Melee/ShieldRegenSystem.cs +++ b/Content.Server/Imperial/Medieval/Melee/ShieldRegenSystem.cs @@ -5,15 +5,15 @@ using Content.Shared.Blocking; using Robust.Shared.Player; using Robust.Shared.Audio.Systems; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.ShieldRegen { public sealed partial class GribInfectedSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] internal readonly IEntityManager _entityManager = default!; - [Dependency] internal readonly IMapManager _mapManager = default!; - [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; public override void Initialize() { @@ -23,18 +23,19 @@ public override void Initialize() public override void Update(float frameTime) { base.Update(frameTime); - foreach (var comp in EntityManager.EntityQuery()) - { + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var comp)) + { if (_timing.CurTime > comp.EndTime) { - var entity = comp.Owner; + var entity = uid; comp.StartTime = _timing.CurTime; comp.EndTime = comp.StartTime + comp.ReloadTime; if (TryComp(entity, out var damageable) && TryComp(entity, out var block)) { - if (damageable.TotalDamage > comp.Health) + if (_damageableSystem.GetTotalDamage((entity, damageable)) > comp.Health) { block.PassiveBlockFraction = 0.35f; } @@ -47,7 +48,7 @@ public override void Update(float frameTime) } if (_timing.CurTime > comp.RegenEndTime) { - var entity = comp.Owner; + var entity = uid; comp.RegenStartTime = _timing.CurTime; comp.RegenEndTime = comp.RegenStartTime + comp.RegenReloadTime; diff --git a/Content.Server/Imperial/Medieval/MeleeFighting/MedievalIronHitSoundSystem.cs b/Content.Server/Imperial/Medieval/MeleeFighting/MedievalIronHitSoundSystem.cs index 7e8d5e28d3c..4f6996e51fb 100644 --- a/Content.Server/Imperial/Medieval/MeleeFighting/MedievalIronHitSoundSystem.cs +++ b/Content.Server/Imperial/Medieval/MeleeFighting/MedievalIronHitSoundSystem.cs @@ -21,6 +21,7 @@ using Robust.Shared.Random; using System.Linq; using Content.Shared.Inventory; +using Content.Shared.Damage.Systems; namespace Content.Server.MedievalIronHitSound { diff --git a/Content.Server/Imperial/Medieval/MobRiding/RideableSystem.cs b/Content.Server/Imperial/Medieval/MobRiding/RideableSystem.cs index 191774222c6..17b67b6c88d 100644 --- a/Content.Server/Imperial/Medieval/MobRiding/RideableSystem.cs +++ b/Content.Server/Imperial/Medieval/MobRiding/RideableSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.NPC.HTN; +using Content.Server.NPC.HTN; using Content.Server.NPC.Systems; using Content.Shared.Buckle; using Content.Shared.Buckle.Components; @@ -25,6 +25,7 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.MobRiding { @@ -139,7 +140,7 @@ private void PikeHitEntity(EntityUid uid, RideableComponent comp, EntityUid othe var velocity = rideablePhysics.LinearVelocity.Length(); - _popup.PopupEntity("Вы сшибаете пехотинца с дороги.", rider, rider); + _popup.PopupEntity(" .", rider, rider); _damageable.TryChangeDamage(other, pikeComp.RidingDamage * velocity); var oneSound = _random.Pick(_clashOne); @@ -333,16 +334,16 @@ private void PikeClash(EntityUid uid, RideableComponent comp, EntityUid other) var oneSound = _random.Pick(_clashOne); switch (stabilityResult) { - // TODO: текст в Loc, посмотреть, почему не работают звуки - // TODO: сделаю после ОБТ, сейчас впадлу + // TODO: Loc, , + // TODO: , case CheckResult.Self: - _popup.PopupEntity("Противник не удерживается в седле и падает.", rider.Value, rider.Value); + _popup.PopupEntity(" .", rider.Value, rider.Value); _audio.PlayPvs(oneSound, uid); ThrowClash(uid, rider.Value, otherRider.Value); DelayPike(rider.Value); break; case CheckResult.Other: - _popup.PopupEntity("Противник не удерживается в седле и падает.", otherRider.Value, otherRider.Value); + _popup.PopupEntity(" .", otherRider.Value, otherRider.Value); _audio.PlayPvs(oneSound, other); ThrowClash(other, otherRider.Value, rider.Value); DelayPike(otherRider.Value); @@ -352,8 +353,8 @@ private void PikeClash(EntityUid uid, RideableComponent comp, EntityUid other) ThrowClashBoth(uid, other, rider.Value, otherRider.Value); break; case CheckResult.Both: - _popup.PopupEntity("Вы сталкиваетесь, но оба удерживаетесь в седле.", rider.Value, rider.Value); - _popup.PopupEntity("Вы сталкиваетесь, но оба удерживаетесь в седле.", otherRider.Value, otherRider.Value); + _popup.PopupEntity(" , .", rider.Value, rider.Value); + _popup.PopupEntity(" , .", otherRider.Value, otherRider.Value); DelayPike(rider.Value); DelayPike(otherRider.Value); _audio.PlayPvs(_clashNone, uid); diff --git a/Content.Server/Imperial/Medieval/Myrmex/MyrmexActionSystem.cs b/Content.Server/Imperial/Medieval/Myrmex/MyrmexActionSystem.cs index 6b960b02b7d..c1c258aff2c 100644 --- a/Content.Server/Imperial/Medieval/Myrmex/MyrmexActionSystem.cs +++ b/Content.Server/Imperial/Medieval/Myrmex/MyrmexActionSystem.cs @@ -67,7 +67,7 @@ private void OnBoost(Entity ent, ref ActionMyrmexBoostEvent arg private void OnToggleArmor(Entity ent, ref ActionMyrmexToggleArmorEvent args) { ent.Comp.ArmorActive = !ent.Comp.ArmorActive; - _damageable.SetDamageModifierSetId(ent, ent.Comp.ArmorActive ? ent.Comp.ActiveArmorProto : ent.Comp.StandardArmorProto); + _damageable.SetDamageModifierSetId(ent.Owner, ent.Comp.ArmorActive ? ent.Comp.ActiveArmorProto : ent.Comp.StandardArmorProto); ModifyMoveSpeed(ent, ent.Comp.ArmorActive ? -ent.Comp.ActiveArmorSpeedMultiplier : ent.Comp.ActiveArmorSpeedMultiplier); args.Handled = true; diff --git a/Content.Server/Imperial/Medieval/Myrmex/MyrmexQueenSystem.cs b/Content.Server/Imperial/Medieval/Myrmex/MyrmexQueenSystem.cs index 3ef0a048ae8..49c1d36479f 100644 --- a/Content.Server/Imperial/Medieval/Myrmex/MyrmexQueenSystem.cs +++ b/Content.Server/Imperial/Medieval/Myrmex/MyrmexQueenSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Actions; +using Content.Shared.Actions; using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; @@ -47,7 +47,7 @@ private void OnLayEgg(EntityUid uid, MyrmexQueenComponent comp, ref ActionMyrmex if (hunger.Buffs.Count == 0) { - //_popup.PopupEntity(Loc.GetString("medieval-myrmex-queen-egg-no-buff"), uid, uid); + //return; } diff --git a/Content.Server/Imperial/Medieval/Myrmex/MyrmexSystem.cs b/Content.Server/Imperial/Medieval/Myrmex/MyrmexSystem.cs index 07e36873ea7..71b06a77ece 100644 --- a/Content.Server/Imperial/Medieval/Myrmex/MyrmexSystem.cs +++ b/Content.Server/Imperial/Medieval/Myrmex/MyrmexSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.Body; using System.Linq; using System.Numerics; using Content.Server.Actions; @@ -26,6 +27,7 @@ using Robust.Shared.Random; using Robust.Shared.Spawners; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Myrmex { diff --git a/Content.Server/Imperial/Medieval/Nocturn/RaceSystem.cs b/Content.Server/Imperial/Medieval/Nocturn/RaceSystem.cs index 74c9264575f..57c849a0825 100644 --- a/Content.Server/Imperial/Medieval/Nocturn/RaceSystem.cs +++ b/Content.Server/Imperial/Medieval/Nocturn/RaceSystem.cs @@ -2,6 +2,7 @@ using Robust.Shared.Timing; using Robust.Shared.Map; using Content.Shared.Humanoid; +using Content.Shared.Body; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Robust.Shared.Random; @@ -30,6 +31,8 @@ using Content.Shared.Imperial.Medieval.Skills; using Content.Server.Imperial.Medieval.Skills; using Content.Shared.Random.Helpers; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Nocturn { @@ -54,6 +57,7 @@ public sealed partial class RaceSystem : EntitySystem [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly SharedTypingIndicatorSystem _typing = default!; [Dependency] private readonly SkillsSystem _skills = default!; + [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!; public override void Initialize() @@ -141,17 +145,25 @@ public override void Update(float frameTime) comp.BloodLevel = 400f; } - if (comp.BloodLevel >= 200f && TryComp(comp.Owner, out var damageable) && damageable.TotalDamage < 61f && damageable.TotalDamage > 5f) + if (comp.BloodLevel >= 200f && TryComp(comp.Owner, out var damageable)) { + var total = _damageableSystem.GetTotalDamage((comp.Owner, damageable)); + if (total < 61f && total > 5f) + { _damageableSystem.TryChangeDamage(uid, -comp.BloodLostDamage, true, false); //comp.BloodLevel -= comp.BloodDrainPerSecond * 2; + } } - if (comp.BloodLevel >= 200f && TryComp(comp.Owner, out var damag) && damag.TotalDamage < 105f && damag.TotalDamage > 60f) + if (comp.BloodLevel >= 200f && TryComp(comp.Owner, out var damag)) { + var total2 = _damageableSystem.GetTotalDamage((comp.Owner, damag)); + if (total2 < 105f && total2 > 60f) + { _damageableSystem.TryChangeDamage(uid, -comp.BloodLostDamage, true, false); //_damageableSystem.TryChangeDamage(uid, -comp.RegenDamage * 3.5f, true, false); //comp.BloodLevel -= comp.BloodDrainPerSecond * 39; + } } if (comp.BloodDrainPerSecond > comp.BloodLevel) @@ -185,7 +197,7 @@ public override void Update(float frameTime) if (comp.BloodLevel < 50f && comp.IsDisguised) { - if (TryComp(uid, out var appearance)) + if (TryComp(uid, out var appearance)) { _popupSystem.PopupEntity(Loc.GetString("nocturn-disguise-low-blood"), uid, uid, PopupType.LargeCaution); @@ -325,7 +337,7 @@ private void OnNocturnDrinkDoAfter(EntityUid uid, NocturnComponent component, No return; } } - if (HasComp(args.Args.Target.Value)) + if (HasComp(args.Args.Target.Value)) { if (!HasComp(args.Args.Target)) { @@ -397,8 +409,9 @@ private void OnNocturnDisguiseDoAfter(EntityUid uid, NocturnComponent component, { if (args.Handled || args.Cancelled) return; - - if (!TryComp(uid, out var appearance)) + // if (!TryComp(uid, out var appearance)) + // return; + if (!TryComp(uid, out var appearance)) return; if (!component.IsDisguised) @@ -419,9 +432,9 @@ private void OnNocturnDisguiseDoAfter(EntityUid uid, NocturnComponent component, } } - private void ApplyDisguise(EntityUid uid, NocturnComponent component, HumanoidAppearanceComponent appearance) + private void ApplyDisguise(EntityUid uid, NocturnComponent component, HumanoidProfileComponent appearance) { - appearance.Species = "Human"; + _humanoidProfile.SetSpecies((uid, appearance), "Human"); component.BloodDrainPerSecond *= 1.3f; component.BloodLevel -= 10; @@ -435,9 +448,9 @@ private void ApplyDisguise(EntityUid uid, NocturnComponent component, HumanoidAp } } - private void RevertToOriginalForm(EntityUid uid, NocturnComponent component, HumanoidAppearanceComponent appearance) + private void RevertToOriginalForm(EntityUid uid, NocturnComponent component, HumanoidProfileComponent appearance) { - appearance.Species = "Drou"; + _humanoidProfile.SetSpecies((uid, appearance), "Drou"); component.BloodDrainPerSecond /= 1.3f; component.IsDisguised = false; @@ -452,12 +465,13 @@ private void RevertToOriginalForm(EntityUid uid, NocturnComponent component, Hum public void ShowEyes(EntityUid uid) { - if (TryComp(uid, out var appeareance)) - { - appeareance.EyeColor = Color.Red; - Dirty(uid, appeareance); - } - + // if (TryComp(uid, out var appeareance)) + // { + // appeareance.EyeColor = Color.Red; + // Dirty(uid, appeareance); + // } + var visualBody = EntityManager.System(); + visualBody.ApplyProfile(uid, new OrganProfileData { EyeColor = Color.Red }); } private void OnExamine(EntityUid uid, NocturnComponent component, ExaminedEvent args) diff --git a/Content.Server/Imperial/Medieval/ObeliskDestroyable/ObeliskDestroyableSystem.cs b/Content.Server/Imperial/Medieval/ObeliskDestroyable/ObeliskDestroyableSystem.cs index 2ef5e1e0b2e..c0bf87008e4 100644 --- a/Content.Server/Imperial/Medieval/ObeliskDestroyable/ObeliskDestroyableSystem.cs +++ b/Content.Server/Imperial/Medieval/ObeliskDestroyable/ObeliskDestroyableSystem.cs @@ -1,4 +1,6 @@ using System.Linq; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; using Content.Server.Chat.Systems; using Content.Server.Imperial.Medieval.Factions; @@ -44,7 +46,7 @@ private void OnDamageChanged(EntityUid uid, ObeliskDestroyableComponent componen } var requiredDamage = GetCumulativeThreshold(component, phaseIndex + 1); - if (damageable.TotalDamage < requiredDamage) + if (_damageable.GetTotalDamage((uid, damageable)) < requiredDamage) return; SendAnnouncement(component, phase); @@ -52,7 +54,7 @@ private void OnDamageChanged(EntityUid uid, ObeliskDestroyableComponent componen if (phase.DestroyOnReached) { var trueOrigin = _achievement.GetPlayerFromOrigin(args.Origin); // xd - + if (trueOrigin != null && TryComp(trueOrigin, out var trueOriginFaction)) { var query = EntityQueryEnumerator(); @@ -86,8 +88,7 @@ public void ResetObelisk(EntityUid uid, ObeliskDestroyableComponent component, D component.CurrentPhase = 0; component.InvincibilityActive = false; _tempInvincibility.EndTempInvincibilityEarly(uid); - - _damageable.SetAllDamage(uid, damageable, FixedPoint2.Zero); + _damageable.SetAllDamage((uid, damageable), FixedPoint2.Zero); Dirty(uid, component); } @@ -110,12 +111,13 @@ private void DestroyObelisk(EntityUid uid, ObeliskDestroyableComponent component private void SetTotalDamage(EntityUid uid, DamageableComponent damageable, FixedPoint2 targetDamage) { - var currentDamage = new DamageSpecifier(damageable.Damage); + // var currentDamage = new DamageSpecifier(damageable.Damage); + var currentDamage = _damageable.GetAllDamage((uid, damageable)); var overflow = currentDamage.GetTotal() - targetDamage; if (overflow <= FixedPoint2.Zero) return; - var damageTypes = new List(currentDamage.DamageDict.Keys); + var damageTypes = currentDamage.DamageDict.Keys.ToList(); foreach (var damageType in damageTypes) { if (!currentDamage.DamageDict.TryGetValue(damageType, out var amount) || @@ -134,7 +136,7 @@ private void SetTotalDamage(EntityUid uid, DamageableComponent damageable, Fixed } } - _damageable.SetDamage(uid, damageable, currentDamage); + _damageable.SetDamage((uid, damageable), currentDamage); } private bool TryGetCurrentPhase(ObeliskDestroyableComponent component, out ObeliskDestroyablePhaseData phase, out int phaseIndex) diff --git a/Content.Server/Imperial/Medieval/ObeliskFixer/ObeliskFixerSystem.cs b/Content.Server/Imperial/Medieval/ObeliskFixer/ObeliskFixerSystem.cs index 22bc268ad5f..86efc92493b 100644 --- a/Content.Server/Imperial/Medieval/ObeliskFixer/ObeliskFixerSystem.cs +++ b/Content.Server/Imperial/Medieval/ObeliskFixer/ObeliskFixerSystem.cs @@ -6,6 +6,8 @@ using Content.Shared.Imperial.Medieval.ObeliskFixer; using Content.Shared.Imperial.Medieval.Skills; using Content.Shared.Interaction; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Imperial.Medieval.ObeliskFixer; @@ -13,6 +15,7 @@ public sealed class ObeliskFixerSystem : EntitySystem { [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly ObeliskDestroyableSystem _obelisks = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public override void Initialize() { @@ -33,7 +36,7 @@ args.Target is not { } target || return; } - if (damageable.TotalDamage <= FixedPoint2.Zero && + if (_damageable.GetTotalDamage((target, damageable)) <= FixedPoint2.Zero && obelisk.CurrentPhase == 0) { return; @@ -71,7 +74,7 @@ args.Target is not { } target || return; } - if (damageable.TotalDamage <= FixedPoint2.Zero && + if (_damageable.GetTotalDamage((target, damageable)) <= FixedPoint2.Zero && obelisk.CurrentPhase == 0) { return; diff --git a/Content.Server/Imperial/Medieval/Pasport/MedievalPasportSystem.cs b/Content.Server/Imperial/Medieval/Pasport/MedievalPasportSystem.cs index c180ffb1244..3d54807bc89 100644 --- a/Content.Server/Imperial/Medieval/Pasport/MedievalPasportSystem.cs +++ b/Content.Server/Imperial/Medieval/Pasport/MedievalPasportSystem.cs @@ -55,14 +55,16 @@ public void OnStart(EntityUid uid, MedievalPasportPersonComponent comp, Componen var pasport = EnsureComp(comp.PasportEntity.Value); if (TryComp(uid, out var metadata)) pasport.PersonName = metadata.EntityName; - if (TryComp(uid, out var appearance)) + if (TryComp(uid, out var appearance)) { if (appearance.Sex == Sex.Male) pasport.PersonGender = "мужской"; else pasport.PersonGender = "женский"; - pasport.PersonAge = appearance.Age.ToString(); - pasport.PersonRace = appearance.Species.ToString(); + var age = appearance.Age; + var species = appearance.Species; + pasport.PersonAge = age.ToString(); + pasport.PersonRace = species.ToString(); } pasport.PersonJob = comp.PersonJob; _metaData.SetEntityName(comp.PasportEntity.Value, "волшебное удостоверение " + pasport.PersonName); diff --git a/Content.Server/Imperial/Medieval/PinpointerCritical/PinpointerCriticalSystems.cs b/Content.Server/Imperial/Medieval/PinpointerCritical/PinpointerCriticalSystems.cs index efe69c6f34a..a1de750120c 100644 --- a/Content.Server/Imperial/Medieval/PinpointerCritical/PinpointerCriticalSystems.cs +++ b/Content.Server/Imperial/Medieval/PinpointerCritical/PinpointerCriticalSystems.cs @@ -28,7 +28,7 @@ public void OnSwitch(EntityUid uid, PinpointerCriticalComponent comp, ActivateIn var query = EntityQueryEnumerator(); while (query.MoveNext(out var uidd, out var compp)) { - if (HasComp(uidd) && compp.CurrentThresholdState == MobState.Critical + if (HasComp(uidd) && compp.CurrentThresholdState == MobState.Critical && Transform(ev.User).ParentUid == Transform(uidd).ParentUid) // Проверка на критическое состояние { var targetCoords = _transform.GetMapCoordinates(uidd); // Координаты цели @@ -53,7 +53,7 @@ public void OnSwitch(EntityUid uid, PinpointerCriticalComponent comp, ActivateIn } if (TryComp(ev.Target, out var pinComp) && closestUid != null) { - _pinpointer.SetTarget(uid, closestUid, pinComp); + _pinpointer.SetTarget((uid, pinComp), closestUid); } } diff --git a/Content.Server/Imperial/Medieval/Plague/Effects/ChangeDamage.cs b/Content.Server/Imperial/Medieval/Plague/Effects/ChangeDamage.cs index acb37e3a149..5cbf4d95b94 100644 --- a/Content.Server/Imperial/Medieval/Plague/Effects/ChangeDamage.cs +++ b/Content.Server/Imperial/Medieval/Plague/Effects/ChangeDamage.cs @@ -1,5 +1,6 @@ using Content.Shared.Damage; using Content.Shared.Imperial.Medieval.Plague; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Plague; diff --git a/Content.Server/Imperial/Medieval/Plague/Effects/Slowdown.cs b/Content.Server/Imperial/Medieval/Plague/Effects/Slowdown.cs index 849421f12e4..dfb623ced38 100644 --- a/Content.Server/Imperial/Medieval/Plague/Effects/Slowdown.cs +++ b/Content.Server/Imperial/Medieval/Plague/Effects/Slowdown.cs @@ -32,10 +32,17 @@ public override Slowdown CreateInstance() protected override void Effect(EntityUid uid, IEntityManager entMan) { - var comp = entMan.EnsureComponent(uid); - comp.SprintSpeedModifier = 0.7f; - comp.WalkSpeedModifier = 0.7f; - comp.ModifierTimer = IoCManager.Resolve().CurTime + TimeSpan.FromSeconds(Duration); - entMan.Dirty(uid, comp); + // var comp = entMan.EnsureComponent(uid); + // comp.SprintSpeedModifier = 0.7f; + // comp.WalkSpeedModifier = 0.7f; + // comp.ModifierTimer = IoCManager.Resolve().CurTime + TimeSpan.FromSeconds(Duration); + // entMan.Dirty(uid, comp); + var moveMod = entMan.System(); + moveMod.TryAddMovementSpeedModDuration( + uid, + Content.Shared.Movement.Systems.MovementModStatusSystem.VomitingSlowdown, + TimeSpan.FromSeconds(Duration), + 0.7f, + 0.7f); } } diff --git a/Content.Server/Imperial/Medieval/Plague/Effects/SpawnBlood.cs b/Content.Server/Imperial/Medieval/Plague/Effects/SpawnBlood.cs index 60611c7eff9..b599ceca8bd 100644 --- a/Content.Server/Imperial/Medieval/Plague/Effects/SpawnBlood.cs +++ b/Content.Server/Imperial/Medieval/Plague/Effects/SpawnBlood.cs @@ -26,8 +26,8 @@ protected override void Effect(EntityUid uid, IEntityManager entMan) var xform = entMan.GetComponent(uid); if (!entMan.TryGetComponent(uid, out var blood)) return; - - Solution sol = new(blood.BloodReagent, 20f); + // Solution sol = new(blood.BloodReagent, 20f); + Solution sol = new("Blood", 20f); puddle.TrySpillAt(xform.Coordinates, sol, out _, false); } } diff --git a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Ghost.cs b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Ghost.cs index 0c3c7990307..e5aa349f6e3 100644 --- a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Ghost.cs +++ b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Ghost.cs @@ -10,6 +10,7 @@ using Robust.Shared.Player; using Robust.Shared.Random; +using Content.Shared.Movement.Systems; namespace Content.Server.Imperial.Medieval.Plague; public sealed partial class MedievalPlagueSystem @@ -101,7 +102,8 @@ private void OnVomitAction(EntityUid uid, MedievalPlagueGhostComponent comp, Pla if (_symptoms.Where(x => x.Value.Unlocked).ToDictionary().ContainsKey("BloodVomit") && TryComp(uid, out var bloodstream)) { - Solution sol = new(bloodstream.BloodReagent, 20f); + // Solution sol = new(bloodstream.BloodReagent, 20f); + Solution sol = new("Blood", 20f); _puddle.TrySpillAt(Transform(uid).Coordinates, sol, out _, false); DamageSpecifier damage = new() { @@ -145,11 +147,14 @@ private void OnDizzinessAction(EntityUid uid, MedievalPlagueGhostComponent comp, _jitter.DoJitter(args.Target, TimeSpan.FromSeconds(6), true, 10, 3); _drunk.TryApplyDrunkenness(args.Target, TimeSpan.FromSeconds(600)); - var modifier = EnsureComp(uid); - modifier.SprintSpeedModifier = 0.7f; - modifier.WalkSpeedModifier = 0.7f; - modifier.ModifierTimer = _timing.CurTime + TimeSpan.FromSeconds(6); - Dirty(uid, modifier); + // var modifier = EnsureComp(uid); + // ... + _movementMod.TryAddMovementSpeedModDuration( + uid, + MovementModStatusSystem.VomitingSlowdown, + TimeSpan.FromSeconds(6), + 0.7f, + 0.7f); _audio.PlayGlobal(new SoundCollectionSpecifier("PlagueDizziness"), Filter.Empty().FromEntities(args.Target), false); _audio.PlayGlobal(new SoundPathSpecifier("/Audio/Imperial/Medieval/Plague/dizzy.ogg"), Filter.Empty().FromEntities(uid), false); @@ -213,7 +218,7 @@ private void OnMousuePolymorphAction(EntityUid uid, MedievalPlagueGhostComponent var xform = Transform(uid); - if (_lookup.GetEntitiesInRange(xform.Coordinates, 8f).Any()) + if (_lookup.GetEntitiesInRange(xform.Coordinates, 8f).Any()) { _popup.PopupEntity(Loc.GetString("medieval-plague-mouse-polymorph-nearby-popup"), uid, uid); return; diff --git a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Spread.cs b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Spread.cs index e0b446f955e..7d4fcf2d829 100644 --- a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Spread.cs +++ b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Spread.cs @@ -190,7 +190,7 @@ private void OnBloodlettingDoAfter(EntityUid uid, BloodlettingToolComponent comp Loc.GetString($"plague-bloodletting-result-{result.ToString().ToLower()}", ("target", Identity.Name(args.Target.Value, EntityManager, args.User))), args.User, args.User, Shared.Popups.PopupType.Medium); - _damageable.TryChangeDamage(args.Target, comp.Damage, true); + _damageable.TryChangeDamage(args.Target.Value, comp.Damage, true); comp.Result = result; _appearance.SetData(uid, BloodlettingVisuals.Data, (int)result); diff --git a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Symptoms.cs b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Symptoms.cs index c0934b8a1f9..3e4be0c73b9 100644 --- a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Symptoms.cs +++ b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.Symptoms.cs @@ -15,6 +15,7 @@ using Content.Shared.Tag; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Plague; @@ -232,7 +233,7 @@ private void UpdateSickness() _vomit.Vomit(uid); if (comp.Level >= PlagueVomitLevel.Blood && TryComp(uid, out var bloodstream)) { - Solution sol = new(bloodstream.BloodReagent, 20f); + Solution sol = new("Blood", 20f); _puddle.TrySpillAt(Transform(uid).Coordinates, sol, out _, false); } diff --git a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.cs b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.cs index 1df980ad782..a5e5efe51d9 100644 --- a/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.cs +++ b/Content.Server/Imperial/Medieval/Plague/Systems/MedievalPlagueSystem.cs @@ -35,6 +35,8 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Toolshed.Commands.Values; +using Content.Shared.Medical; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Plague; @@ -63,6 +65,7 @@ public sealed partial class MedievalPlagueSystem : SharedMedievalPlagueSystem [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly StatusEffectsSystem _status = default!; [Dependency] private readonly MovementSpeedModifierSystem _moveSpeed = default!; + [Dependency] private readonly MovementModStatusSystem _movementMod = default!; [Dependency] private readonly PolymorphSystem _polymorph = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly IChatManager _chat = default!; diff --git a/Content.Server/Imperial/Medieval/PlayerCreations/Administration/CreationsPanelEui.cs b/Content.Server/Imperial/Medieval/PlayerCreations/Administration/CreationsPanelEui.cs index 02bd9940abe..f6f0b044b87 100644 --- a/Content.Server/Imperial/Medieval/PlayerCreations/Administration/CreationsPanelEui.cs +++ b/Content.Server/Imperial/Medieval/PlayerCreations/Administration/CreationsPanelEui.cs @@ -4,7 +4,6 @@ using Content.Shared.Eui; using Content.Server.Administration; using Content.Shared.Imperial.Medieval.PlayerCreations; -using Microsoft.CodeAnalysis.Differencing; namespace Content.Server.Imperial.Medieval.PlayerCreations.Administration; diff --git a/Content.Server/Imperial/Medieval/Power/EntitySystems/MyrmexPipeSystem.cs b/Content.Server/Imperial/Medieval/Power/EntitySystems/MyrmexPipeSystem.cs index 145ea8043e8..73186524e64 100644 --- a/Content.Server/Imperial/Medieval/Power/EntitySystems/MyrmexPipeSystem.cs +++ b/Content.Server/Imperial/Medieval/Power/EntitySystems/MyrmexPipeSystem.cs @@ -69,12 +69,15 @@ private void OnValveDoAfter(Entity pipe, ref MyrmexVal if (args.Handled || args.Cancelled) return; - if (!_nodeContainer.TryGetNode(pipe.Owner, pipe.Comp.NodeId, out var pipeNode)) + if (!_nodeContainer.TryGetNode(pipe.Owner, pipe.Comp.NodeId, out var pipeNode) + || !CanRefloodNode(pipe.Owner, pipeNode)) return; pipeNode.Enabled ^= true; - _nodeGroupSystem.QueueReflood(pipeNode); + if (CanRefloodNode(pipe.Owner, pipeNode)) + _nodeGroupSystem.QueueReflood(pipeNode); + _appearance.SetData(pipe, MyrmexValveVisuals.State, pipeNode.Enabled); args.Handled = true; @@ -239,7 +242,34 @@ private void Kaboom(EntityUid cable1, EntityUid cable2, MapCoordinates coordinat cable1 ); - QueueDel(cable1); - QueueDel(cable2); + QueueNodeContainerDeletion(cable1); + QueueNodeContainerDeletion(cable2); + } + + private bool CanRefloodNode(EntityUid uid, Node node) + { + return node.Owner == uid + && !node.Deleting + && !TerminatingOrDeleted(uid) + && !EntityManager.IsQueuedForDeletion(uid) + && HasComp(uid); + } + + private void QueueNodeContainerDeletion(EntityUid uid) + { + if (TerminatingOrDeleted(uid)) + return; + + if (TryComp(uid, out var container)) + { + foreach (var node in container.Nodes.Values) + { + node.Deleting = true; + _nodeGroupSystem.QueueNodeRemove(node); + } + } + + if (!EntityManager.IsQueuedForDeletion(uid)) + QueueDel(uid); } } diff --git a/Content.Server/Imperial/Medieval/Power/EntitySystems/RandomPowerSystem.cs b/Content.Server/Imperial/Medieval/Power/EntitySystems/RandomPowerSystem.cs index 6f97b698d66..3a7e3e8083b 100644 --- a/Content.Server/Imperial/Medieval/Power/EntitySystems/RandomPowerSystem.cs +++ b/Content.Server/Imperial/Medieval/Power/EntitySystems/RandomPowerSystem.cs @@ -50,9 +50,13 @@ private void OnMapInit(Entity ent, ref MapInitEvent args) _prototypeVoltages[prototypeId] = voltage; } - if (!_nodeContainer.TryGetNode(ent.Owner, ent.Comp.Node, out var node)) + if (!_nodeContainer.TryGetNode(ent.Owner, ent.Comp.Node, out var node) + || !CanRefloodNode(ent.Owner, node)) return; + if (node.NodeGroup != null) + _nodeGroupSystem.QueueNodeRemove(node); + node.SetNodeGroupID(voltage); if (TryComp(ent, out var consumer)) @@ -64,6 +68,16 @@ private void OnMapInit(Entity ent, ref MapInitEvent args) var state = ent.Comp.AvailableVoltages[voltage]; _appearance.SetData(ent, RandomPowerVisuals.Voltage, state); - _nodeGroupSystem.QueueReflood(node); + if (CanRefloodNode(ent.Owner, node)) + _nodeGroupSystem.QueueReflood(node); + } + + private bool CanRefloodNode(EntityUid uid, Node node) + { + return node.Owner == uid + && !node.Deleting + && !TerminatingOrDeleted(uid) + && !EntityManager.IsQueuedForDeletion(uid) + && HasComp(uid); } } diff --git a/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexDeviceNode.cs b/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexDeviceNode.cs index 892603723cc..34da3378319 100644 --- a/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexDeviceNode.cs +++ b/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexDeviceNode.cs @@ -2,8 +2,6 @@ using Content.Server.NodeContainer.Nodes; using Content.Shared.NodeContainer; using Robust.Shared.Map.Components; -using Robust.Shared.Map; -using Robust.Shared.Maths; namespace Content.Server.Imperial.Medieval.Power; @@ -14,23 +12,29 @@ public partial class MyrmexDeviceNode : Node public DirectionFlag ConnectDirections = DirectionFlag.None; public override IEnumerable GetReachableNodes( - TransformComponent xform, + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + // if (!xform.Anchored || grid == null) + // yield break; + // var gridIndex = grid.TileIndicesFor(xform.Coordinates); + // foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex)) + + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex)) + foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (dir == Direction.Invalid) continue; - if ((ConnectDirections == DirectionFlag.None || (ConnectDirections & dir.AsFlag()) != 0) + if ((ConnectDirections == DirectionFlag.None || (ConnectDirections & dir.AsFlag()) != 0) && node is MyrmexPipeNode pipeNode) { if (CanPipeConnectFromDirection(pipeNode, dir.GetOpposite(), entMan, xformQuery)) @@ -38,8 +42,7 @@ public override IEnumerable GetReachableNodes( } } - - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node != this && node is MyrmexPipeNode) yield return node; diff --git a/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexPipeNode.cs b/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexPipeNode.cs index 48ca5887469..2de5513f730 100644 --- a/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexPipeNode.cs +++ b/Content.Server/Imperial/Medieval/Power/Nodes/MyrmexPipeNode.cs @@ -1,9 +1,6 @@ using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; -using Content.Server.Power.Components; -using Content.Server.Power.Nodes; using Content.Shared.NodeContainer; -using Robust.Shared.Map; using Robust.Shared.Map.Components; namespace Content.Server.Imperial.Medieval.Power; @@ -29,24 +26,26 @@ public override bool Connectable(IEntityManager entMan, TransformComponent? xfor return base.Connectable(entMan, xform); } - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); - + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); + if (!entMan.TryGetComponent(Owner, out var pipeComp)) yield break; - var rotation = xform.LocalRotation.GetCardinalDir(); + var rotation = xform.Comp.LocalRotation.GetCardinalDir(); var allowedDirections = GetAllowedDirections(pipeComp.PipeType, rotation); - foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex)) + foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (dir == Direction.Invalid) continue; @@ -61,23 +60,20 @@ public override IEnumerable GetReachableNodes(TransformComponent xform, yield return node; } - - else if (node is MyrmexDeviceNode deviceNode) + else if (node is MyrmexDeviceNode) { if (allowedDirections.Contains(dir)) yield return node; } } - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node == this) continue; if (node is MyrmexDeviceNode && entMan.HasComponent(node.Owner)) - { yield return node; - } } } @@ -88,26 +84,22 @@ private HashSet GetAllowedDirections(MyrmexPipeType pipeType, Directi switch (pipeType) { case MyrmexPipeType.Straight: - // Прямая: вперед и назад относительно ротации directions.Add(rotation); directions.Add(rotation.GetOpposite()); break; case MyrmexPipeType.Corner: - // Угловая: вперед и вправо относительно ротации directions.Add(rotation); - directions.Add(rotation.GetClockwise90Degrees()); // Поворот на 90° по часовой + directions.Add(rotation.GetClockwise90Degrees()); break; case MyrmexPipeType.TJunction: - // Т-образная: вперед, назад и вправо относительно ротации directions.Add(rotation); directions.Add(rotation.GetOpposite()); - directions.Add(rotation.GetClockwise90Degrees()); // Поворот на 90° по часовой + directions.Add(rotation.GetClockwise90Degrees()); break; case MyrmexPipeType.Cross: - // Перекрестная: все 4 направления directions.Add(Direction.North); directions.Add(Direction.South); directions.Add(Direction.East); @@ -118,7 +110,7 @@ private HashSet GetAllowedDirections(MyrmexPipeType pipeType, Directi return directions; } - private bool CanOtherPipeConnect(MyrmexPipeNode otherNode, Direction oppositeDir, + private bool CanOtherPipeConnect(MyrmexPipeNode otherNode, Direction oppositeDir, IEntityManager entMan, EntityQuery xformQuery) { if (!entMan.TryGetComponent(otherNode.Owner, out var otherPipeComp)) @@ -132,4 +124,4 @@ private bool CanOtherPipeConnect(MyrmexPipeNode otherNode, Direction oppositeDir return otherAllowedDirs.Contains(oppositeDir); } -} \ No newline at end of file +} diff --git a/Content.Server/Imperial/Medieval/Quest/DamageCheckSystem.cs b/Content.Server/Imperial/Medieval/Quest/DamageCheckSystem.cs index 0585ce7c2fb..cce0213f275 100644 --- a/Content.Server/Imperial/Medieval/Quest/DamageCheckSystem.cs +++ b/Content.Server/Imperial/Medieval/Quest/DamageCheckSystem.cs @@ -1,9 +1,13 @@ -using Content.Shared.Examine; +using Content.Shared.Examine; using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; namespace Content.Server.DamageCheck; public partial class DamageCheckSystem : EntitySystem { + [Dependency] private readonly DamageableSystem _damageable = default!; + public override void Initialize() { base.Initialize(); @@ -13,15 +17,16 @@ private void OnExamine(EntityUid uid, DamageCheckableComponent comp, ExaminedEve { // Bad shitcode for gates. Fix later if (!TryComp(uid, out var damageable)) return; - if (damageable.TotalDamage > 3600) + var total = _damageable.GetTotalDamage((uid, damageable)); + if (total > 3600) args.PushMarkup("[color=red]Объект весь покрыт крупными трещинами и вот-вот развалится[/color]"); - else if (damageable.TotalDamage > 2700) + else if (total > 2700) args.PushMarkup("[color=orange]Объект весь покрыт крупными трещинами[/color]"); - else if (damageable.TotalDamage > 1800) + else if (total > 1800) args.PushMarkup("[color=orange]По объекту расходятся трещины[/color]"); - else if (damageable.TotalDamage > 900) + else if (total > 900) args.PushMarkup("[color=yellow]Заметны серьезные царапины[/color]"); - else if (damageable.TotalDamage > 220) + else if (total > 220) args.PushMarkup("[color=green]Заметны легкие царапины[/color]"); } diff --git a/Content.Server/Imperial/Medieval/Quest/QuestSystem.cs b/Content.Server/Imperial/Medieval/Quest/QuestSystem.cs index 0d382449c89..87a966d2cbb 100644 --- a/Content.Server/Imperial/Medieval/Quest/QuestSystem.cs +++ b/Content.Server/Imperial/Medieval/Quest/QuestSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Server.Quest.Components; using Content.Shared.Speech; using Content.Shared.Random.Helpers; @@ -14,6 +14,7 @@ using Content.Server.Chat.Systems; using Content.Server.Imperial.Medieval.Trading; using Content.Shared.Storage.Components; +using Content.Shared.Chat; namespace Content.Server.Quest; public partial class QuestSystem : EntitySystem @@ -152,7 +153,7 @@ private void OnStartPallete(EntityUid uid, PalletContractComponent comp, Compone foreach (var spy in EntityManager.EntityQuery()) { EnsureComp(spy.Owner); - _chat.TrySendInGameICMessage(spy.Owner, "Замечено прибытие ценного груза... его собираются доставить в " + comp.ContractPartner +"!!!", InGameICChatType.Speak, false); + _chat.TrySendInGameICMessage(spy.Owner, " ... " + comp.ContractPartner +"!!!", InGameICChatType.Speak, false); } } @@ -165,17 +166,17 @@ private void OnStartPallete2(EntityUid uid, PalletStorageComponent comp, Compone private void OnExamine(EntityUid uid, QuestContractComponent comp, ExaminedEvent args) { - args.PushMarkup("[color=sandybrown]Тип контракта: [/color]добыча"); - args.PushMarkup("[color=lightgreen]Место сдачи: [/color]" + comp.ContractPartner); - args.PushMarkup("[color=orange]Тип добычи: [/color]" + comp.ContractName); - args.PushMarkup("[color=red]Необходимое количество: [/color]" + comp.Amount); - args.PushMarkup("[color=yellow]Награда: [/color]" + comp.Reward); + args.PushMarkup("[color=sandybrown] : [/color]"); + args.PushMarkup("[color=lightgreen] : [/color]" + comp.ContractPartner); + args.PushMarkup("[color=orange] : [/color]" + comp.ContractName); + args.PushMarkup("[color=red] : [/color]" + comp.Amount); + args.PushMarkup("[color=yellow]: [/color]" + comp.Reward); } private void OnExaminePallete(EntityUid uid, PalletContractComponent comp, ExaminedEvent args) { - args.PushMarkup("[color=sandybrown]Тип контракта: [/color]доставка"); - args.PushMarkup("[color=lightgreen]Место сдачи: [/color]" + comp.ContractPartner); - args.PushMarkup("[color=yellow]Награда: [/color]" + comp.Reward); + args.PushMarkup("[color=sandybrown] : [/color]"); + args.PushMarkup("[color=lightgreen] : [/color]" + comp.ContractPartner); + args.PushMarkup("[color=yellow]: [/color]" + comp.Reward); } } diff --git a/Content.Server/Imperial/Medieval/Revive/KillsUntilReviveSystem.cs b/Content.Server/Imperial/Medieval/Revive/KillsUntilReviveSystem.cs index 0b6610a7131..a67dec3c9d0 100644 --- a/Content.Server/Imperial/Medieval/Revive/KillsUntilReviveSystem.cs +++ b/Content.Server/Imperial/Medieval/Revive/KillsUntilReviveSystem.cs @@ -14,7 +14,9 @@ using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible; namespace Content.Server.Imperial.Medieval.Revive { public sealed class KillsUntilReviveSystem : EntitySystem diff --git a/Content.Server/Imperial/Medieval/SSDFree/SSDFreeSystem.cs b/Content.Server/Imperial/Medieval/SSDFree/SSDFreeSystem.cs index 4a948d61f82..29377c0db31 100644 --- a/Content.Server/Imperial/Medieval/SSDFree/SSDFreeSystem.cs +++ b/Content.Server/Imperial/Medieval/SSDFree/SSDFreeSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Damage; using Robust.Shared.Player; using Content.Shared.Inventory; +using Content.Shared.Damage.Systems; namespace Content.Server.SSDFree { diff --git a/Content.Server/Imperial/Medieval/Ships/PlayerDrowning/PlayerDrowningSystem.cs b/Content.Server/Imperial/Medieval/Ships/PlayerDrowning/PlayerDrowningSystem.cs index eee1bf6fb64..c52ba0fb41e 100644 --- a/Content.Server/Imperial/Medieval/Ships/PlayerDrowning/PlayerDrowningSystem.cs +++ b/Content.Server/Imperial/Medieval/Ships/PlayerDrowning/PlayerDrowningSystem.cs @@ -19,6 +19,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.Ships.PlayerDrowning; diff --git a/Content.Server/Imperial/Medieval/Ships/ShipBuyTerminal/ShipBuyTerminalSystem.cs b/Content.Server/Imperial/Medieval/Ships/ShipBuyTerminal/ShipBuyTerminalSystem.cs index fb84c311264..cb44412abe8 100644 --- a/Content.Server/Imperial/Medieval/Ships/ShipBuyTerminal/ShipBuyTerminalSystem.cs +++ b/Content.Server/Imperial/Medieval/Ships/ShipBuyTerminal/ShipBuyTerminalSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.Store.Components; using System; using System.Linq; using System.Numerics; @@ -306,7 +307,7 @@ private void OnRequestWithdraw(EntityUid uid, ShipBuyTerminalComponent component if (amountToSpawn <= 0) continue; - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates); if (ents.FirstOrDefault() is { } ent) _hands.PickupOrDrop(user, ent); diff --git a/Content.Server/Imperial/Medieval/Ships/Wind/ServerWindSystem.cs b/Content.Server/Imperial/Medieval/Ships/Wind/ServerWindSystem.cs index f4376d6505c..d5872f70c94 100644 --- a/Content.Server/Imperial/Medieval/Ships/Wind/ServerWindSystem.cs +++ b/Content.Server/Imperial/Medieval/Ships/Wind/ServerWindSystem.cs @@ -155,16 +155,21 @@ private void UpdateStormWindPower(float stormLevel) private void UpdateStormWeather(float stormLevel) { + // var rainWeatherId = new ProtoId(_cfg.GetCVar(ShipsCCVars.StormRainWeather)); + // WeatherPrototype? rain = null; + // if (rainLevelReached && !_prototype.TryIndex(rainWeatherId, out rain)) + // return; + // ... + // _weather.SetWeather(mapId, rain!, null); + var rainLevelReached = stormLevel >= _cfg.GetCVar(ShipsCCVars.StormRainLevel); - var rainWeatherId = new ProtoId(_cfg.GetCVar(ShipsCCVars.StormRainWeather)); - WeatherPrototype? rain = null; - if (rainLevelReached && !_prototype.TryIndex(rainWeatherId, out rain)) + var rainWeatherId = new EntProtoId(_cfg.GetCVar(ShipsCCVars.StormRainWeather)); + if (rainLevelReached && !_prototype.TryIndex(rainWeatherId, out _)) return; var stormLevelReached = stormLevel >= _cfg.GetCVar(ShipsCCVars.StormStormLevel); - var stormWeatherId = new ProtoId(_cfg.GetCVar(ShipsCCVars.StormStormWeather)); - WeatherPrototype? storm = null; - if (stormLevelReached && !_prototype.TryIndex(stormWeatherId, out storm)) + var stormWeatherId = new EntProtoId(_cfg.GetCVar(ShipsCCVars.StormStormWeather)); + if (stormLevelReached && !_prototype.TryIndex(stormWeatherId, out _)) return; var seaMaps = new HashSet(); @@ -178,35 +183,20 @@ private void UpdateStormWeather(float stormLevel) continue; if (rainLevelReached) - _weather.SetWeather(mapId, rain!, null); + _weather.TryAddWeather(mapId, rainWeatherId, out _); else - DisableWeather(mapId, rainWeatherId); + _weather.TryRemoveWeather(mapId, rainWeatherId); if (stormLevelReached) - _weather.SetWeather(mapId, storm!, null); + _weather.TryAddWeather(mapId, stormWeatherId, out _); else - DisableWeather(mapId, stormWeatherId); - } - } - - private void DisableWeather(MapId mapId, ProtoId weatherId) - { - if (!_mapSystem.TryGetMap(mapId, out var mapUid) || - !TryComp(mapUid.Value, out var weatherComp)) - { - return; + _weather.TryRemoveWeather(mapId, stormWeatherId); } - - if (!weatherComp.Weather.TryGetValue(weatherId, out var weatherData)) - return; - - var endTime = _timing.CurTime + WeatherComponent.ShutdownTime; - if (weatherData.EndTime != null && weatherData.EndTime <= endTime) - return; - - weatherData.EndTime = endTime; - Dirty(mapUid.Value, weatherComp); } + // private void DisableWeather(MapId mapId, ProtoId weatherId) + // { + // ... + // } private void UpdateStormParallax(float stormLevel) { diff --git a/Content.Server/Imperial/Medieval/Skills/Systems/SkillsSystem.Vitality.cs b/Content.Server/Imperial/Medieval/Skills/Systems/SkillsSystem.Vitality.cs index e0dc3bd03c9..89116bf6e2c 100644 --- a/Content.Server/Imperial/Medieval/Skills/Systems/SkillsSystem.Vitality.cs +++ b/Content.Server/Imperial/Medieval/Skills/Systems/SkillsSystem.Vitality.cs @@ -14,6 +14,7 @@ using Content.Shared.Movement.Pulling.Components; using Robust.Server.GameObjects; using Robust.Shared.Physics.Components; +using Content.Shared.Chat; namespace Content.Server.Imperial.Medieval.Skills; diff --git a/Content.Server/Imperial/Medieval/Sleepy/Systems/NeedSleepSystem.cs b/Content.Server/Imperial/Medieval/Sleepy/Systems/NeedSleepSystem.cs index 55f58fc35ff..1f6a41efb0b 100644 --- a/Content.Server/Imperial/Medieval/Sleepy/Systems/NeedSleepSystem.cs +++ b/Content.Server/Imperial/Medieval/Sleepy/Systems/NeedSleepSystem.cs @@ -84,9 +84,7 @@ public override void Update(float frameTime) if (HasComp(uid)) continue; - var emote = _random.Pick(comp.Emotes); - if (_random.Prob(comp.SleepLevel / 450f) && comp.SleepLevel > 65 && !HasComp(uid)) - _chatSystem.TryEmoteWithChat(uid, emote, ChatTransmitRange.Normal); + TryPlaySleepEmote(uid, comp); if (comp.SleepLevel > 96.5f) { @@ -99,5 +97,14 @@ public override void Update(float frameTime) EnsureComp(uid); } } + + private void TryPlaySleepEmote(EntityUid uid, NeedSleepComponent component) + { + if (component.Emotes.Count == 0 || component.SleepLevel <= 65f || !_random.Prob(component.SleepLevel / 450f)) + return; + + var emote = _random.Pick(component.Emotes); + _chatSystem.TryEmoteWithChat(uid, emote, ChatTransmitRange.Normal); + } } } diff --git a/Content.Server/Imperial/Medieval/SmithingSystem/SmithingSystem.Behaviors.cs b/Content.Server/Imperial/Medieval/SmithingSystem/SmithingSystem.Behaviors.cs index e23183111d4..0696c566955 100644 --- a/Content.Server/Imperial/Medieval/SmithingSystem/SmithingSystem.Behaviors.cs +++ b/Content.Server/Imperial/Medieval/SmithingSystem/SmithingSystem.Behaviors.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Server.Damage.Components; using Content.Server.MedievalMeleeResource; using Content.Shared.IdentityManagement; @@ -8,6 +8,8 @@ using Content.Shared.Imperial.Medieval.SmithingSystem.Events; using Content.Shared.MedievalMeleeResource.Components; using Content.Shared.Weapons.Melee; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.SmithingSystem; @@ -15,16 +17,17 @@ public sealed partial class SmithingSystem { private readonly Dictionary _itemQualityDecorators = new() { - { ItemQuality.Worst, "отвр. " }, - { ItemQuality.ReallyBad, "ужс. " }, - { ItemQuality.Bad, "плох. " }, - { ItemQuality.Default, "хор. " }, - { ItemQuality.Good, "отл. " }, - { ItemQuality.Excellent, "шедевр. " }, + { ItemQuality.Worst, "����. " }, + { ItemQuality.ReallyBad, "���. " }, + { ItemQuality.Bad, "����. " }, + { ItemQuality.Default, "���. " }, + { ItemQuality.Good, "���. " }, + { ItemQuality.Excellent, "������. " }, }; [Dependency] private readonly MedievalMeleeResourceSystem _meleeResource = default!; [Dependency] private readonly MedievalArmorIntegritySystem _armorIntegrity = default!; + [Dependency] private readonly SharedDamageOtherOnHitSystem _damageOtherOnHit = default!; private void InitializeBehaviors() { @@ -159,8 +162,7 @@ private void UpgradeWeapon(Entity ent, re { if (TryComp(item, out DamageOtherOnHitComponent? damageOtherOnHitComponent)) { - damageOtherOnHitComponent.Damage *= modifier.Modifier; - Dirty(item, damageOtherOnHitComponent); + _damageOtherOnHit.ScaleDamage((item, damageOtherOnHitComponent), modifier.Modifier); } if (TryComp(item, out MeleeWeaponComponent? meleeWeaponComponent)) diff --git a/Content.Server/Imperial/Medieval/SoulboundTrigger/SoulboundTriggerSystem.cs b/Content.Server/Imperial/Medieval/SoulboundTrigger/SoulboundTriggerSystem.cs index 85edc46a3e2..bcb6ba590ad 100644 --- a/Content.Server/Imperial/Medieval/SoulboundTrigger/SoulboundTriggerSystem.cs +++ b/Content.Server/Imperial/Medieval/SoulboundTrigger/SoulboundTriggerSystem.cs @@ -36,7 +36,7 @@ private void OnPlayerSpawn(PlayerSpawnCompleteEvent args) private void OnGotEquipped(EntityUid uid, SoulboundTriggerComponent component, GotEquippedEvent args) { if (component.User == null) TryBindSoul(uid, component); - else if (component.User != args.Equipee) _trigger.Trigger(uid, component.User, component.KeyOut); + else if (component.User != args.EquipTarget) _trigger.Trigger(uid, component.User, component.KeyOut); } public void TryBindSoul(EntityUid uid, SoulboundTriggerComponent? component = null) diff --git a/Content.Server/Imperial/Medieval/Species/ZveresAccentSystem.cs b/Content.Server/Imperial/Medieval/Species/ZveresAccentSystem.cs index c1976f31298..c78b733a980 100644 --- a/Content.Server/Imperial/Medieval/Species/ZveresAccentSystem.cs +++ b/Content.Server/Imperial/Medieval/Species/ZveresAccentSystem.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Content.Server.Speech.Components; using Content.Shared.Speech; using Robust.Shared.Random; @@ -8,6 +8,9 @@ namespace Content.Server.Speech.EntitySystems; public sealed class ZveresAccentSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + private static readonly Regex RegexUpperR = new(@"Р+", RegexOptions.Compiled); + private static readonly Regex RegexLowerR = new(@"р+", RegexOptions.Compiled); + public override void Initialize() { base.Initialize(); @@ -17,15 +20,13 @@ public override void Initialize() private void OnAccent(EntityUid uid, ZveresAccentComponent component, AccentGetEvent args) { var message = args.Message; - message = Regex.Replace( + message = RegexUpperR.Replace( message, - "Р+", - _random.Pick(new List() { "Рр", "Ррр" }) + _ => _random.Pick(new List() { "Рр", "Ррр" }) ); - message = Regex.Replace( + message = RegexLowerR.Replace( message, - "р+", - _random.Pick(new List() { "рр", "ррр" }) + _ => _random.Pick(new List() { "рр", "ррр" }) ); args.Message = message; } diff --git a/Content.Server/Imperial/Medieval/SpeechInFight/SpeechInFightSystem.cs b/Content.Server/Imperial/Medieval/SpeechInFight/SpeechInFightSystem.cs index 24f42f720ab..3aebdb6f4b0 100644 --- a/Content.Server/Imperial/Medieval/SpeechInFight/SpeechInFightSystem.cs +++ b/Content.Server/Imperial/Medieval/SpeechInFight/SpeechInFightSystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Content.Shared.Chat; namespace Content.Server.Imperial.Medieval.SpeechInFight; diff --git a/Content.Server/Imperial/Medieval/TempInvincibility/TempInvincibilitySystem.cs b/Content.Server/Imperial/Medieval/TempInvincibility/TempInvincibilitySystem.cs index 02e4031ea54..52581b6d937 100644 --- a/Content.Server/Imperial/Medieval/TempInvincibility/TempInvincibilitySystem.cs +++ b/Content.Server/Imperial/Medieval/TempInvincibility/TempInvincibilitySystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Damage; using Content.Shared.FixedPoint; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Imperial.Medieval.TempInvincibility; diff --git a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Interaction.cs b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Interaction.cs index 81d23e1a092..255e42e4b75 100644 --- a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Interaction.cs +++ b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Interaction.cs @@ -4,6 +4,7 @@ using Content.Shared.Trade; using Content.Shared.Verbs; using Robust.Shared.Player; +using Content.Shared.Damage.Systems; namespace Content.Server.Trade; diff --git a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Storage.cs b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Storage.cs index 69fc51f1c55..417c4794fe5 100644 --- a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Storage.cs +++ b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.Storage.cs @@ -102,7 +102,7 @@ private bool TryInsertOfferAt( TryComp(item, out var heldStack) && heldStack.Count > amount) { - var split = _stack.Split(item, amount, Transform(uid).Coordinates, heldStack); + var split = _stack.Split((item, heldStack), amount, Transform(uid).Coordinates); if (split == null) { _popup.PopupEntity(Loc.GetString("trade-terminal-insert-failed"), uid, user); @@ -134,12 +134,11 @@ private bool TryMergeOfferStacks( { if (!TryComp(item, out var insertStack) || !TryComp(target, out var targetStack) || - !_stack.TryAdd( - item, - target, - amount > 0 ? Math.Min(amount, insertStack.Count) : insertStack.Count, - insertStack, - targetStack)) + !_stack.TryMergeStacks( + (item, insertStack), + (target, targetStack), + out _, + amount > 0 ? Math.Min(amount, insertStack.Count) : insertStack.Count)) { return false; } diff --git a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.cs b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.cs index cbb41db7398..54e8cbfbfb3 100644 --- a/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.cs +++ b/Content.Server/Imperial/Medieval/TradeTerminal/TradeTerminalSystem.cs @@ -16,6 +16,7 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Timing; +using Content.Shared.Damage.Systems; namespace Content.Server.Trade; diff --git a/Content.Server/Imperial/Medieval/Trading/TradingPitCollectSystem.cs b/Content.Server/Imperial/Medieval/Trading/TradingPitCollectSystem.cs index 71225c9f1ba..c1ac659568d 100644 --- a/Content.Server/Imperial/Medieval/Trading/TradingPitCollectSystem.cs +++ b/Content.Server/Imperial/Medieval/Trading/TradingPitCollectSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Interaction; using Content.Shared.Item; using Content.Shared.Storage; +using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.Verbs; using Robust.Shared.Containers; diff --git a/Content.Server/Imperial/Medieval/Trading/TradingSystem.Ui.cs b/Content.Server/Imperial/Medieval/Trading/TradingSystem.Ui.cs index 1207d4d5be8..64740713bbf 100644 --- a/Content.Server/Imperial/Medieval/Trading/TradingSystem.Ui.cs +++ b/Content.Server/Imperial/Medieval/Trading/TradingSystem.Ui.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Server.Imperial.Medieval.Achievements; using Content.Server.Actions; using Content.Server.Administration.Logs; @@ -185,7 +185,7 @@ private void OnRequestWithdraw(EntityUid uid, TradingComponent component, Tradin { var cashId = proto.Cash[value]; var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value)); - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates); if (ents.FirstOrDefault() is {} ent) _hands.PickupOrDrop(buyer, ent); amountRemaining -= value * amountToSpawn; diff --git a/Content.Server/Imperial/Medieval/Traits/MedievalDiggerSystem.cs b/Content.Server/Imperial/Medieval/Traits/MedievalDiggerSystem.cs index 2c6273bb719..02f64de2c89 100644 --- a/Content.Server/Imperial/Medieval/Traits/MedievalDiggerSystem.cs +++ b/Content.Server/Imperial/Medieval/Traits/MedievalDiggerSystem.cs @@ -8,6 +8,8 @@ using Content.Shared.Weapons.Melee.Events; using Content.Server.Imperial.Medieval.GameTicking.Rules; using Content.Shared.Imperial.Medieval.GameTicking.Rules; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.MedievalDigger { @@ -39,7 +41,7 @@ private void OnDamage(EntityUid uid, MedievalDiggAbleComponent component, Damage var xform = Transform(uid); var coords = xform.Coordinates; - if (CheckDiggersNearby(coords) && TryComp(uid, out var damageable) && damageable.TotalDamage < 400f && !component.Digged) + if (CheckDiggersNearby(coords) && TryComp(uid, out var damageable) && _damageableSystem.GetTotalDamage((uid, damageable)) < 400f && !component.Digged) { component.Digged = true; _damageableSystem.TryChangeDamage(uid, component.Damage, true); diff --git a/Content.Server/Imperial/Medieval/Traits/MedievalMoneyCheckerSystem.cs b/Content.Server/Imperial/Medieval/Traits/MedievalMoneyCheckerSystem.cs index 1e5be4227e4..215c44e6ab5 100644 --- a/Content.Server/Imperial/Medieval/Traits/MedievalMoneyCheckerSystem.cs +++ b/Content.Server/Imperial/Medieval/Traits/MedievalMoneyCheckerSystem.cs @@ -1,6 +1,6 @@ using Content.Server.MedievalMoneyChecker.Components; using Content.Shared.Examine; -using Content.Server.Store.Components; +using Content.Shared.Store.Components; using Robust.Shared.Utility; namespace Content.Server.MedievalMoneyChecker diff --git a/Content.Server/Imperial/Medieval/Waystones/WaystoneSystem.cs b/Content.Server/Imperial/Medieval/Waystones/WaystoneSystem.cs index 5c11a4382fd..18b9ebf0259 100644 --- a/Content.Server/Imperial/Medieval/Waystones/WaystoneSystem.cs +++ b/Content.Server/Imperial/Medieval/Waystones/WaystoneSystem.cs @@ -20,6 +20,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Chat; public sealed class WaystoneSystem : EntitySystem { diff --git a/Content.Server/Imperial/Medieval/WhitelistClothing/WhitelistClothingSystems.cs b/Content.Server/Imperial/Medieval/WhitelistClothing/WhitelistClothingSystems.cs index 5a5869082fe..430efc68c29 100644 --- a/Content.Server/Imperial/Medieval/WhitelistClothing/WhitelistClothingSystems.cs +++ b/Content.Server/Imperial/Medieval/WhitelistClothing/WhitelistClothingSystems.cs @@ -2,7 +2,6 @@ using Content.Shared.Tag; using Content.Shared.Inventory.Events; using Content.Shared.Inventory; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Content.Server.Imperial.WhitelistClothing.Systems; @@ -18,9 +17,9 @@ public override void Initialize() } private void OnEquipAttempt(DidEquipEvent args) { - if (TryComp(args.Equipee, out var component) && component.WhitelistState == "humanoid" && !_tagSystem.HasTag(args.Equipment, component.Whitelist) && args.Slot.Equals("outerclothing", StringComparison.CurrentCultureIgnoreCase)) - _inventorySystem.TryUnequip(args.Equipee, args.Slot); - else if (TryComp(args.Equipment, out var componentEquipment) && componentEquipment.WhitelistState == "clothing" && !HasComp(args.Equipee)) - _inventorySystem.TryUnequip(args.Equipee, args.Slot); + if (TryComp(args.EquipTarget, out var component) && component.WhitelistState == "humanoid" && !_tagSystem.HasTag(args.Equipment, component.Whitelist) && args.Slot.Equals("outerclothing", StringComparison.CurrentCultureIgnoreCase)) + _inventorySystem.TryUnequip(args.EquipTarget, args.Slot); + else if (TryComp(args.Equipment, out var componentEquipment) && componentEquipment.WhitelistState == "clothing" && !HasComp(args.EquipTarget)) + _inventorySystem.TryUnequip(args.EquipTarget, args.Slot); } } diff --git a/Content.Server/Imperial/Medieval/Workbench/CP14WorkbenchSystem.cs b/Content.Server/Imperial/Medieval/Workbench/CP14WorkbenchSystem.cs index 2ba8ba77fb9..16b9a0d675b 100644 --- a/Content.Server/Imperial/Medieval/Workbench/CP14WorkbenchSystem.cs +++ b/Content.Server/Imperial/Medieval/Workbench/CP14WorkbenchSystem.cs @@ -19,6 +19,7 @@ using Content.Server.SpikeTrap.Components; using Content.Server.MagicBarrier.Components; using Content.Server.Imperial.Medieval.GameTicking.Rules; +using Content.Shared.FixedPoint; using Content.Shared.Imperial.Medieval.GameTicking.Rules; namespace Content.Server._CP14.Workbench; @@ -82,7 +83,7 @@ private void OnCraftFinished(Entity ent, ref CP14CraftDo _solutionContainer.TryGetSolution(resultEntity, recipe.Solution, out var resultSoln, out var resultSolution); if (recipe.TryMergeSolutions && resultSoln is not null) { - resultSoln.Value.Comp.Solution.MaxVolume = 0; + _solutionContainer.SetCapacity(resultSoln.Value, FixedPoint2.Zero); _solutionContainer.RemoveAllSolution(resultSoln.Value); //If we combine ingredient solutions, we do not use the default solution prescribed in the entity. } @@ -102,7 +103,8 @@ private void OnCraftFinished(Entity ent, ref CP14CraftDo && resultSoln is not null && _solutionContainer.TryGetSolution(placedEntity, recipe.Solution, out var ingredientSoln, out var ingredientSolution)) { - resultSoln.Value.Comp.Solution.MaxVolume += ingredientSoln.Value.Comp.Solution.MaxVolume; + var newMax = resultSoln.Value.Comp.Solution.MaxVolume + ingredientSoln.Value.Comp.Solution.MaxVolume; + _solutionContainer.SetCapacity(resultSoln.Value, newMax); _solutionContainer.TryAddSolution(resultSoln.Value, ingredientSolution); } diff --git a/Content.Server/Imperial/Medieval/XxRaay/Systems/MedievalAmbientToggleSystem.cs b/Content.Server/Imperial/Medieval/XxRaay/Systems/MedievalAmbientToggleSystem.cs index 8f15ed575ff..d1e11257abc 100644 --- a/Content.Server/Imperial/Medieval/XxRaay/Systems/MedievalAmbientToggleSystem.cs +++ b/Content.Server/Imperial/Medieval/XxRaay/Systems/MedievalAmbientToggleSystem.cs @@ -18,12 +18,12 @@ public override void Initialize() } /// - /// Current state: true = medieval ambient music can play, false = disabled. + /// public bool IsMedievalAmbientEnabled => _medievalAmbientEnabled; /// - /// Sets medieval ambient on/off and notifies all clients. Returns new state. + /// public bool SetMedievalAmbientEnabled(bool enabled) { @@ -35,7 +35,7 @@ public bool SetMedievalAmbientEnabled(bool enabled) } /// - /// Toggles medieval ambient and returns new state. + /// public bool ToggleMedievalAmbient() { diff --git a/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.PiercingLunge.cs b/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.PiercingLunge.cs new file mode 100644 index 00000000000..30a4937f8e9 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.PiercingLunge.cs @@ -0,0 +1,24 @@ +using Content.Shared.Damage.Systems; +using Content.Shared.Imperial.MiningWeapons.EmpoweredAttacks.PiercingLunge; +using Robust.Shared.Physics.Events; + +namespace Content.Server.Imperial.MiningWeapons.EmpoweredAttacks; + +public sealed partial class AttacksSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + private void InitializePiercingLunge() + { + SubscribeLocalEvent(OnStartCollide); + } + + private void OnStartCollide(EntityUid user, UserPiercingLungeComponent userComp, StartCollideEvent args) + { + if (!userComp.CanDamage) + return; + + if (userComp.Damage != null) + _damageable.TryChangeDamage(args.OtherEntity, userComp.Damage, interruptsDoAfters: false); + } +} diff --git a/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.cs b/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.cs new file mode 100644 index 00000000000..e2b79829319 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/EmpoweredAttacks/AttacksSystem.cs @@ -0,0 +1,13 @@ +using Content.Shared.Imperial.MiningWeapons.EmpoweredAttacks; + +namespace Content.Server.Imperial.MiningWeapons.EmpoweredAttacks; + +public sealed partial class AttacksSystem : SharedAttacksSystem +{ + public override void Initialize() + { + base.Initialize(); + + InitializePiercingLunge(); + } +} diff --git a/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Activation.cs b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Activation.cs new file mode 100644 index 00000000000..e55e6133652 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Activation.cs @@ -0,0 +1,65 @@ +using Content.Shared.Imperial.MiningWeapons.Smasher; +using Content.Shared.Imperial.MiningWeapons.Smasher.Components; + +namespace Content.Server.Imperial.MiningWeapons.Smasher; + +public sealed partial class SmasherSystem +{ + #region Shield Activation + private void CompleteCharging(EntityUid user, EntityUid smasherUid, SmasherComponent smasher) + { + smasher.ActiveCharges.Remove(smasherUid); + ActivateShield(smasherUid, smasher, user); + + if (HasComp(user)) + { + RemComp(user); + _movementSpeed.RefreshMovementSpeedModifiers(user); + } + } + + public void ActivateShield(EntityUid smasherUid, SmasherComponent smasher, EntityUid user) + { + if (HasComp(user)) + { + RemComp(user); + _movementSpeed.RefreshMovementSpeedModifiers(user); + } + + if (HasComp(user)) + return; + + var shieldActive = EnsureComp(user); + shieldActive.EffectActived = smasher.EffectActived; + shieldActive.SmasherUid = smasherUid; + shieldActive.EndTime = _timing.CurTime + smasher.NextActiveShieldTime; + Dirty(user, shieldActive); + + SetCooldown(smasherUid, smasher, smasher.ActiveShieldCooldown); + ShieldActivated(user, smasher); + } + + private void ShieldActivated(EntityUid user, SmasherComponent smasher) + { + ShowShieldEffect(user, smasher.EffectActived, true); + _audio.PlayPvs(smasher.ActivateSound, user); + } + + #region Helpers + private void StartCharging(EntityUid user, EntityUid smasherUid, SmasherComponent smasher) + { + var chargeData = new ChargeData + { + StartTime = _timing.CurTime, + User = user + }; + smasher.ActiveCharges[smasherUid] = chargeData; + + ShowShieldEffect(user, smasher.EffectCharging, true); + EnsureComp(user); + _movementSpeed.RefreshMovementSpeedModifiers(user); + _audio.PlayPvs(smasher.StartChargingSound, user); + } + #endregion + #endregion +} diff --git a/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Deactivation.cs b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Deactivation.cs new file mode 100644 index 00000000000..a13c071b455 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.Deactivation.cs @@ -0,0 +1,80 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.MiningWeapons.Smasher.Components; +using Robust.Shared.Input; + +namespace Content.Server.Imperial.MiningWeapons.Smasher; + +public sealed partial class SmasherSystem +{ + #region Shield Deactivation + private void CancelCharging(EntityUid user, EntityUid smasherUid, SmasherComponent smasher) + { + smasher.ActiveCharges.Remove(smasherUid); + + if (smasher.EffectDecay != null) + { + ShowShieldEffect(user, smasher.EffectDecay, false); + } + + if (HasComp(user)) + { + RemComp(user); + _movementSpeed.RefreshMovementSpeedModifiers(user); + } + + var decay = EnsureComp(user); + decay.DecayEndTime = _timing.CurTime + smasher.TimeDecay; + + smasher.StateUseKey = BoundKeyState.Up; + _audio.PlayPvs(smasher.DeactivateSound, user); + SetCooldown(smasherUid, smasher, smasher.TimeCooldownDownedDecay); + } + + private void DeactivateShield(EntityUid user, ShieldActiveComponent shield) + { + if (!TryComp(shield.SmasherUid, out var smasher)) + return; + + HideShieldEffect(user); + + smasher.StateUseKey = BoundKeyState.Up; + _audio.PlayPvs(smasher.DeactivateSound, user); + RemComp(user); + } + + private bool CheckDamageInterruption(EntityUid user, SmasherComponent smasher) + { + if (!HasComp(user)) + return false; + + if (!smasher.LastTotalDamage.TryGetValue(user, out var lastDamage)) + { + smasher.LastTotalDamage[user] = _damageableSystem.GetTotalDamage(user); + return false; + } + + var damageReceived = _damageableSystem.GetTotalDamage(user) - lastDamage; + + if (damageReceived > FixedPoint2.New(1.0)) + { + if (smasher.EffectDecay != null) + { + ShowShieldEffect(user, smasher.EffectDecay, false); + } + + if (HasComp(user)) + { + RemComp(user); + _movementSpeed.RefreshMovementSpeedModifiers(user); + } + + smasher.LastTotalDamage.Remove(user); + return true; + } + + smasher.LastTotalDamage[user] = _damageableSystem.GetTotalDamage(user); + return false; + } + #endregion +} diff --git a/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.cs b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.cs new file mode 100644 index 00000000000..9cfda8b0966 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/Smasher/SmasherSystem.cs @@ -0,0 +1,169 @@ +using Content.Shared.Imperial.MiningWeapons.Smasher; +using Content.Shared.Imperial.MiningWeapons.Smasher.Components; +using Content.Shared.Imperial.MiningWeapons.Smasher.Events; +using Content.Shared.Imperial.MiningWeapons; +using Robust.Shared.Input; +using Robust.Shared.Timing; +using Robust.Shared.Audio.Systems; +using Content.Shared.CombatMode; +using Content.Shared.Damage; +using Content.Shared.Movement.Systems; +using Content.Shared.Damage.Systems; + +namespace Content.Server.Imperial.MiningWeapons.Smasher; + +public sealed partial class SmasherSystem : SharedSmasherSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + [Dependency] private readonly MiningWeaponsHelpers _miningWeaponsHelpers = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnUpdateKeyState); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnUserDamageModified); + SubscribeLocalEvent(OnRefreshMovespeed); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + UpdateSmashers(); + UpdateActiveShields(); + UpdateDecayShield(); + } + + #region Update Methods + private void UpdateSmashers() + { + var smasherQuery = EntityQueryEnumerator(); + while (smasherQuery.MoveNext(out var smasherUid, out var smasher)) + { + ProcessSmasher(smasherUid, smasher); + } + } + + private void UpdateActiveShields() + { + var shieldQuery = EntityQueryEnumerator(); + while (shieldQuery.MoveNext(out var uid, out var shield)) + { + if (_timing.CurTime >= shield.EndTime) + { + DeactivateShield(uid, shield); + } + } + } + + private void UpdateDecayShield() + { + var shieldQuery = EntityQueryEnumerator(); + while (shieldQuery.MoveNext(out var uid, out var decay)) + { + // Manually disable sprite state animation at the last state in time (see meta.json) + if (_timing.CurTime >= decay.DecayEndTime) + { + HideShieldEffect(uid); + RemComp(uid); + } + } + } + + private void ProcessSmasher(EntityUid smasherUid, SmasherComponent smasher) + { + if (!TryGetHolder(smasherUid, out var user)) + { + CleanupAlerts(smasherUid, smasher); + return; + } + + smasher.LastAlertedUser[smasherUid] = user.Value; + UpdateCooldownAlert(smasherUid, user.Value, smasher); + + if (!_miningWeaponsHelpers.IsItemWielded(smasherUid) && HasComp(user.Value)) + { + CancelCharging(user.Value, smasherUid, smasher); + return; + } + + if (!_miningWeaponsHelpers.IsItemWielded(smasherUid)) + return; + + if (smasher.StateUseKey == BoundKeyState.Down && + !HasComp(user) && + _combatMode.IsInCombatMode(user.Value) && + CanActivateShield(smasher)) + { + ProcessCharging(user.Value, smasherUid, smasher); + } + else if (smasher.ActiveCharges.ContainsKey(smasherUid)) + { + CancelCharging(user.Value, smasherUid, smasher); + } + } + + private void ProcessCharging(EntityUid user, EntityUid smasherUid, SmasherComponent smasher) + { + if (!smasher.ActiveCharges.TryGetValue(smasherUid, out var chargeData)) + { + StartCharging(user, smasherUid, smasher); + return; + } + + var holdTime = _timing.CurTime - chargeData.StartTime; + + if (CheckDamageInterruption(user, smasher)) + { + CancelCharging(user, smasherUid, smasher); + return; + } + + if (holdTime >= smasher.TimeChargingSmasher) + { + CompleteCharging(user, smasherUid, smasher); + } + } + #endregion + + #region Event Handlers + private void OnUpdateKeyState(UpdateSmasherStateEvent ev, EntitySessionEventArgs args) + { + var user = GetEntity(ev.User); + if (!TryGetSmasherInHands(user, out var _, out var smasher)) + return; + + smasher.StateUseKey = ev.KeyState; + } + + private void OnUserDamageModified(EntityUid uid, ShieldActiveComponent component, ref DamageModifyEvent args) + { + var modifier = component.PassiveBlockDamageModifer; + if (modifier == null) + return; + + args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifier); + } + + private void OnComponentInit(EntityUid uid, ShieldActiveComponent component, ComponentInit args) + { + if (!TryGetSmasherInHands(uid, out var _, out var smasher)) + return; + + component.PassiveBlockDamageModifer ??= new DamageModifierSet(); + component.PassiveBlockDamageModifer.Coefficients ??= new Dictionary(); + component.PassiveBlockDamageModifer.Coefficients = smasher.DamageBlockedCoefficients; + } + + private void OnRefreshMovespeed(EntityUid uid, SmasherChargingComponent component, RefreshMovementSpeedModifiersEvent args) + { + args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); + } + #endregion +} diff --git a/Content.Server/Imperial/MiningWeapons/license.txt b/Content.Server/Imperial/MiningWeapons/license.txt new file mode 100644 index 00000000000..62a912b3e61 --- /dev/null +++ b/Content.Server/Imperial/MiningWeapons/license.txt @@ -0,0 +1,2 @@ +license: This content is under ICLA. Copyright holder: kotinggg (discord) (Read more on: https://wiki.imperialspace.net/ru/icla) +copyright: "kotinggg (discord)" diff --git a/Content.Server/Imperial/NinjaMultitask/Systems/DealDamageConditionSystem.cs b/Content.Server/Imperial/NinjaMultitask/Systems/DealDamageConditionSystem.cs index d992af9a582..7e6024ea146 100644 --- a/Content.Server/Imperial/NinjaMultitask/Systems/DealDamageConditionSystem.cs +++ b/Content.Server/Imperial/NinjaMultitask/Systems/DealDamageConditionSystem.cs @@ -1,26 +1,19 @@ -using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; -using Content.Server.Shuttles.Systems; -using Content.Shared.CCVar; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Objectives.Components; -using Robust.Shared.Configuration; -using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; using Robust.Shared.Prototypes; -using Content.Server.Objectives.Systems; -using Content.Shared.Damage; using Content.Server.Imperial.NinjaMultitask.Components; using Robust.Shared.Random; -using Robust.Shared.Network; using Robust.Shared.Player; -using Content.Shared.Ninja.Components; -using Robust.Shared.Prototypes; using Content.Shared.Roles.Jobs; using Content.Shared.Mobs; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Imperial.NinjaMultitask.Systems; + public sealed class DealDamageConditionSystem : EntitySystem { [Dependency] private readonly TargetObjectiveSystem _target = default!; @@ -29,6 +22,8 @@ public sealed class DealDamageConditionSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedJobSystem _job = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + public override void Initialize() { base.Initialize(); @@ -52,13 +47,13 @@ private float GetProgress(EntityUid target, FixedPoint2 mindmg, FixedPoint2 maxd if (failed) return 0f; if (Math.Clamp((float)(dltdmg), (float)(mindmg), (float)(maxdmg)) == dltdmg) - { - return 1f; - } - else if (dltdmg < mindmg) - { - return (float)(dltdmg / mindmg); - } + { + return 1f; + } + else if (dltdmg < mindmg) + { + return (float)(dltdmg / mindmg); + } return 0f; } @@ -71,7 +66,7 @@ private void OnAfterAssign(EntityUid uid, DealDamageConditionComponent comp, ref { bodyUid = mindComp.OwnedEntity ?? EntityUid.Invalid; } - comp.Ninja = args.Mind.Owner; + comp.Ninja = args.MindId; var trgt = target ?? EntityUid.Invalid; var targcomp = EnsureComp(bodyUid); comp.OriginalBody = args.Mind.OwnedEntity; @@ -114,7 +109,7 @@ private void OnDamageChanged(EntityUid uid, NinjaDamageTargetComponent component session = actor.PlayerSession; if (!_mindManager.TryGetMind(session, out var mindIdNinja, out var mindComponentNinja)) return; - if (mindComponentNinja.Owner == comp.Ninja && args.Origin == comp.OriginalBody && args.DamageIncreased && damageDelta > 0) + if (mindIdNinja == comp.Ninja && args.Origin == comp.OriginalBody && args.DamageIncreased && damageDelta > 0) { comp.DamageDealt += damageDelta; } @@ -135,7 +130,7 @@ private string GetTitle(EntityUid target, string title) } if (string.IsNullOrEmpty(title)) return string.Empty; - if (ownedEntity == null || !TryComp(ownedEntity, out var compninjatarget)) + if (!TryComp(ownedEntity, out var compninjatarget)) { return string.Empty; } @@ -147,14 +142,13 @@ private string GetTitle(EntityUid target, string title) } var mindmg = comp.MinDamage.ToString(); var maxdmg = comp.MaxDamage.ToString(); - var damageTypeProto = new DamageTypePrototype(); - if (_prototype.TryIndex(comp.DamageType, out var proto)) - { - damageTypeProto = proto; - } + + if (!_prototype.TryIndex(comp.DamageType, out var damageTypeProto)) + return "error"; + var type = damageTypeProto?.LocalizedName ?? comp.DamageType.Value; var jobName = "Unknown"; - if (TryComp(target, out var mindComp)) + if (TryComp(target, out var _)) { jobName = _job.MindTryGetJobName(target) ?? jobName; } @@ -178,7 +172,7 @@ private void OnMobStateChanged(EntityUid uid, NinjaDamageTargetComponent compone { return; } - if (!TryComp(component.Owner, out var mccomp)) + if (!TryComp(uid, out var mccomp)) { return; } @@ -191,7 +185,7 @@ private void OnMobStateChanged(EntityUid uid, NinjaDamageTargetComponent compone return; } var damageType = comp.DamageType.Value; - if (!damagecomp.Damage.DamageDict.TryGetValue(damageType, out var damageDelta)) + if (!_damageableSystem.GetAllDamage(mcomp.OwnedEntity.Value).DamageDict.TryGetValue(damageType, out var damageDelta)) { return; } diff --git a/Content.Server/Imperial/NinjaMultitask/Systems/PickRandomSyndicateTargetSystem.cs b/Content.Server/Imperial/NinjaMultitask/Systems/PickRandomSyndicateTargetSystem.cs index 39831787b7a..eb0b3c2414f 100644 --- a/Content.Server/Imperial/NinjaMultitask/Systems/PickRandomSyndicateTargetSystem.cs +++ b/Content.Server/Imperial/NinjaMultitask/Systems/PickRandomSyndicateTargetSystem.cs @@ -2,12 +2,11 @@ using Content.Shared.Mind; using Content.Shared.Objectives.Components; using Content.Server.GameTicking.Rules; -using Content.Server.Revolutionary.Components; using Robust.Shared.Random; using System.Linq; -using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; using Content.Server.Imperial.NinjaMultitask.Components; +using Content.Shared.Objectives.Systems; namespace Content.Server.Imperial.NinjaMultitask.Systems; public sealed class PickRandomSyndicateTargetSystem : EntitySystem @@ -16,6 +15,8 @@ public sealed class PickRandomSyndicateTargetSystem : EntitySystem [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly TargetSystem _targetSystem = default!; + public override void Initialize() { base.Initialize(); @@ -55,7 +56,7 @@ private void OnRandomSyndicateTargetAssigned(Entity +/// Настройки случайного наполнения EntityStorage уникальными плюшевыми персонажами. +/// +[RegisterComponent] +public sealed partial class UniqueRandomPlushieFillComponent : Component +{ + /// + /// Редкие варианты игрушек. Для Gift-версий обычный прототип считается тем же персонажем. + /// + [DataField] + public List RarePrototypes = new(); + + /// + /// Шанс выбрать редкую игрушку на каждой попытке. + /// + [DataField] + public float RareChance = 0.15f; + + /// + /// Основной список игрушек. + /// + [DataField] + public List Prototypes = new(); + + /// + /// Шанс выбрать обычную игрушку, если редкая не выпала. + /// + [DataField] + public float Chance = 0.75f; + + /// + /// Максимальное число попыток выбора уникальной игрушки. + /// Неудачная проверка Chance оставляет соответствующую попытку пустой. + /// + [DataField] + public int Amount = 25; +} diff --git a/Content.Server/Imperial/Other/TFG/Toughguys/UniqueRandomPlushieFillSystem.cs b/Content.Server/Imperial/Other/TFG/Toughguys/UniqueRandomPlushieFillSystem.cs new file mode 100644 index 00000000000..5c1c454c287 --- /dev/null +++ b/Content.Server/Imperial/Other/TFG/Toughguys/UniqueRandomPlushieFillSystem.cs @@ -0,0 +1,85 @@ +using System.Linq; +using Content.Server.Storage.EntitySystems; +using Content.Shared.Storage.Components; +using Robust.Shared.Random; + +namespace Content.Server.Imperial.Other.TFG.Toughguys; + +/// +/// При инициализации сущности выбирает плюшки без повторения персонажей +/// и помещает их непосредственно в EntityStorage этой сущности. +/// +public sealed class UniqueRandomPlushieFillSystem : EntitySystem +{ + [Dependency] private readonly EntityStorageSystem _entityStorage = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var storage)) + { + Log.Error($"{ToPrettyString(ent)} has UniqueRandomPlushieFill but no EntityStorage component."); + return; + } + + // Работаем с временными копиями: исходные списки компонента остаются неизменными + // и могут повторно использоваться другими созданными ящиками. + var rare = ent.Comp.RarePrototypes.Distinct().ToList(); + var common = ent.Comp.Prototypes.Distinct().ToList(); + + // Не пытаемся создать больше сущностей, чем физически помещается в хранилище. + var availableSlots = Math.Max(0, storage.Capacity - storage.Contents.ContainedEntities.Count); + var attempts = Math.Min(Math.Max(0, ent.Comp.Amount), availableSlots); + + // Защита от ошибочных значений вероятности в YAML. + var rareChance = Math.Clamp(ent.Comp.RareChance, 0f, 1f); + var commonChance = Math.Clamp(ent.Comp.Chance, 0f, 1f); + + for (var i = 0; i < attempts && (rare.Count > 0 || common.Count > 0); i++) + { + string? prototype = null; + + if (rare.Count > 0 && _random.Prob(rareChance)) + { + prototype = _random.PickAndTake(rare); + } + else if (common.Count > 0 && _random.Prob(commonChance)) + { + prototype = _random.PickAndTake(common); + } + + if (prototype == null) + continue; + + // Gift-прототип является вариантом обычной игрушки, а не отдельным персонажем. + // Удаляем всё семейство, чтобы, например, PlushieCP и PlushieCPGift + // не могли одновременно оказаться в одном ящике. + var family = GetPlushieFamily(prototype); + rare.RemoveAll(candidate => GetPlushieFamily(candidate.Id) == family); + common.RemoveAll(candidate => GetPlushieFamily(candidate.Id) == family); + + var spawned = Spawn(prototype, Transform(ent).Coordinates); + if (_entityStorage.Insert(spawned, ent, storage)) + continue; + + Log.Error($"Failed to insert unique plushie {prototype} into {ToPrettyString(ent)}."); + QueueDel(spawned); + } + } + + private static string GetPlushieFamily(string prototype) + { + // Все редкие варианты в текущем пуле используют единый суффикс Gift. + // После его удаления ID совпадает с ID обычной версии персонажа. + const string giftSuffix = "Gift"; + return prototype.EndsWith(giftSuffix, StringComparison.Ordinal) + ? prototype[..^giftSuffix.Length] + : prototype; + } +} diff --git a/Content.Server/Imperial/PiratesNewHorizon/GPS/Systems/GPSTrackerRemoverSystem.cs b/Content.Server/Imperial/PiratesNewHorizon/GPS/Systems/GPSTrackerRemoverSystem.cs index 702af7f9685..a91f2e54002 100644 --- a/Content.Server/Imperial/PiratesNewHorizon/GPS/Systems/GPSTrackerRemoverSystem.cs +++ b/Content.Server/Imperial/PiratesNewHorizon/GPS/Systems/GPSTrackerRemoverSystem.cs @@ -1,39 +1,23 @@ - -using Content.Server.Cargo.Components; using Content.Server.Popups; -using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Timing; -using Content.Shared.Cargo.Events; using Content.Shared.Verbs; using Content.Shared.DoAfter; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; -using Content.Server.Storage; -using Content.Shared.Storage; -using Content.Server.Cargo; -using Content.Server.Cargo.Systems; -using Content.Shared.Cargo.Systems; using Content.Server.Imperial.PiratesNewHorizon.GPS.Components; using Content.Shared.Imperial.PiratesNewHorizon.GPS.Events; -using Content.Shared.Body.Components; using Content.Shared.Hands.Components; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; -using Robust.Shared.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Content.Server.Hands.Systems; +using Content.Shared.Body; namespace Content.Server.Imperial.PiratesNewHorizon.GPS.Systems; public sealed class GPSTrackerRemoverSystem : EntitySystem { [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly PricingSystem _pricingSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly CargoSystem _bountySystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly HandsSystem _handsSystem = default!; diff --git a/Content.Server/Imperial/PiratesNewHorizon/Rules/Systems/PiratesRuleSystem.cs b/Content.Server/Imperial/PiratesNewHorizon/Rules/Systems/PiratesRuleSystem.cs index 7a214fd97b0..cf4df5b49cd 100644 --- a/Content.Server/Imperial/PiratesNewHorizon/Rules/Systems/PiratesRuleSystem.cs +++ b/Content.Server/Imperial/PiratesNewHorizon/Rules/Systems/PiratesRuleSystem.cs @@ -1,18 +1,13 @@ using Content.Server.Antag; using Content.Server.Cargo.Systems; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Imperial.PiratesNewHorizon.Roles; using Content.Shared.GameTicking.Components; using Content.Shared.Mind; -using Robust.Shared.GameObjects; using Robust.Shared.Utility; -using Robust.Shared.Maths; using Content.Server.Imperial.PiratesNewHorizon.Rules.Components; using Content.Server.Roles; using Content.Server.GameTicking; -using Content.Server.Mind; -using Content.Shared.Mind; using Robust.Server.Player; namespace Content.Server.Imperial.PiratesNewHorizon.Rules.Systems; @@ -21,7 +16,6 @@ public sealed class PiratesRuleSystem : GameRuleSystem { [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly PricingSystem _pricingSystem = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly IPlayerManager _players = default!; public override void Initialize() diff --git a/Content.Server/Imperial/PiratesNewHorizon/Trigger/Systems/TriggerSystemOnHeat.cs b/Content.Server/Imperial/PiratesNewHorizon/Trigger/Systems/TriggerSystemOnHeat.cs index 74bc46f5041..c24f4eb794a 100644 --- a/Content.Server/Imperial/PiratesNewHorizon/Trigger/Systems/TriggerSystemOnHeat.cs +++ b/Content.Server/Imperial/PiratesNewHorizon/Trigger/Systems/TriggerSystemOnHeat.cs @@ -9,7 +9,6 @@ using Content.Server.Atmos.EntitySystems; using Robust.Shared.Random; using Content.Server.Imperial.PiratesNewHorizon.Trigger.Components; -using Content.Server.Explosion.EntitySystems; using Content.Shared.Trigger.Components; using Content.Shared.Trigger.Systems; @@ -18,7 +17,6 @@ namespace Content.Server.Imperial.PiratesNewHorizon.Trigger.Systems; public sealed class TriggerSystemOnHeat : EntitySystem { [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly IEntityManager _entities = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TriggerSystem _triggerSystem = default!; diff --git a/Content.Server/Imperial/PlantsAnalyzer/PlantsAnalyzerSystem.cs b/Content.Server/Imperial/PlantsAnalyzer/PlantsAnalyzerSystem.cs index 08a52e0eca1..2a80489f97f 100644 --- a/Content.Server/Imperial/PlantsAnalyzer/PlantsAnalyzerSystem.cs +++ b/Content.Server/Imperial/PlantsAnalyzer/PlantsAnalyzerSystem.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using Content.Server.Botany.Components; using Content.Server.Imperial.PlantsAnalyzer.Components; -using Content.Server.PowerCell; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.DoAfter; using Content.Shared.Interaction; @@ -14,6 +13,7 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Timing; +using Content.Shared.PowerCell; namespace Content.Server.Imperial.PlantsAnalyzer; @@ -24,10 +24,13 @@ public sealed class PlantsAnalyzerSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly ItemToggleSystem _toggle = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; + private readonly Regex _mutationRegex = new(@"(?<=[a-z])([A-Z])"); + private readonly Regex _chemicalsRegex = new(@"(?<=[a-z])([A-Z])"); + + public override void Initialize() { SubscribeLocalEvent(OnAfterInteract); @@ -69,7 +72,7 @@ public override void Update(float frameTime) private void OnAfterInteract(Entity uid, ref AfterInteractEvent args) { - if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid, user: args.User)) + if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User)) return; _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid); @@ -86,7 +89,7 @@ private void OnAfterInteract(Entity uid, ref AfterInter private void OnDoAfter(Entity uid, ref PlantsAnalyzerDoAfterEvent args) { - if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User)) + if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User)) return; if (!uid.Comp.Silent) @@ -190,14 +193,14 @@ public void UpdateScannedUser(EntityUid plantsAnalyzer, EntityUid target, bool s ? string.Join(",\n " + string.Concat(Enumerable.Repeat(" ", Loc.GetString("plants-analyzer-window-mutations").Length * 2)), plantHolder.Seed.Mutations.Select(mutation => Loc.GetString("random-mutation-name-" + - Regex.Replace(mutation.Name, @"(?<=[a-z])([A-Z])", "-$1").ToLower()))) + _mutationRegex.Replace(mutation.Name, "-$1").ToLower()))) : Loc.GetString("plants-analyzer-window-no-mutations"); var chemicals = plantHolder.Seed?.Chemicals.Any() == true ? string.Join(",\n " + string.Concat(Enumerable.Repeat(" ", Loc.GetString("plants-analyzer-window-chemicals").Length * 2)), plantHolder.Seed.Chemicals.Keys.Select(reagent => Loc.GetString("reagent-name-" + - Regex.Replace(reagent, @"(?<=[a-z])([A-Z])", "-$1").ToLower()))) + _chemicalsRegex.Replace(reagent, "-$1").ToLower()))) : Loc.GetString("plants-analyzer-window-no-chemicals"); _uiSystem.ServerSendUiMessage(plantsAnalyzer, PlantsAnalyzerUiKey.Key, new PlantsAnalyzerScannedUserMessage( diff --git a/Content.Server/Imperial/Power/Components/EventComponents/ISupermatterEventComponent.cs b/Content.Server/Imperial/Power/Components/EventComponents/ISupermatterEventComponent.cs new file mode 100644 index 00000000000..5d514cb1688 --- /dev/null +++ b/Content.Server/Imperial/Power/Components/EventComponents/ISupermatterEventComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Imperial.Power.Components.EventComponents; + +public interface ISupermatterEventComponent +{ + /// + /// Имя события, отображаемое в консоли мониторинга суперматерии + /// + LocId? EventName { get; } +} diff --git a/Content.Server/Imperial/Power/Components/EventComponents/SupermatterLightningEventComponent.cs b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterLightningEventComponent.cs new file mode 100644 index 00000000000..4fadb791c2f --- /dev/null +++ b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterLightningEventComponent.cs @@ -0,0 +1,35 @@ +namespace Content.Server.Imperial.Power.Components.EventComponents; + +[RegisterComponent] +public sealed partial class SupermatterLightningEventComponent : Component, ISupermatterEventComponent +{ + [DataField] + public LocId? EventName { get; set; } = "supermatter-event-lightning-name"; + + [DataField] + public LocId EventDesc = "supermatter-event-lightning"; + + [DataField] + public TimeSpan? Duration; + + + [DataField] + public TimeSpan? EndTime; + + [DataField] + public TimeSpan? EventAfterTime; + + + [DataField] + public int BoltCount = 1; + + [DataField] + public float BoltRadius = 8f; + + + [DataField] + public TimeSpan? LightningInterval; + + [DataField] + public TimeSpan NextLightningTime; +} diff --git a/Content.Server/Imperial/Power/Components/EventComponents/SupermatterNoneEventComponent.cs b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterNoneEventComponent.cs new file mode 100644 index 00000000000..4856c427f99 --- /dev/null +++ b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterNoneEventComponent.cs @@ -0,0 +1,21 @@ +namespace Content.Server.Imperial.Power.Components.EventComponents; + +[RegisterComponent] +public sealed partial class SupermatterNoneEventComponent : Component, ISupermatterEventComponent +{ + [DataField] + public LocId? EventName { get; set; } + + [DataField] + public LocId EventDesc = "supermatter-event-none"; + + [DataField] + public TimeSpan? Duration; + + + [DataField] + public TimeSpan? EndTime; + + [DataField] + public TimeSpan? EventAfterTime; +} diff --git a/Content.Server/Imperial/Power/Components/EventComponents/SupermatterPlasmaEventComponent.cs b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterPlasmaEventComponent.cs new file mode 100644 index 00000000000..79163a44ef0 --- /dev/null +++ b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterPlasmaEventComponent.cs @@ -0,0 +1,38 @@ +namespace Content.Server.Imperial.Power.Components.EventComponents; + +[RegisterComponent] +public sealed partial class SupermatterPlasmaEventComponent : Component, ISupermatterEventComponent +{ + [DataField] + public LocId? EventName { get; set; } = "supermatter-event-plasma-name"; + + [DataField] + public LocId EventDesc = "supermatter-event-plasma"; + + [DataField] + public TimeSpan? Duration; + + + [DataField] + public TimeSpan? EndTime; + + [DataField] + public TimeSpan? EventAfterTime; + + + [DataField] + public float? HotspotTemperature; + + [DataField] + public float? HotspotVolume; + + + [DataField] + public float? MolesAmount; + + [DataField] + public TimeSpan? PlasmaInterval; + + [DataField] + public TimeSpan NextPlasmaTime; +} diff --git a/Content.Server/Imperial/Power/Components/EventComponents/SupermatterRadiationEventComponent.cs b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterRadiationEventComponent.cs new file mode 100644 index 00000000000..63694062519 --- /dev/null +++ b/Content.Server/Imperial/Power/Components/EventComponents/SupermatterRadiationEventComponent.cs @@ -0,0 +1,28 @@ +namespace Content.Server.Imperial.Power.Components.EventComponents; + +[RegisterComponent] +public sealed partial class SupermatterRadiationEventComponent : Component, ISupermatterEventComponent +{ + [DataField] + public LocId? EventName { get; set; } = "supermatter-event-radiation-name"; + + [DataField] + public LocId EventDesc = "supermatter-event-radiation"; + + [DataField] + public TimeSpan? Duration; + + + [DataField] + public TimeSpan? EndTime; + + [DataField] + public TimeSpan? EventAfterTime; + + + [DataField] + public float DefaultIntensity = 4.5f; + + [DataField] + public float? Intensity; +} diff --git a/Content.Server/Imperial/Power/Components/SupermatterConsoleComponent.cs b/Content.Server/Imperial/Power/Components/SupermatterConsoleComponent.cs new file mode 100644 index 00000000000..d6c9733ea9f --- /dev/null +++ b/Content.Server/Imperial/Power/Components/SupermatterConsoleComponent.cs @@ -0,0 +1,57 @@ +using Content.Shared.DeviceLinking; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.Power.Components; + +[RegisterComponent] +public sealed partial class SupermatterConsoleComponent : Component +{ + /// + /// Подключённая суперматерия + /// + [ViewVariables(VVAccess.ReadWrite)] + public EntityUid? ConnectedSupermatter; + + /// + /// Максимальный радиус подключения к суперматерии + /// + [DataField] + public float MaxRange = 15f; + + /// + /// Имя порта для связи + /// + [DataField] + public ProtoId InputPort = "Input"; + + /// + /// Звук пиликанья при низкой целостности + /// + [DataField] + public SoundPathSpecifier BeepSound = new("/Audio/Machines/beep.ogg"); + + /// + /// Время следующего пиликанья + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextBeep = TimeSpan.Zero; + + /// + /// Интервал пиликанья + /// + [DataField] + public TimeSpan BeepInterval = TimeSpan.FromSeconds(2f); + + /// + /// Время следующего обновления UI + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextUiUpdate = TimeSpan.Zero; + + /// + /// Интервал обновления UI + /// + [DataField] + public TimeSpan UiUpdateInterval = TimeSpan.FromSeconds(0.5f); +} diff --git a/Content.Server/Imperial/Power/Components/SupermatterEventComponent.cs b/Content.Server/Imperial/Power/Components/SupermatterEventComponent.cs deleted file mode 100644 index 6a1ed4ea7e7..00000000000 --- a/Content.Server/Imperial/Power/Components/SupermatterEventComponent.cs +++ /dev/null @@ -1,195 +0,0 @@ -using Robust.Shared.Prototypes; -using Content.Shared.Radio; -using Content.Server.Imperial.Power.EntitySystems.Events; -using Robust.Shared.Random; - -namespace Content.Server.Imperial.Power.Components; - -[RegisterComponent] -public sealed partial class SupermatterEventComponent : Component -{ - /// - /// Значения видов событий суперматерии. - /// - public enum SupermatterEventType - { - None = 0, - Lightning = 1, - Radiation = 2, - Plasma = 3, - } - - /// - /// Список разрешенных типов событий для этого кристалла. - /// - [ViewVariables(VVAccess.ReadOnly)] - public List AllowedEventTypes { get; set; } = - [ - SupermatterEventType.None, - SupermatterEventType.Lightning, - SupermatterEventType.Radiation, - SupermatterEventType.Plasma, - ]; - - /// - /// Тип события в само событие. - /// - public readonly Dictionary SupermatterEventTypesToEvents = new() - { - { SupermatterEventType.None, new SupermatterNoneEvent() }, - { SupermatterEventType.Lightning, new SupermatterLightningEvent() }, - { SupermatterEventType.Radiation, new SupermatterRadiationEvent() }, - { SupermatterEventType.Plasma, new SupermatterPlasmaEvent() }, - }; - - /// - /// Объявлена ли война Ядерными Оперативниками. Если да - не создавать события. - /// - [ViewVariables(VVAccess.ReadOnly)] - public bool IsWarOps = false; - - /// - /// Время до первого события, далее устанавливается самими событиями (в секундах). - /// - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan NextEventTimer = TimeSpan.FromSeconds(Random.Shared.Next(600, 900)); - - /// - /// Текущий активный тип события. - /// - [ViewVariables(VVAccess.ReadWrite)] - public SupermatterEventType CurrentEvent = SupermatterEventType.None; - - /// - /// Время окончания текущего события (0, если событие не активно). - /// - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan EventEndTime = TimeSpan.Zero; - - /// - /// Кулдаун между всплесками молний. - /// - public TimeSpan LightningCooldown = TimeSpan.Zero; - - /// - /// Таймер для генерации плазмы во время события Plasma. - /// - public TimeSpan? PlasmaTickAccumulator = null; - - /// - /// Радио каналы для оповещений о событиях. - /// - [DataField] - public ProtoId[] RadioChannels = ["Engineering"]; - - /// - /// Время жизни кэша консоли (в секундах). - /// - public readonly TimeSpan ConsoleCacheLifetime = TimeSpan.FromSeconds(10); - - /// - /// Длительность события None (в секундах). - /// - [DataField] - public TimeSpan NoneEventDuration = TimeSpan.FromSeconds(Random.Shared.Next(200, 300)); - - // LightningEvent - - /// - /// Длительность события LightningEvent (в секундах). - /// - [DataField] - public TimeSpan LightningEventDuration = TimeSpan.FromSeconds(Random.Shared.Next(80, 120)); - - /// - /// Кулдаун между молниями во время события LightningEvent (в секундах). - /// - [DataField] - public TimeSpan LightningCooldownDuration = TimeSpan.FromSeconds(Random.Shared.Next(5, 8)); - - /// - /// Время до следующего события после LightningEvent (в секундах). - /// - public readonly TimeSpan EventAfterLightingTime = TimeSpan.FromSeconds(Random.Shared.Next(180, 420)); - - /// - /// Количество молний, выпускаемых за один раз при LightningEvent. - /// - [DataField] - public int LightningBoltCount = 1; - - /// - /// Радиус, в котором молнии будут выпускаться при LightingEvent. - /// - [DataField] - public float LightningBoltRadius = 8f; - - // RadiationEvent - - /// - /// Длительность RadiationEvent (в секундах). - /// - [DataField] - public TimeSpan RadiationEventDuration = TimeSpan.FromSeconds(Random.Shared.Next(80, 120)); - - /// - /// Интенсивность радиации во время события RadiationEvent. - /// - [DataField] - public float RadiationEventIntensity = Random.Shared.NextFloat(6f, 12f); - - /// - /// Время до следующего события после RadiationEvent (в секундах). - /// - public readonly TimeSpan EventAfterRadiationTime = TimeSpan.FromSeconds(Random.Shared.Next(180, 420)); - - // PlasmaEvent - - /// - /// Длительность PlasmaEvent (в секундах). - /// - [DataField] - public TimeSpan PlasmaEventDuration = TimeSpan.FromSeconds(Random.Shared.Next(80, 120)); - - /// - /// Время до следующего события после PlasmaEvent (в секундах). - /// - public readonly TimeSpan EventAfterPlasmaTime = TimeSpan.FromSeconds(Random.Shared.Next(140, 180)); - - /// - /// Интервал генерации плазмы во время PlasmaEvent (в секундах). - /// - public readonly TimeSpan PlasmaTickInterval = TimeSpan.FromSeconds(Random.Shared.Next(6, 10)); - - /// - /// Количество молей плазмы, генерируемых за тик. - /// - [DataField] - public float PlasmaMolesAmount = Random.Shared.NextFloat(3f, 7f); - - /// - /// Температура хотспота плазмы при PlasmaEvent. - /// - [DataField] - public float PlasmaHotspotTemperature = Random.Shared.NextFloat(1250f, 1500f); - - /// - /// Объем хотспота плазмы при PlasmaEvent. - /// - [DataField] - public float PlasmaHotspotVolume = Random.Shared.NextFloat(35f, 50f); - - // Default radiation - - /// - /// Базовая интенсивность радиации вне событий. - /// - [DataField] - public float DefaultRadiationIntensity = Random.Shared.NextFloat(4, 5); - - public TimeSpan LastConsoleCacheUpdate = TimeSpan.Zero; - public TimeSpan LastEventEndTimeUpdate = TimeSpan.Zero; - public TimeSpan LastNextEventTimerUpdate = TimeSpan.Zero; - public TimeSpan LastLightningCooldownUpdate = TimeSpan.Zero; - public TimeSpan LastPlasmaTickUpdate = TimeSpan.Zero; -} diff --git a/Content.Server/Imperial/Power/Components/SupermatterEventSchedulerComponent.cs b/Content.Server/Imperial/Power/Components/SupermatterEventSchedulerComponent.cs new file mode 100644 index 00000000000..e218429a5d0 --- /dev/null +++ b/Content.Server/Imperial/Power/Components/SupermatterEventSchedulerComponent.cs @@ -0,0 +1,43 @@ +using Content.Shared.Whitelist; + +namespace Content.Server.Imperial.Power.Components; + +[RegisterComponent] +public sealed partial class SupermatterEventSchedulerComponent : Component +{ + /// + /// Список компонентов событий Суперматерии + /// + [DataField] + public EntityWhitelist? Events; + + /// + /// Максимальное число событий которое может происходить одновременно + /// + [DataField] + public int MaxConcurrentEvents = 3; + + /// + /// Текущее число событий происходящих одновременно + /// + [DataField] + public int ConcurrentEvents; + + /// + /// Отключены ли события Суперматерии + /// + [ViewVariables(VVAccess.ReadOnly)] + public bool Disabled; + + /// + /// Время когда появится новое событие Суперматерии + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan? NextEventTime; + + /// + /// Время первого события Суперматерии с её активации (не гибом) + /// + [DataField] + public TimeSpan? FirstEventInterval; +} diff --git a/Content.Server/Imperial/Power/Components/SupermatterIntegrityComponent.cs b/Content.Server/Imperial/Power/Components/SupermatterIntegrityComponent.cs deleted file mode 100644 index 1816f0fa774..00000000000 --- a/Content.Server/Imperial/Power/Components/SupermatterIntegrityComponent.cs +++ /dev/null @@ -1,203 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Radio; -using Content.Shared.Tag; -using Robust.Shared.Prototypes; -using Content.Shared.Explosion; - -namespace Content.Server.Imperial.Power.Components; - -[RegisterComponent] -public sealed partial class SupermatterIntegrityComponent : Component -{ - /// - /// Описание кристалла и оповещения в зависимости от состояния. - /// Каждый элемент списка содержит: - /// - /// - /// Threshold - /// Значение целостности кристалла (%) при котором активен этот уровень. - /// - /// - /// Color - /// Цвет описания консоли мониторинга суперматерии. - /// - /// - /// Description - /// LocId строки с описанием состояния. - /// - /// - /// Warning - /// LocId предупреждения для отправки в рацию. - /// - /// - /// Flag - /// Флаг, указывающий, отправлялось ли предупреждение. - /// - /// - /// - public List<(float Threshold, Color Color, LocId Description, LocId Warning, bool Flag)> SupermatterIntegrity = - [ - (95f, Color.Green, "supermatter-desc-pristine", "supermatter-warn-95", false), - (75f, Color.Yellow, "supermatter-desc-scratched", "supermatter-warn-75", false), - (50f, Color.Orange, "supermatter-desc-cracked", "supermatter-warn-50", false), - (25f, Color.Brown, "supermatter-desc-badly-cracked", "supermatter-warn-25", false), - (10f, Color.DarkRed, "supermatter-desc-critical", "supermatter-warn-10", false), - (0f, Color.Red, "", "", false), - ]; - - /// - /// Активирована ли суперматерия. - /// До активации она не выполняет никаких процессов. - /// - [DataField] - public bool Activated; - - /// - /// Текущая целостность кристалла - /// - [DataField] - public float Integrity = 100f; - - /// - /// Максимальная целостность - /// - [DataField] - public float MaxIntegrity = 100f; - - /// - /// Сколько урона наносится за тик при опасных условиях - /// - [DataField, ViewVariables(VVAccess.ReadOnly)] - public DamageSpecifier TickDamage = new(); - - /// - /// Интервал между тиками урона - /// - [DataField] - public TimeSpan DamageTickInterval = TimeSpan.FromSeconds(1); - - /// - /// Индивидуальный таймер для тиков урона - /// - public TimeSpan TickAccumulator = TimeSpan.Zero; - - /// - /// Минимальная целостность, при которой начинается катастрофа - /// - [DataField] - public float CatastropheThreshold; - - /// - /// Канал радио, в который будут отправляться оповещения - /// - [DataField] - public ProtoId RadioChannel = "Engineering"; - - /// - /// Активна ли катастрофа - /// - [ViewVariables(VVAccess.ReadWrite)] - public bool CatastropheActive = false; - - /// - /// Верхняя граница температуры, после которой наступают плохие для суперматерии условия - /// - public readonly float UpperTempThreshold = 350f; - - /// - /// Нижняя граница температуры, после которой наступают плохие для суперматерии условия - /// - public readonly float LowerTempThreshold = 250f; - - /// - /// Верхняя граница давления, после которой наступают плохие для суперматерии условия - /// - public readonly float UpperPressureThreshold = 300f; - - /// - /// Нижняя граница давления, после которой наступают плохие для суперматерии условия - /// - public readonly float LowerPressureThreshold = 10f; - - /// - /// Таймер катастрофы - /// - [ViewVariables(VVAccess.ReadOnly)] - public TimeSpan CatastropheTimer = TimeSpan.Zero; - - /// - /// Продолжительность катастрофы до финального события (взрыва). - /// - [DataField] - public TimeSpan CatastropheDuration = TimeSpan.FromSeconds(120); - - /// - /// Тег, прототипы с которым лечат Суперматерию - /// - [DataField] - public ProtoId HealTag = "EmitterBolt"; - - /// - /// Количество здоровья, восстанавливаемое за один выстрел эмиттера - /// - [DataField] - public float EmitterHealAmount = 0.1f; - - /// - /// Идентификатор прототипа взрыва, который будет использован при катастрофе. - /// - [DataField] - public ProtoId ExplosionPrototypeId = "Supermatter"; - - /// - /// Общая интенсивность взрыва при катастрофе. - /// - [DataField] - public float CatastropheTotalIntensity = 2500f; - - /// - /// Крутизна спадания интенсивности взрыва. - /// - [DataField] - public float CatastropheSlope = 1f; - - /// - /// Максимальная интенсивность на тайле для взрыва. - /// - [DataField] - public float CatastropheMaxTileIntensity = 35f; - - /// - /// Интервал между молниями во время катастрофы - /// - [DataField] - public TimeSpan CatastropheLightningInterval = TimeSpan.FromSeconds(1.0); - - /// - /// Таймер для молний во время катастрофы - /// - [DataField] - public TimeSpan CatastropheLightningTimer = TimeSpan.Zero; - - /// - /// Дальность молний во время катастрофы - /// - [DataField] - public float CatastropheLightningRange = 15f; - - /// - /// Количество молний за раз во время катастрофы - /// - [DataField] - public int CatastropheLightningCount = 3; - - /// - /// Настройки звука для включённой [0] и выключенной [1] суперматерии - /// - [DataField] - public List<(float Volume, float Range)> AmbientSound = - [ - (0f, 5f), - (-10f, 3f), - ]; -} diff --git a/Content.Server/Imperial/Power/Components/SupermatterMonitorConsoleComponent.cs b/Content.Server/Imperial/Power/Components/SupermatterMonitorConsoleComponent.cs deleted file mode 100644 index 77bb389742f..00000000000 --- a/Content.Server/Imperial/Power/Components/SupermatterMonitorConsoleComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Audio; - -namespace Content.Server.Imperial.Power.Components; - -[RegisterComponent] -public sealed partial class SupermatterMonitorConsoleComponent : Component -{ - /// - /// Таймер до следующего пиликанья - /// - public TimeSpan BeepCooldownTimer = TimeSpan.Zero; - - /// - /// Звук пиликанья при низкой целостности - /// - [DataField] - public SoundPathSpecifier BeepSound = new("/Audio/Machines/beep.ogg"); -} diff --git a/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterLightningEventSystem.cs b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterLightningEventSystem.cs new file mode 100644 index 00000000000..bbcc86acfc1 --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterLightningEventSystem.cs @@ -0,0 +1,67 @@ +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Server.Lightning; +using Content.Shared.Imperial.Power.Components; +using Content.Shared.Imperial.Power.Events; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems.EventSystems; + +public sealed class SupermatterLightningEventSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IRobustRandom _random = null!; + [Dependency] private readonly LightningSystem _lightning = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + ent.Comp.Duration ??= TimeSpan.FromSeconds(_random.Next(80, 120)); + ent.Comp.EndTime ??= _gameTiming.CurTime + ent.Comp.Duration; + ent.Comp.EventAfterTime ??= TimeSpan.FromSeconds(_random.Next(140, 180)); + + ent.Comp.LightningInterval ??= TimeSpan.FromSeconds(_random.Next(6, 10)); + + var msg = Loc.GetString(ent.Comp.EventDesc); + var ev = new SupermatterSendRadioEvent(msg); + RaiseLocalEvent(ent.Owner, ref ev); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var currentTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var lightning)) + { + if (currentTime >= lightning.EndTime) + { + RemComp(uid); + continue; + } + + if (TryComp(uid, out var integrity) && !integrity.Activated) + continue; + + if (currentTime < lightning.NextLightningTime) + continue; + + lightning.NextLightningTime = currentTime + lightning.LightningInterval!.Value; + _lightning.ShootRandomLightnings(uid, lightning.BoltRadius, lightning.BoltCount); + } + } + + private void OnRemove(Entity ent, ref ComponentRemove args) + { + var nextEventInterval = ent.Comp.EventAfterTime!.Value; + var ev = new SupermatterEventFinishedEvent(nextEventInterval); + RaiseLocalEvent(ent, ref ev); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterNoneEventSystem.cs b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterNoneEventSystem.cs new file mode 100644 index 00000000000..5f561e1e55c --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterNoneEventSystem.cs @@ -0,0 +1,52 @@ +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Shared.Imperial.Power.Events; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems.EventSystems; + +public sealed class SupermatterNoneEventSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IRobustRandom _random = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + ent.Comp.Duration ??= TimeSpan.FromSeconds(_random.Next(200, 300)); + ent.Comp.EndTime ??= _gameTiming.CurTime + ent.Comp.Duration; + ent.Comp.EventAfterTime ??= TimeSpan.FromSeconds(_random.Next(600, 900)); + + var msg = Loc.GetString(ent.Comp.EventDesc); + var ev = new SupermatterSendRadioEvent(msg); + RaiseLocalEvent(ent.Owner, ref ev); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var currentTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var none)) + { + if (currentTime < none.EndTime) + continue; + + RemComp(uid); + } + } + + private void OnRemove(Entity ent, ref ComponentRemove args) + { + var nextEventTime = ent.Comp.EventAfterTime!.Value; + var ev = new SupermatterEventFinishedEvent(nextEventTime); + RaiseLocalEvent(ent, ref ev); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterPlasmaEventSystem.cs b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterPlasmaEventSystem.cs new file mode 100644 index 00000000000..cad44715e07 --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterPlasmaEventSystem.cs @@ -0,0 +1,78 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Shared.Atmos; +using Content.Shared.Imperial.Power.Components; +using Content.Shared.Imperial.Power.Events; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems.EventSystems; + +public sealed class SupermatterPlasmaEventSystem : EntitySystem +{ + [Dependency] private readonly AtmosphereSystem _atmos = null!; + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IRobustRandom _random = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + ent.Comp.HotspotTemperature ??= _random.NextFloat(1250f, 1500f); + ent.Comp.HotspotVolume ??= _random.NextFloat(35f, 50f); + ent.Comp.MolesAmount ??= _random.NextFloat(3f, 7f); + + var currentTime = _gameTiming.CurTime; + ent.Comp.Duration ??= TimeSpan.FromSeconds(_random.Next(80, 120)); + ent.Comp.EndTime ??= currentTime + ent.Comp.Duration; + ent.Comp.EventAfterTime ??= TimeSpan.FromSeconds(_random.Next(140, 180)); + + ent.Comp.PlasmaInterval ??= TimeSpan.FromSeconds(_random.Next(6, 10)); + ent.Comp.NextPlasmaTime = currentTime + ent.Comp.PlasmaInterval.Value; + + var msg = Loc.GetString(ent.Comp.EventDesc); + var ev = new SupermatterSendRadioEvent(msg); + RaiseLocalEvent(ent.Owner, ref ev); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var currentTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var plasma)) + { + if (currentTime >= plasma.EndTime) + { + RemComp(uid); + continue; + } + + if (TryComp(uid, out var integrity) && !integrity.Activated) + continue; + + if (currentTime < plasma.NextPlasmaTime) + continue; + plasma.NextPlasmaTime = currentTime + plasma.PlasmaInterval!.Value; + + var transComp = Transform(uid); + var mix = _atmos.GetContainingMixture((uid, transComp)); + + mix?.AdjustMoles(Gas.Plasma, plasma.MolesAmount!.Value); + mix?.Temperature = MathF.Max(mix.Temperature, plasma.HotspotTemperature!.Value); + } + } + + private void OnRemove(Entity ent, ref ComponentRemove args) + { + var nextEventInterval = ent.Comp.EventAfterTime!.Value; + var ev = new SupermatterEventFinishedEvent(nextEventInterval); + RaiseLocalEvent(ent, ref ev); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterRadiationEventSystem.cs b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterRadiationEventSystem.cs new file mode 100644 index 00000000000..97b97704a4a --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/EventSystems/SupermatterRadiationEventSystem.cs @@ -0,0 +1,74 @@ +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Server.Radiation.Systems; +using Content.Shared.Imperial.Power.Components; +using Content.Shared.Imperial.Power.Events; +using Content.Shared.Radiation.Components; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems.EventSystems; + +public sealed class SupermatterRadiationEventSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IRobustRandom _random = null!; + [Dependency] private readonly RadiationSystem _radiationSystem = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + ent.Comp.Duration ??= TimeSpan.FromSeconds(_random.Next(80, 120)); + ent.Comp.EndTime ??= _gameTiming.CurTime + ent.Comp.Duration; + + ent.Comp.Intensity ??= _random.NextFloat(6f, 12f); + ent.Comp.EventAfterTime ??= TimeSpan.FromSeconds(_random.Next(180, 420)); + + EnsureComp(ent.Owner); + _radiationSystem.SetIntensity(ent.Owner, ent.Comp.Intensity!.Value); + + var msg = Loc.GetString(ent.Comp.EventDesc); + var ev = new SupermatterSendRadioEvent(msg); + RaiseLocalEvent(ent.Owner, ref ev); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var currentTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var radEvent)) + { + if (currentTime < radEvent.EndTime) + { + if (TryComp(uid, out var integrity)) + { + if (!TryComp(uid, out var radSource)) + continue; + + _radiationSystem.SetIntensity((uid, radSource), + !integrity.Activated ? radEvent.DefaultIntensity : radEvent.Intensity!.Value); + } + continue; + } + + RemComp(uid); + } + } + + private void OnRemove(Entity ent, ref ComponentRemove args) + { + if (TryComp(ent, out var radSource)) + _radiationSystem.SetIntensity((ent, radSource), ent.Comp.DefaultIntensity); + + var nextEventInterval = ent.Comp.EventAfterTime!.Value; + var ev = new SupermatterEventFinishedEvent(nextEventInterval); + RaiseLocalEvent(ent, ref ev); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterLightningEvent.cs b/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterLightningEvent.cs deleted file mode 100644 index 5377ca29aa0..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterLightningEvent.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Content.Server.Imperial.Power.Components; -using Content.Shared.Damage; -using Robust.Shared.Localization; -using System; - -namespace Content.Server.Imperial.Power.EntitySystems.Events; - -/// -/// Событие "Молния" - суперматерия генерирует электрические разряды -/// -public sealed class SupermatterLightningEvent -{ - public static void Activate(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem) - { - // Валидация входных параметров - if (uid == EntityUid.Invalid) - { - supermatterSystem.Log.Error("SupermatterLightningEvent.Activate: Invalid EntityUid provided"); - return; - } - if (comp == null) - { - return; - } - - if (supermatterSystem == null) - { - return; - } - - // Валидация конфигурации компонента - if (comp.LightningEventDuration <= TimeSpan.Zero) - { - supermatterSystem.Log.Warning($"SupermatterLightningEvent.Activate: Invalid LightningEventDuration: {comp.LightningEventDuration}"); - return; - } - - if (comp.LightningCooldownDuration <= TimeSpan.Zero) - { - supermatterSystem.Log.Warning($"SupermatterLightningEvent.Activate: Invalid LightningCooldownDuration: {comp.LightningCooldownDuration}"); - return; - } - - var currentTime = supermatterSystem.GameTiming.CurTime; - comp.CurrentEvent = SupermatterEventComponent.SupermatterEventType.Lightning; - comp.EventEndTime = comp.LightningEventDuration; - comp.NextEventTimer = comp.EventAfterLightingTime; - comp.LightningCooldown = TimeSpan.Zero; - comp.LastEventEndTimeUpdate = currentTime; - comp.LastNextEventTimerUpdate = currentTime; - comp.LastLightningCooldownUpdate = currentTime; - - // Стреляем молнии в случайные цели вокруг суперматерии - ShootRandomLightnings(uid, supermatterSystem, comp); - } - - public static void Process(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem, TimeSpan currentTime) - { - var elapsedSinceLastUpdate = currentTime - comp.LastLightningCooldownUpdate; - comp.LightningCooldown -= elapsedSinceLastUpdate; - comp.LastLightningCooldownUpdate = currentTime; - - if (comp.LightningCooldown > TimeSpan.Zero) - return; - - // Стреляем молнии в случайные цели вокруг суперматерии - ShootRandomLightnings(uid, supermatterSystem, comp); - - if (supermatterSystem.TryGetComponent(uid, out var integrity) && integrity != null && - supermatterSystem.TryGetComponent(uid, out var _)) - { - supermatterSystem.Damageable.TryChangeDamage(uid, integrity.TickDamage, origin: null); - } - - comp.LightningCooldown = comp.LightningCooldownDuration; - } - - private static void ShootRandomLightnings(EntityUid uid, SupermatterEventSystem supermatterSystem, SupermatterEventComponent component) - { - // Используем ShootRandomLightnings для стрельбы в случайные цели в радиусе - supermatterSystem.LightningSystem?.ShootRandomLightnings(uid, component.LightningBoltRadius, component.LightningBoltCount, "Lightning", 0, true); - } - - public static string GetAnnouncement() - { - return Loc.GetString("supermatter-event-lightning"); - } -} - diff --git a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterNoneEvent.cs b/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterNoneEvent.cs deleted file mode 100644 index 5ca84c85076..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterNoneEvent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Imperial.Power.Components; - -namespace Content.Server.Imperial.Power.EntitySystems.Events; - -/// -/// Событие "Ничего" - период спокойствия суперматерии -/// -public sealed class SupermatterNoneEvent -{ - public static void Activate(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem) - { - - if (uid == EntityUid.Invalid) - { - supermatterSystem.Log.Error("SupermatterNoneEvent.Activate: Invalid EntityUid provided"); - return; - } - - var currentTime = supermatterSystem.GameTiming.CurTime; - comp.CurrentEvent = SupermatterEventComponent.SupermatterEventType.None; - comp.EventEndTime = TimeSpan.Zero; - comp.NextEventTimer = comp.NoneEventDuration; - comp.LastEventEndTimeUpdate = currentTime; - comp.LastNextEventTimerUpdate = currentTime; - } - - public static void Process(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem, TimeSpan currentTime) - { - - } - - public static string GetAnnouncement() - { - return Loc.GetString("supermatter-event-none"); - } -} diff --git a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterPlasmaEvent.cs b/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterPlasmaEvent.cs deleted file mode 100644 index b23d854f699..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterPlasmaEvent.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Shared.Atmos; -using Content.Server.Imperial.Power.Components; -using Robust.Shared.Map.Components; -using Content.Server.Atmos.EntitySystems; - -namespace Content.Server.Imperial.Power.EntitySystems.Events; - -/// -/// Событие "Плазма" - суперматерия генерирует плазму -/// -public sealed class SupermatterPlasmaEvent -{ - public static void Activate(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem) - { - // Валидация входных параметров - if (uid == EntityUid.Invalid) - { - supermatterSystem.Log.Error("SupermatterPlasmaEvent.Activate: Invalid EntityUid provided"); - return; - } - - var currentTime = supermatterSystem.GameTiming.CurTime; - comp.CurrentEvent = SupermatterEventComponent.SupermatterEventType.Plasma; - comp.EventEndTime = comp.PlasmaEventDuration; - comp.NextEventTimer = comp.EventAfterPlasmaTime; - comp.LastEventEndTimeUpdate = currentTime; - comp.LastNextEventTimerUpdate = currentTime; - comp.LastPlasmaTickUpdate = currentTime; - } - - public static void Process(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem, TimeSpan currentTime) - { - comp.PlasmaTickAccumulator ??= TimeSpan.Zero; - - var elapsedSinceLastUpdate = currentTime - comp.LastPlasmaTickUpdate; - comp.PlasmaTickAccumulator += elapsedSinceLastUpdate; - comp.LastPlasmaTickUpdate = currentTime; - - if (comp.PlasmaTickAccumulator < comp.PlasmaTickInterval) - return; - - // Получаем компоненты один раз - if (!supermatterSystem.TryGetComponent(uid, out var xform) || xform == null) - { - return; - } - - var gas = supermatterSystem.Atmos.GetContainingMixture(uid, true); - if (gas == null) - return; - - // Добавляем газы - gas.AdjustMoles((int)Gas.Plasma, comp.PlasmaMolesAmount); - gas.AdjustMoles((int)Gas.Oxygen, comp.PlasmaMolesAmount); - - // Создаём хотспот - if (!TryGetGridUid(xform, out var gridUid)) - { - supermatterSystem.Log.Warning($"Supermatter plasma event triggered for entity {uid} without grid"); - return; - } - - if (!supermatterSystem.TryGetComponent(gridUid, out var grid) || grid == null) - return; - - var tile = supermatterSystem.MapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); - CreateHotspot(supermatterSystem.Atmos, gridUid, tile, comp.PlasmaHotspotTemperature, comp.PlasmaHotspotVolume, uid); - - comp.PlasmaTickAccumulator -= comp.PlasmaTickInterval; - } - - private static bool TryGetGridUid(TransformComponent xform, out EntityUid gridUid) - { - gridUid = xform.GridUid ?? default; - return xform.GridUid.HasValue; - } - - private static void CreateHotspot(AtmosphereSystem atmos, EntityUid gridUid, Vector2i tile, float temp, float volume, EntityUid uid) - { - atmos.HotspotExpose(gridUid, tile, temp, volume, uid, true); - } - - public static string GetAnnouncement() - { - return Loc.GetString("supermatter-event-plasma"); - } -} diff --git a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterRadiationEvent.cs b/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterRadiationEvent.cs deleted file mode 100644 index f3c3f79dcf1..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/Events/SupermatterRadiationEvent.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Server.Imperial.Power.Components; - -namespace Content.Server.Imperial.Power.EntitySystems.Events; - -/// -/// Событие "Радиация" - суперматерия излучает повышенную радиацию -/// -public sealed class SupermatterRadiationEvent -{ - public static void Activate(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem) - { - // Валидация входных параметров - if (uid == EntityUid.Invalid) - { - supermatterSystem.Log.Error("SupermatterRadiationEvent.Activate: Invalid EntityUid provided"); - return; - } - - var currentTime = supermatterSystem.GameTiming.CurTime; - comp.CurrentEvent = SupermatterEventComponent.SupermatterEventType.Radiation; - comp.EventEndTime = comp.RadiationEventDuration; - comp.NextEventTimer = comp.EventAfterRadiationTime; - comp.LastEventEndTimeUpdate = currentTime; - comp.LastNextEventTimerUpdate = currentTime; - - supermatterSystem.SetRadiation(uid, comp.RadiationEventIntensity); - } - - public static void Process(EntityUid uid, SupermatterEventComponent comp, SupermatterEventSystem supermatterSystem, TimeSpan currentTime) - { - supermatterSystem.SetRadiation(uid, comp.RadiationEventIntensity); - } - - public static string GetAnnouncement() - { - return Loc.GetString("supermatter-event-radiation"); - } -} diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterConsoleSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterConsoleSystem.cs new file mode 100644 index 00000000000..192ad1053e7 --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/SupermatterConsoleSystem.cs @@ -0,0 +1,276 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.DeviceLinking.Systems; +using Content.Server.Imperial.Power.Components; +using Content.Shared.DeviceLinking.Events; +using Content.Shared.Imperial.Power; +using Content.Shared.Imperial.Power.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using System.Linq; +using Content.Server.Chat.Systems; +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Server.Radio.EntitySystems; +using Content.Shared.Chat; +using Content.Shared.DeviceLinking; +using Content.Shared.Imperial.Power.Events; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems; + +public sealed class SupermatterConsoleSystem : EntitySystem +{ + [Dependency] private readonly AtmosphereSystem _atmosSystem = null!; + [Dependency] private readonly ChatSystem _chatSystem = null!; + [Dependency] private readonly DeviceLinkSystem _signalSystem = null!; + [Dependency] private readonly IGameTiming _timing = null!; + [Dependency] private readonly RadioSystem _radioSystem = null!; + [Dependency] private readonly SharedAudioSystem _audioSystem = null!; + [Dependency] private readonly UserInterfaceSystem _uiSystem = null!; + [Dependency] private readonly IComponentFactory _componentFactory = null!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnMapInit); + + SubscribeLocalEvent(OnLinkAttempt); + SubscribeLocalEvent(OnNewLink); + SubscribeLocalEvent(OnPortDisconnected); + + SubscribeLocalEvent(OnUiOpened); + + SubscribeLocalEvent(OnSupermatterStartup); + SubscribeLocalEvent(OnSendRadioEvent); + } + + private void OnInit(Entity entity, ref ComponentInit args) + { + _signalSystem.EnsureSinkPorts(entity, entity.Comp.InputPort); + } + + private void OnMapInit(Entity entity, ref MapInitEvent args) + { + if (TryComp(entity, out var sink)) + { + foreach (var sourceUid in sink.LinkedSources.Where(HasComp)) + { + entity.Comp.ConnectedSupermatter = sourceUid; + _signalSystem.LinkDefaults(null, sourceUid, entity.Owner); + return; + } + } + + entity.Comp.ConnectedSupermatter ??= FindNearestSupermatter(entity); + } + + private void OnLinkAttempt(Entity entity, ref LinkAttemptEvent args) + { + if (args.SinkPort != entity.Comp.InputPort || HasComp(args.Source)) + return; + + args.Cancel(); + } + + private static void OnNewLink(Entity entity, ref NewLinkEvent args) + { + entity.Comp.ConnectedSupermatter = args.Source; + entity.Comp.NextUiUpdate = TimeSpan.Zero; + } + + private void OnPortDisconnected(Entity entity, ref PortDisconnectedEvent args) + { + if (args.Port != entity.Comp.InputPort) + return; + entity.Comp.ConnectedSupermatter = null; + UpdateUi(entity, true); + } + + private static void OnUiOpened(Entity entity, ref BoundUIOpenedEvent args) + { + if (!args.UiKey.Equals(SupermatterConsoleUiKey.Key)) + return; + + entity.Comp.NextUiUpdate = TimeSpan.Zero; + } + + private void OnSupermatterStartup(Entity supermatter, ref SupermatterStartupEvent args) + { + var query = EntityQueryEnumerator(); + var smTrans = Transform(supermatter.Owner); + + while (query.MoveNext(out var consoleUid, out var consoleComp, out var consoleTrans)) + { + if (consoleComp.ConnectedSupermatter != null) + continue; + var maxRange = consoleComp.MaxRange; + + if (!smTrans.Coordinates.TryDistance(EntityManager, consoleTrans.Coordinates, out var distance) + || distance > maxRange) + continue; + + _signalSystem.LinkDefaults(null, supermatter.Owner, consoleUid); + consoleComp.ConnectedSupermatter = supermatter.Owner; + } + } + + private void OnSendRadioEvent(Entity entity, ref SupermatterSendRadioEvent args) + { + var query = EntityQueryEnumerator(); + var radioSent = false; + + while (query.MoveNext(out var consoleUid, out var consoleComp)) + { + if (consoleComp.ConnectedSupermatter != entity) + continue; + + _chatSystem.TrySendInGameICMessage(consoleUid, args.Message, InGameICChatType.Speak, ChatTransmitRange.Normal); + + if (radioSent) + continue; + + foreach (var channel in entity.Comp.RadioChannels) + _radioSystem.SendRadioMessage(consoleUid, args.Message, channel, consoleUid); + + radioSent = true; + } + } + + private EntityUid? FindNearestSupermatter(Entity console) + { + var pos = Transform(console).Coordinates; + EntityUid? nearest = null; + var minDist = console.Comp.MaxRange; + + var smEnumerator = EntityQueryEnumerator(); + while (smEnumerator.MoveNext(out var smUid, out _, out var smTrans)) + { + if (!pos.TryDistance(EntityManager, smTrans.Coordinates, out var dist) + || dist > minDist) + continue; + + minDist = dist; + nearest = smUid; + } + + if (nearest != null) + _signalSystem.LinkDefaults(null, nearest.Value, console); + + return nearest; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var consUid, out var console)) + { + var uiOpen = _uiSystem.IsUiOpen(consUid, SupermatterConsoleUiKey.Key); + + if (console.ConnectedSupermatter == null) + { + if (uiOpen) + UpdateUi((consUid, console)); + continue; + } + + var smUid = console.ConnectedSupermatter.Value; + if (!TryComp(smUid, out var integrityComponent)) + { + console.ConnectedSupermatter = null; + if (Exists(smUid)) + _signalSystem.RemoveSinkFromSource(smUid, consUid); + + if (uiOpen) + UpdateUi((consUid, console)); + continue; + } + + var (integrityPercent, level) = CalculateIntegrity(integrityComponent); + var integrity = (int)Math.Round(integrityPercent); + var highestLevel = integrityComponent.SupermatterIntegrity.MaxBy(e => e.Threshold); + + if (highestLevel == null || integrity >= highestLevel.Threshold) + console.NextBeep = _timing.CurTime + console.BeepInterval; + else if (integrityComponent.Activated && _timing.CurTime >= console.NextBeep) + { + _audioSystem.PlayPvs(console.BeepSound, consUid); + console.NextBeep = _timing.CurTime + console.BeepInterval; + } + + if (!uiOpen || _timing.CurTime < console.NextUiUpdate) + continue; + + console.NextUiUpdate = _timing.CurTime + console.UiUpdateInterval; + + var supermatterEv = "—"; + if (TryComp(smUid, out var scheduler) && scheduler.Events?.Components is not null) + { + var locList = new List(); + foreach (var compStr in scheduler.Events.Components) + { + if (!_componentFactory.TryGetRegistration(compStr, out var registration)) + continue; + + if (!EntityManager.TryGetComponent(smUid, registration.Type, out var comp) || + comp is not ISupermatterEventComponent eventComp) + continue; + + if (eventComp.EventName != null) + locList.Add(Loc.GetString(eventComp.EventName)); + } + + supermatterEv = locList.Count > 0 + ? string.Join(", ", locList) + : "—"; + } + + var transComp = Transform(smUid); + var gas = _atmosSystem.GetContainingMixture((smUid, transComp)); + + var pressure = gas?.Pressure ?? 0f; + var temperature = gas?.Temperature ?? 0f; + + var state = new SupermatterConsoleBuiState( + activated: integrityComponent.Activated, + temperature: temperature, + integrityComponent.TempThresholds, + pressure: pressure, + integrityComponent.PressureThresholds, + integrity: integrityPercent, + integrityColor: level.Color, + currentEvent: supermatterEv + ); + + _uiSystem.SetUiState(consUid, SupermatterConsoleUiKey.Key, state); + } + } + + private void UpdateUi(Entity entity, bool force = false) + { + if (!_uiSystem.IsUiOpen(entity.Owner, SupermatterConsoleUiKey.Key)) + return; + if (!force && _timing.CurTime < entity.Comp.NextUiUpdate) + return; + + entity.Comp.NextUiUpdate = _timing.CurTime + entity.Comp.UiUpdateInterval; + + _uiSystem.SetUiState( + entity.Owner, + SupermatterConsoleUiKey.Key, + new SupermatterConsoleBuiState(activated: false)); + } + + private static (float integrity, SupermatterIntegrityLevel integrityLevel) + CalculateIntegrity(SupermatterIntegrityComponent component) + { + var integrity = component.Integrity / component.MaxIntegrity * 100f; + + foreach (var level in component.SupermatterIntegrity.Where(level => integrity >= level.Threshold)) + return (integrity, level); + + return (integrity, component.SupermatterIntegrity[^1]); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSchedulerSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSchedulerSystem.cs new file mode 100644 index 00000000000..0a74e88890e --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSchedulerSystem.cs @@ -0,0 +1,118 @@ +using Content.Server.Imperial.Power.Components; +using Content.Server.NukeOps; +using Content.Shared.Imperial.Power.Components; +using Content.Shared.Imperial.Power.Events; +using Content.Shared.NukeOps; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using SupermatterIntegrityComponent = Content.Shared.Imperial.Power.Components.SupermatterIntegrityComponent; + +namespace Content.Server.Imperial.Power.EntitySystems; + +public sealed class SupermatterEventSchedulerSystem : EntitySystem +{ + [Dependency] private readonly IComponentFactory _componentFactory = null!; + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IRobustRandom _random = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnWarOps); + SubscribeLocalEvent(OnEventFinished); + SubscribeLocalEvent(OnTouched); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + ent.Comp.FirstEventInterval ??= TimeSpan.FromSeconds(_random.Next(600, 900)); + ent.Comp.NextEventTime ??= _gameTiming.CurTime + ent.Comp.FirstEventInterval.Value; + } + + private void OnWarOps(ref WarDeclaredEvent args) + { + if (args.Status != WarConditionStatus.YesWar) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + comp.Disabled = true; + Log.Info($"Суперматерия ({uid}) более не может создавать события: война."); + } + } + + private void OnEventFinished(Entity ent, ref SupermatterEventFinishedEvent args) + { + var multiplier = 1f; + + if (TryComp(ent, out var gasComp)) + multiplier = MathF.Max(gasComp.RuntimeEventSpeedMultiplier, 0.01f); + + ent.Comp.NextEventTime = _gameTiming.CurTime + args.NextEventInterval / multiplier; + ent.Comp.ConcurrentEvents--; + } + + private void OnTouched(Entity ent, ref SupermatterTouchedEvent args) + { + if (TryComp(ent, out var gas) && gas.RuntimeDisableTouchGib) + { + args.Cancelled = true; + return; + } + + ent.Comp.Activated = true; + if (TryComp(ent, out var schedulerComp)) + schedulerComp.NextEventTime = _gameTiming.CurTime; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var currentTime = _gameTiming.CurTime; + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var schedComp, out var integrityComp)) + { + if (!integrityComp.Activated || schedComp.Disabled) + continue; + + if (currentTime > schedComp.NextEventTime) + StartRandomEvent((uid, schedComp)); + } + } + + private void StartRandomEvent(Entity ent) + { + if (ent.Comp.Events is { Components.Length: 0 } || ent.Comp.ConcurrentEvents >= ent.Comp.MaxConcurrentEvents) + return; + + var availableEvents = new List(); + + if (ent.Comp.Events is { Components: not null }) + { + foreach (var eventName in ent.Comp.Events.Components) + { + if (!_componentFactory.TryGetRegistration(eventName, out var registration)) + { + Log.Error($"Не найден компонент события: {eventName}"); + continue; + } + + if (!HasComp(ent, registration.Type)) + availableEvents.Add(registration); + } + } + + if (availableEvents.Count == 0) + return; + var randomEvent = _random.Pick(availableEvents); + + ent.Comp.NextEventTime = TimeSpan.MaxValue; + ent.Comp.ConcurrentEvents++; + var newComponent = _componentFactory.GetComponent(randomEvent); + AddComp(ent, newComponent); + } +} diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSystem.cs deleted file mode 100644 index 7a627f2ba52..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/SupermatterEventSystem.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Chat.Systems; -using Content.Server.Imperial.Power.Components; -using Content.Server.Imperial.Power.EntitySystems.Events; -using Content.Server.Lightning; -using Content.Server.NukeOps; -using Content.Server.Radio.EntitySystems; -using Content.Shared.Damage; -using Content.Shared.NukeOps; -using Content.Shared.Radiation.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Server.Imperial.Power.EntitySystems; -using Content.Shared.Chat; - -public sealed class SupermatterEventSystem : EntitySystem -{ - [Dependency] public readonly AtmosphereSystem Atmos = default!; - [Dependency] public readonly DamageableSystem Damageable = default!; - [Dependency] public readonly IGameTiming GameTiming = default!; - [Dependency] public readonly LightningSystem LightningSystem = default!; - [Dependency] public readonly SharedMapSystem MapSystem = default!; - [Dependency] private readonly ChatSystem _chatSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly RadioSystem _radio = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - - // Кеш ближайших консолей для кристаллов - private readonly Dictionary _nearestConsoleCache = new(); - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnTouched); - SubscribeLocalEvent(OnWarOps); - } - - private void OnInit(EntityUid uid, SupermatterEventComponent comp, ComponentInit args) - { - var currentTime = GameTiming.CurTime; - comp.LastConsoleCacheUpdate = currentTime; - comp.LastEventEndTimeUpdate = currentTime; - comp.LastNextEventTimerUpdate = currentTime; - comp.LastLightningCooldownUpdate = currentTime; - comp.LastPlasmaTickUpdate = currentTime; - } - - private void OnWarOps(ref WarDeclaredEvent args) - { - if (args.Status != WarConditionStatus.YesWar) - return; - - var componentEnumerator = EntityQueryEnumerator(); - while (componentEnumerator.MoveNext(out var uid, out var supermatterEventComponent)) - { - supermatterEventComponent.IsWarOps = true; - Log.Log(LogLevel.Info, $"Суперматерия ({uid}) более не может создавать события, так как статус войны теперь - {args.Status}"); - } - } - - private void OnTouched(EntityUid uid, SupermatterEventComponent comp, SupermatterTouchedEvent args) - { - TriggerEventNow(uid); - } - - private void TriggerEventNow(EntityUid uid) - { - if (!EntityManager.TryGetComponent(uid, out var comp)) - return; - comp.EventEndTime = TimeSpan.Zero; - comp.NextEventTimer = TimeSpan.Zero; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - var currentTime = GameTiming.CurTime; - var enumerator = EntityQueryEnumerator(); - - while (enumerator.MoveNext(out var uid, out var eventComponent, out var integrityComponent)) - { - // Очистка кэша консоли - if (currentTime - eventComponent.LastConsoleCacheUpdate >= eventComponent.ConsoleCacheLifetime) - { - _nearestConsoleCache.Clear(); - eventComponent.LastConsoleCacheUpdate = currentTime; - } - - var lastEvent = eventComponent.CurrentEvent; - - // Обновление времени окончания события - if (eventComponent.EventEndTime > TimeSpan.Zero) - { - var elapsedSinceLastUpdate = currentTime - eventComponent.LastEventEndTimeUpdate; - eventComponent.EventEndTime -= elapsedSinceLastUpdate; - if (eventComponent.EventEndTime < TimeSpan.Zero) - eventComponent.EventEndTime = TimeSpan.Zero; - eventComponent.LastEventEndTimeUpdate = currentTime; - } - - if (!integrityComponent.Activated) - { - eventComponent.LastNextEventTimerUpdate = currentTime; - eventComponent.LastEventEndTimeUpdate = currentTime; - continue; - } - - // Обновление таймера до следующего события - if (eventComponent.NextEventTimer > TimeSpan.Zero) - { - var elapsedSinceLastUpdate = currentTime - eventComponent.LastNextEventTimerUpdate; - eventComponent.NextEventTimer -= elapsedSinceLastUpdate; - if (eventComponent.NextEventTimer < TimeSpan.Zero) - eventComponent.NextEventTimer = TimeSpan.Zero; - eventComponent.LastNextEventTimerUpdate = currentTime; - } - - if (eventComponent.EventEndTime == TimeSpan.Zero - && eventComponent.NextEventTimer == TimeSpan.Zero && !eventComponent.IsWarOps) - { - if (lastEvent == SupermatterEventComponent.SupermatterEventType.Radiation) - { - var rad = Comp(uid); - rad.Intensity = eventComponent.DefaultRadiationIntensity; - } - - var randomEvtIndex = _random.Next(0, eventComponent.AllowedEventTypes.Count); - var randomEvtType = eventComponent.AllowedEventTypes[randomEvtIndex]; - - eventComponent.SupermatterEventTypesToEvents.TryGetValue(randomEvtType, out var eventHandler); - - switch (eventHandler) - { - case SupermatterNoneEvent: - SupermatterNoneEvent.Activate(uid, eventComponent, this); - AnnounceFromSupermatterConsole(uid, SupermatterNoneEvent.GetAnnouncement()); - break; - case SupermatterLightningEvent: - SupermatterLightningEvent.Activate(uid, eventComponent, this); - AnnounceFromSupermatterConsole(uid, SupermatterLightningEvent.GetAnnouncement()); - break; - case SupermatterRadiationEvent: - SupermatterRadiationEvent.Activate(uid, eventComponent, this); - AnnounceFromSupermatterConsole(uid, SupermatterRadiationEvent.GetAnnouncement()); - break; - case SupermatterPlasmaEvent: - SupermatterPlasmaEvent.Activate(uid, eventComponent, this); - AnnounceFromSupermatterConsole(uid, SupermatterPlasmaEvent.GetAnnouncement()); - break; - } - } - - if (eventComponent.EventEndTime == TimeSpan.Zero) - continue; - - if (eventComponent.SupermatterEventTypesToEvents.TryGetValue(eventComponent.CurrentEvent, out var newEventHandler)) - { - switch (newEventHandler) - { - case SupermatterNoneEvent: - SupermatterNoneEvent.Process(uid, eventComponent, this, currentTime); - break; - case SupermatterLightningEvent: - SupermatterLightningEvent.Process(uid, eventComponent, this, currentTime); - break; - case SupermatterRadiationEvent: - SupermatterRadiationEvent.Process(uid, eventComponent, this, currentTime); - break; - case SupermatterPlasmaEvent: - SupermatterPlasmaEvent.Process(uid, eventComponent, this, currentTime); - break; - } - } - } - } - - private void AnnounceFromSupermatterConsole(EntityUid crystal, string message) - { - var timeNow = (float)GameTiming.CurTime.TotalSeconds; - EntityUid? nearestConsole = null; - - var mapCoordinates = _transformSystem.GetMapCoordinates(crystal); - var crystalPos = mapCoordinates.Position; - var mapId = mapCoordinates.MapId; - - if (!EntityManager.TryGetComponent(crystal, out var eventComp)) - return; - - if (_nearestConsoleCache.TryGetValue(crystal, out var cached) && TimeSpan.FromSeconds(timeNow - cached.time) < eventComp.ConsoleCacheLifetime) - { - nearestConsole = cached.console; - } - else - { - var minDist = float.MaxValue; - var enumerator = EntityQueryEnumerator(); - while (enumerator.MoveNext(out var consoleUid, out _, out var transformComp)) - { - if (transformComp.MapID != mapId) - continue; - var consolePos = _transformSystem.GetMapCoordinates(consoleUid).Position; - var dist = (consolePos - crystalPos).LengthSquared(); - - if (dist > minDist) - continue; - - minDist = dist; - nearestConsole = consoleUid; - } - if (nearestConsole != null) - _nearestConsoleCache[crystal] = (nearestConsole.Value, timeNow); - } - - foreach (var channel in eventComp.RadioChannels) - { - _chatSystem.TrySendInGameICMessage(nearestConsole ?? crystal, message, InGameICChatType.Speak, ChatTransmitRange.Normal); - _radio.SendRadioMessage(nearestConsole ?? crystal, message, channel, nearestConsole ?? crystal); - } - } - - public void SetRadiation(EntityUid uid, float intensity) - { - if (EntityManager.TryGetComponent(uid, out var radComponent)) - radComponent.Intensity = intensity; - else - { - var newRad = EntityManager.EnsureComponent(uid); - newRad.Intensity = intensity; - } - } - - public bool TryGetComponent(EntityUid uid, out T? component) where T : IComponent - { - return EntityManager.TryGetComponent(uid, out component); - } -} diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterGasSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterGasSystem.cs new file mode 100644 index 00000000000..e040c718dba --- /dev/null +++ b/Content.Server/Imperial/Power/EntitySystems/SupermatterGasSystem.cs @@ -0,0 +1,130 @@ +using Content.Server.Atmos.Components; +using Content.Server.Imperial.Power.Components.EventComponents; +using Content.Server.Radiation.Systems; +using Content.Shared.Atmos; +using Content.Shared.Imperial.Power.Components; +using Content.Shared.Imperial.Power.Prototypes; +using Content.Shared.Radiation.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Power.EntitySystems; + +/// +/// Обрабатывает газовые эффекты суперматерии: +/// расход газов, самовосстановление и модификаторы радиации. +/// +public sealed class SupermatterGasSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly IPrototypeManager _protoMan = null!; + [Dependency] private readonly RadiationSystem _radiationSystem = null!; + + private readonly List<(int gasId, SupermatterGasReactionPrototype proto)> _reactions = []; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSupermatterGasInit); + SubscribeLocalEvent(OnAtmosExposedUpdate); + + CacheReactionPrototypes(); + } + + private void CacheReactionPrototypes() + { + _reactions.Clear(); + + foreach (var proto in _protoMan.EnumeratePrototypes()) + { + if (!AtmosCommandUtils.TryParseGasID(proto.Gas, out var gasId)) + { + Log.Error($"Supermatter gas reaction prototype '{proto.ID}' has invalid gas id '{proto.Gas}'."); + continue; + } + + _reactions.Add((gasId, proto)); + } + + _reactions.Sort((a, b) => b.proto.Priority.CompareTo(a.proto.Priority)); + } + + private void OnSupermatterGasInit(EntityUid uid, SupermatterGasComponent component, ComponentInit args) + { + EnsureComp(uid); + } + + private void OnAtmosExposedUpdate(EntityUid uid, SupermatterGasComponent component, ref AtmosExposedUpdateEvent args) + { + if (!TryComp(uid, out SupermatterIntegrityComponent? integrity)) + return; + + var now = _gameTiming.CurTime; + var frameTime = component.LastAtmosUpdate == TimeSpan.Zero + ? 0f + : (float) (now - component.LastAtmosUpdate).TotalSeconds; + + component.LastAtmosUpdate = now; + + var gas = args.GasMixture; + component.RuntimeRadiationMultiplier = 1f; + component.RuntimeLightningMultiplier = 1f; + component.RuntimeEventSpeedMultiplier = 1f; + component.RuntimeDisableTouchGib = false; + + foreach (var (gasId, proto) in _reactions) + { + if (!integrity.Activated && !proto.ConsumeWhenInactive) + continue; + + var moles = gas.GetMoles(gasId); + var excess = moles - component.GasActivationMoles; + if (excess <= 0f) + continue; + + gas.AdjustMoles(gasId, -component.GasConsumptionPerSecond); + } + + ApplyGasEffects((uid, integrity), (uid, component), gas, frameTime); + } + + private void ApplyGasEffects( + Entity integrity, + Entity gasComp, + GasMixture gas, + float frameTime) + { + var entMan = EntityManager; + var sysMan = EntityManager.EntitySysManager; + + foreach (var (gasId, proto) in _reactions) + { + var active = gas.GetMoles(gasId) > gasComp.Comp.GasActivationMoles; + if (!active && !proto.ProcessWhenBelowThreshold) + continue; + + if (!integrity.Comp.Activated && !proto.AppliesWhenInactive) + continue; + + foreach (var reaction in proto.Reactions) + { + reaction.React(integrity.Owner, integrity.Comp, gasComp.Comp, gas, frameTime, entMan, sysMan, active); + } + } + + if (!TryComp(integrity, out RadiationSourceComponent? radiation)) + return; + + var baseIntensity = radiation.Intensity; + + if (TryComp(integrity, out var eventComp)) + { + if (_gameTiming.CurTime <= eventComp.EndTime) + baseIntensity = eventComp.Intensity!.Value; + } + _radiationSystem.SetIntensity(integrity.Owner, baseIntensity * gasComp.Comp.RuntimeRadiationMultiplier); + } +} + + diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterIntegritySystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterIntegritySystem.cs index 7170e25d5ab..d8a32a2372e 100644 --- a/Content.Server/Imperial/Power/EntitySystems/SupermatterIntegritySystem.cs +++ b/Content.Server/Imperial/Power/EntitySystems/SupermatterIntegritySystem.cs @@ -2,72 +2,33 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Systems; using Content.Server.Explosion.EntitySystems; -using Content.Server.Imperial.Power.Components; using Content.Server.Lightning; -using Content.Server.Radio.EntitySystems; using Content.Server.Station.Systems; using Content.Shared.Audio; -using Content.Shared.Examine; +using Content.Shared.Imperial.Power.Components; using Content.Shared.Radiation.Components; -using Content.Shared.Tag; -using Robust.Shared.Physics.Events; using System.Linq; using Content.Shared.Chat; using Robust.Server.GameObjects; +using Robust.Shared.Timing; +using Content.Server.Radiation.Systems; +using Content.Shared.Explosion.Components; +using Content.Shared.Imperial.Power.Events; namespace Content.Server.Imperial.Power.EntitySystems; public sealed class SupermatterIntegritySystem : EntitySystem { - [Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!; - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly ChatSystem _chatSystem = default!; - [Dependency] private readonly ExplosionSystem _explosionSystem = default!; - [Dependency] private readonly LightningSystem _lightning = default!; - [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; - [Dependency] private readonly SharedPointLightSystem _lightSystem = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly RadioSystem _radioSystem = default!; - [Dependency] private readonly StationSystem _stationSystem = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnStartCollide); - } - - private void OnExamined(EntityUid uid, SupermatterIntegrityComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - args.PushMarkup(component.Activated - ? $"[color=yellow]{Loc.GetString("supermatter-status-active")}[/color]" - : $"[color=gray]{Loc.GetString("supermatter-status-inactive")}[/color]"); - - var integrityPercent = component.Integrity / component.MaxIntegrity * 100; - var integrityLevel = component.SupermatterIntegrity.First(entry => integrityPercent > entry.Threshold); - - args.PushMarkup(Loc.GetString(integrityLevel.Description)); - } - - private void OnStartCollide(EntityUid uid, SupermatterIntegrityComponent component, ref StartCollideEvent args) - { - var other = args.OtherEntity; - if (!_tagSystem.HasTag(other, component.HealTag)) - return; - - if (!component.Activated) - { - component.Activated = true; - SendSupermatterRadio(args.OurEntity,Loc.GetString("supermatter-activated"), component); - } - - component.Integrity = MathF.Min(component.MaxIntegrity, component.Integrity + component.EmitterHealAmount); - } - + [Dependency] private readonly AlertLevelSystem _alertLevelSystem = null!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = null!; + [Dependency] private readonly ChatSystem _chatSystem = null!; + [Dependency] private readonly ExplosionSystem _explosionSystem = null!; + [Dependency] private readonly IGameTiming _gameTiming = null!; + [Dependency] private readonly LightningSystem _lightning = null!; + [Dependency] private readonly RadiationSystem _radiationSystem = null!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = null!; + [Dependency] private readonly SharedPointLightSystem _lightSystem = null!; + [Dependency] private readonly StationSystem _stationSystem = null!; public override void Update(float frameTime) { @@ -75,72 +36,73 @@ public override void Update(float frameTime) var enumerator = EntityQueryEnumerator(); while (enumerator.MoveNext(out var uid, out var comp, out var transComp)) { - ProcessSupermatterUpdate(uid, comp, transComp, frameTime); + ProcessSupermatterUpdate((uid, comp), transComp); } } - private void ProcessSupermatterUpdate(EntityUid uid, SupermatterIntegrityComponent comp, TransformComponent transComp, float frameTime) + private void ProcessSupermatterUpdate(Entity ent, TransformComponent transComp) { - if (TryComp(uid, out RadiationSourceComponent? radiation)) - radiation.Enabled = comp.Activated; + if (TryComp(ent, out RadiationSourceComponent? radiation)) + _radiationSystem.SetSourceEnabled((ent, radiation), ent.Comp.Activated); - if (TryComp(uid, out PointLightComponent? light)) - _lightSystem.SetEnabled(uid, comp.Activated, light); + if (TryComp(ent, out PointLightComponent? light)) + _lightSystem.SetEnabled(ent, ent.Comp.Activated, light); - if (TryComp(uid, out AmbientSoundComponent? ambient)) + if (TryComp(ent, out AmbientSoundComponent? ambient)) { - if (comp.Activated) + if (ent.Comp.Activated) { - _ambientSound.SetVolume(uid, comp.AmbientSound[0].Volume, ambient); - _ambientSound.SetRange(uid, comp.AmbientSound[0].Range, ambient); + _ambientSound.SetVolume(ent, ent.Comp.AmbientSound[0].Volume, ambient); + _ambientSound.SetRange(ent, ent.Comp.AmbientSound[0].Range, ambient); } else { - _ambientSound.SetVolume(uid, comp.AmbientSound[1].Volume, ambient); - _ambientSound.SetRange(uid, comp.AmbientSound[1].Range, ambient); + _ambientSound.SetVolume(ent, ent.Comp.AmbientSound[1].Volume, ambient); + _ambientSound.SetRange(ent, ent.Comp.AmbientSound[1].Range, ambient); } } - if (!comp.Activated) + if (!ent.Comp.Activated) return; - var gas = _atmosphereSystem.GetContainingMixture((uid, transComp), true, true); + var gas = _atmosphereSystem.GetContainingMixture((ent, transComp), true, true); bool badConditions; if (gas != null) { - badConditions = gas.Temperature > comp.UpperTempThreshold - || gas.Temperature < comp.LowerTempThreshold - || gas.Pressure > comp.UpperPressureThreshold - || gas.Pressure < comp.LowerPressureThreshold; + badConditions = gas.Temperature < ent.Comp.TempThresholds.Item1 + || gas.Temperature > ent.Comp.TempThresholds.Item2 + || gas.Pressure < ent.Comp.PressureThresholds.Item1 + || gas.Pressure > ent.Comp.PressureThresholds.Item2; } else badConditions = true; - var integrityPercent = comp.Integrity / comp.MaxIntegrity * 100f; + var integrityPercent = ent.Comp.Integrity / ent.Comp.MaxIntegrity * 100f; // Сброс флага предупреждения для текущего уровня - var index = comp.SupermatterIntegrity.FindIndex(entry => integrityPercent > entry.Threshold); + var index = ent.Comp.SupermatterIntegrity.FindIndex(entry => integrityPercent > entry.Threshold); if (index >= 0) { - var oldEntry = comp.SupermatterIntegrity[index]; + var oldEntry = ent.Comp.SupermatterIntegrity[index]; if (oldEntry.Flag) - comp.SupermatterIntegrity[index] = (oldEntry.Threshold, oldEntry.Color, oldEntry.Description, oldEntry.Warning, false); + oldEntry.Flag = false; } - foreach (var level in comp.SupermatterIntegrity.OrderByDescending(entry => entry.Threshold)) + foreach (var level in ent.Comp.SupermatterIntegrity.OrderByDescending(entry => entry.Threshold)) { if (integrityPercent > level.Threshold || level.Flag || string.IsNullOrEmpty(level.Warning)) continue; var integrityWarning = Loc.GetString(level.Warning); - SendSupermatterRadio(uid, integrityWarning, comp); + var ev = new SupermatterSendRadioEvent(integrityWarning); + RaiseLocalEvent(ent, ref ev); // Если мы достигли уровня с порогом <= 10% — выставляем код тревоги для станции и объявление. // Раньше использовался MinBy по всем порогам (что возвращало 0) и из-за этого код не ставился. - if (level.Threshold <= 10f) + if (level.Threshold <= 30f) { - var station = _stationSystem.GetOwningStation(uid, transComp); + var station = _stationSystem.GetOwningStation(ent, transComp); if (station != null) { _alertLevelSystem.SetLevel(station.Value, "yellow", true, true, true); @@ -154,27 +116,21 @@ private void ProcessSupermatterUpdate(EntityUid uid, SupermatterIntegrityCompone } // Устанавливаем флаг предупреждения - var levelIndex = comp.SupermatterIntegrity.FindIndex(entry => Math.Abs(level.Threshold - entry.Threshold) < 1f); - if (levelIndex >= 0) - { - var updated = comp.SupermatterIntegrity[levelIndex]; - comp.SupermatterIntegrity[levelIndex] = (updated.Threshold, updated.Color, updated.Description, updated.Warning, true); - } + level.Flag = true; break; } // Обработка катастрофы - if (!comp.CatastropheActive && integrityPercent <= comp.CatastropheThreshold) + if (!ent.Comp.CatastropheActivated && integrityPercent <= ent.Comp.CatastropheThreshold) { - comp.CatastropheActive = true; - comp.CatastropheTimer = TimeSpan.Zero; // Начинаем с 0 - comp.CatastropheLightningTimer = TimeSpan.Zero; // Сбрасываем таймер молний + ent.Comp.CatastropheActivated = true; + ent.Comp.CatastropheEndTime = _gameTiming.CurTime + ent.Comp.CatastropheDuration; + ent.Comp.CatastropheLightningNextTime = _gameTiming.CurTime + ent.Comp.CatastropheLightningInterval; // Отправляем предупреждение о катастрофе - var station = _stationSystem.GetOwningStation(uid, transComp); + var station = _stationSystem.GetOwningStation(ent, transComp); if (station != null) { - _alertLevelSystem.SetLevel(station.Value, "red", true, true, true); _chatSystem.DispatchStationAnnouncement( station.Value, Loc.GetString("supermatter-station-catastrophe"), @@ -182,38 +138,23 @@ private void ProcessSupermatterUpdate(EntityUid uid, SupermatterIntegrityCompone colorOverride: Color.Red ); } - - // Отправляем сообщение в радио о начале катастрофы - SendSupermatterRadio(uid, Loc.GetString("supermatter-catastrophe-warning"), comp); } - if (comp.CatastropheActive) + if (ent.Comp.CatastropheActivated) { - comp.CatastropheTimer += TimeSpan.FromSeconds(frameTime); - // Молнии во время катастрофы - comp.CatastropheLightningTimer += TimeSpan.FromSeconds(frameTime); - if (comp.CatastropheLightningTimer >= comp.CatastropheLightningInterval) + if (_gameTiming.CurTime > ent.Comp.CatastropheLightningNextTime) { - comp.CatastropheLightningTimer = TimeSpan.Zero; - _lightning.ShootRandomLightnings(uid, comp.CatastropheLightningRange, comp.CatastropheLightningCount); + ent.Comp.CatastropheLightningNextTime = _gameTiming.CurTime + ent.Comp.CatastropheLightningInterval; + _lightning.ShootRandomLightnings(ent, ent.Comp.CatastropheLightningRange, ent.Comp.CatastropheLightningCount); } - if (comp.CatastropheTimer >= comp.CatastropheDuration) + if (_gameTiming.CurTime > ent.Comp.CatastropheEndTime) { - if (TryComp(uid, out TransformComponent? xformCat)) - { - var coords = _transformSystem.ToMapCoordinates(xformCat.Coordinates); - _explosionSystem.QueueExplosion( - coords, - comp.ExplosionPrototypeId, - comp.CatastropheTotalIntensity, - comp.CatastropheSlope, - comp.CatastropheMaxTileIntensity, - cause: uid - ); - } - EntityManager.QueueDeleteEntity(uid); + if (TryComp(ent, out var explosive)) + _explosionSystem.TriggerExplosive(ent, explosive); + else + QueueDel(ent); return; } } @@ -221,20 +162,22 @@ private void ProcessSupermatterUpdate(EntityUid uid, SupermatterIntegrityCompone // Обработка урона от плохих условий if (badConditions) { - comp.TickAccumulator += TimeSpan.FromSeconds(frameTime); - while (comp.TickAccumulator >= comp.DamageTickInterval) - { - comp.TickAccumulator -= comp.DamageTickInterval; - var tickAmount = comp.TickDamage.DamageDict.Values.Sum(v => (float)v); - comp.Integrity = MathF.Max(0, comp.Integrity - tickAmount); - } - } - } + if (ent.Comp.NextDamageTime == TimeSpan.MaxValue) + ent.Comp.NextDamageTime = _gameTiming.CurTime + ent.Comp.DamageInterval; - // Отправка сообщения в общую рацию от имени суперматерии - private void SendSupermatterRadio(EntityUid source, string message, SupermatterIntegrityComponent component) - { - _chatSystem.TrySendInGameICMessage(source, message, InGameICChatType.Speak, ChatTransmitRange.Normal); - _radioSystem.SendRadioMessage(source, message, component.RadioChannel, source); + if (_gameTiming.CurTime < ent.Comp.NextDamageTime) + return; + + ent.Comp.NextDamageTime = _gameTiming.CurTime + ent.Comp.DamageInterval; + + var tickAmount = ent.Comp.TickDamage.GetTotal().Float(); + if (tickAmount <= 0) + tickAmount = 0.65f; + + ent.Comp.Integrity = MathF.Max(0, ent.Comp.Integrity - tickAmount); + Dirty(ent); + } + else + ent.Comp.NextDamageTime = TimeSpan.MaxValue; } } diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterMonitorConsoleSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterMonitorConsoleSystem.cs deleted file mode 100644 index c530becee01..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/SupermatterMonitorConsoleSystem.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Imperial.Power.Components; -using Content.Shared.Examine; -using Robust.Shared.Audio.Systems; -using System.Linq; - -namespace Content.Server.Imperial.Power.EntitySystems; - -public sealed class SupermatterMonitorConsoleSystem : EntitySystem -{ - [Dependency] private readonly AtmosphereSystem _atmosSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamined); - } - - private void OnExamined(EntityUid uid, SupermatterMonitorConsoleComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - var nearestUid = FindNearestSupermatter(uid); - if (nearestUid == null || !TryComp(nearestUid, out SupermatterIntegrityComponent? nearest)) - { - args.PushMarkup(Loc.GetString("supermatter-monitor-none-nearby")); - return; - } - - if (!nearest.Activated) - { - args.PushMarkup(Loc.GetString("supermatter-monitor-inactive")); - return; - } - - var (integrityPercent, level) = CalculateIntegrity(nearest); - var integrity = (int)Math.Round(integrityPercent); - args.PushMarkup(Loc.GetString("supermatter-monitor-integrity", - ("integrity", integrity), - ("color", level.Color))); - - var transComp = Transform(nearestUid.Value); - var gas = _atmosSystem.GetContainingMixture((nearestUid.Value, transComp)); - if (gas == null) - return; - - var pressure = (int)Math.Round(gas.Pressure); - var temperature = (int)Math.Round(gas.Temperature); - args.PushMarkup(Loc.GetString("supermatter-monitor-atmospherics", - ("pressure", pressure), - ("temperature", temperature))); - } - - private EntityUid? FindNearestSupermatter(EntityUid consoleUid) - { - var transformCompConsole = Transform(consoleUid); - var mapId = transformCompConsole.MapID; - var pos = _transformSystem.GetMapCoordinates(transformCompConsole).Position; - - EntityUid? nearest = null; - var minDist = float.MaxValue; - - var smEnumerator = EntityQueryEnumerator(); - while (smEnumerator.MoveNext(out var smUid, out _, out var transComp)) - { - if (transComp.MapID != mapId) - continue; - - var smPos = _transformSystem.GetMapCoordinates(smUid).Position; - var dist = (smPos - pos).LengthSquared(); - if (dist > minDist) - continue; - - minDist = dist; - nearest = smUid; - } - return nearest; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var enumerator = EntityQueryEnumerator(); - while (enumerator.MoveNext(out var uid, out var console, out _)) - { - var nearestUid = FindNearestSupermatter(uid); - if (nearestUid == null - || !EntityManager.TryGetComponent(nearestUid.Value, out var nearest) - || !nearest.Activated) - { - console.BeepCooldownTimer = TimeSpan.Zero; - continue; - } - - var (integrityPercent, _) = CalculateIntegrity(nearest); - var integrity = (int)Math.Round(integrityPercent); - - var highestThreshold = nearest.SupermatterIntegrity.MaxBy(e => e.Threshold).Threshold; - if (integrity >= highestThreshold) - { - console.BeepCooldownTimer = TimeSpan.Zero; - } - else - { - console.BeepCooldownTimer -= TimeSpan.FromSeconds(frameTime); - if (console.BeepCooldownTimer > TimeSpan.Zero) - continue; - - _audioSystem.PlayPvs(console.BeepSound, uid); - console.BeepCooldownTimer = TimeSpan.FromSeconds(2); - } - } - } - - private static (float integrity, (float Threshold, Color Color, LocId Description, LocId Warning, bool Flag) integrityLevel) - CalculateIntegrity(SupermatterIntegrityComponent component) - { - var integrity = component.Integrity / component.MaxIntegrity * 100f; - - // Выбираем самый высокий уровень, порог которого меньше или равен текущей целостности. - var ordered = component.SupermatterIntegrity.OrderByDescending(e => e.Threshold).ToList(); - var idx = ordered.FindIndex(entry => integrity >= entry.Threshold); - - var level = idx >= 0 ? ordered[idx] : component.SupermatterIntegrity.MinBy(e => e.Threshold); - - return (integrity, level); - } -} diff --git a/Content.Server/Imperial/Power/EntitySystems/SupermatterTouchSystem.cs b/Content.Server/Imperial/Power/EntitySystems/SupermatterTouchSystem.cs deleted file mode 100644 index 1d43a6c2b73..00000000000 --- a/Content.Server/Imperial/Power/EntitySystems/SupermatterTouchSystem.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Content.Server.Effects; -using Content.Server.Imperial.Power.Components; -using Content.Shared.Mobs.Components; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Physics.Events; -using Robust.Shared.Player; - -namespace Content.Server.Imperial.Power.EntitySystems; - -public sealed class SupermatterTouchSystem : EntitySystem -{ - [Dependency] private readonly ColorFlashEffectSystem _colorFlash = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStartCollide); - } - - private void OnStartCollide(EntityUid uid, SupermatterTouchComponent component, ref StartCollideEvent args) - { - var other = args.OtherEntity; - if (!EntityManager.HasComponent(other)) - return; - - var transformComp = Transform(other); - - GibCollidedEntity(component, transformComp, other, uid); - RaiseLocalEvent(uid, new SupermatterTouchedEvent()); - } - - private void GibCollidedEntity(SupermatterTouchComponent supermatterTouchComponent, TransformComponent transformComp, EntityUid entityUid, EntityUid supermatterUid) - { - _audio.PlayPvs(supermatterTouchComponent.GibSound, transformComp.Coordinates); - _colorFlash.RaiseEffect(supermatterTouchComponent.FlashColor, [supermatterUid], Filter.Pvs(supermatterUid)); - EntityManager.QueueDeleteEntity(entityUid); - EntityManager.SpawnEntity(supermatterTouchComponent.AshPrototype, transformComp.Coordinates); - - if (TryComp(supermatterUid, out var integrityComponent) && !integrityComponent.Activated) - integrityComponent.Activated = true; - } -} - -public sealed class SupermatterTouchedEvent : EntityEventArgs; diff --git a/Content.Server/Imperial/PrinterDoc/PrinterDocSystem.cs b/Content.Server/Imperial/PrinterDoc/PrinterDocSystem.cs index ebe91fdc3ad..17ae758f085 100644 --- a/Content.Server/Imperial/PrinterDoc/PrinterDocSystem.cs +++ b/Content.Server/Imperial/PrinterDoc/PrinterDocSystem.cs @@ -32,7 +32,7 @@ public override void Initialize() private void OnPrinterDocCheckIdCard(PrinterDocCheckIdCardMessage msg, EntitySessionEventArgs args) { var uid = args.SenderSession.AttachedEntity; - if (uid == null || !EntityManager.TryGetComponent(uid, out var latheComponent)) + if (uid == null || !TryComp(uid, out var latheComponent)) return; latheComponent.UseCardId = msg.UseCardId; @@ -41,10 +41,9 @@ private void OnPrinterDocCheckIdCard(PrinterDocCheckIdCardMessage msg, EntitySes public void TrySetContentPrintedDocument(EntityUid uid, EntityUid? userId, bool useCardId) { - if (!HasComp(uid)) + if (!TryComp(uid, out var paperComp)) return; - var paperComp = EntityManager.GetComponent(uid); var content = paperComp.Content; var stationName = GetStationNameForObject(uid); var currentDate = DateTime.Now.AddYears(1000).ToString("dd/MM/yyyy").Replace(".", "/"); @@ -68,21 +67,18 @@ public void TrySetContentPrintedDocument(EntityUid uid, EntityUid? userId, bool { if (idCard.Comp != null) { - string profession = string.Empty; + var profession = string.Empty; + var idCardEntity = idCard.Owner; + var metaData = MetaData(idCardEntity); - EntityUid idCardEntity = idCard.Comp.Owner; + var entityName = metaData.EntityName; - if (TryComp(idCardEntity, out var metaData)) - { - string entityName = metaData.EntityName; - - Regex regex = new Regex(@"\((.*?)\)"); - Match match = regex.Match(entityName); + var regex = new Regex(@"\((.*?)\)"); + var match = regex.Match(entityName); - if (match.Success && match.Groups.Count > 1) - { - profession = match.Groups[1].Value; - } + if (match.Success && match.Groups.Count > 1) + { + profession = match.Groups[1].Value; } content = content @@ -104,10 +100,10 @@ private string GetStationNameForObject(EntityUid uid) { var stationUid = _stationSystem.GetOwningStation(uid); - if (stationUid == null || !EntityManager.TryGetComponent(stationUid.Value, out var stationMetaData)) + if (stationUid == null) return string.Empty; - return stationMetaData.EntityName; + return MetaData(stationUid.Value).EntityName; } public bool TryToCheckPrinter(EntityUid uid) @@ -127,16 +123,16 @@ public bool TryToCheckPrinter(EntityUid uid) return false; } - public void TryAddPaperToPrinter(EntityUid toInsert, MaterialStorageComponent? storage, EntityUid receiver, SharedPopupSystem _popup, IAdminLogManager _adminLogger, EntityUid user) + public void TryAddPaperToPrinter(EntityUid toInsert, MaterialStorageComponent? storage, EntityUid receiver, SharedPopupSystem popup, IAdminLogManager adminLogger, EntityUid user) { if (HasComp(toInsert) && TryToCheckPrinter(receiver)) { _materialStorageSystem.TryChangeMaterialAmount(receiver, "Paper", 100, storage); - _popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver), ("item", toInsert)), receiver); + popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver), ("item", toInsert)), receiver); QueueDel(toInsert); - _adminLogger.Add(LogType.Action, LogImpact.Low, + adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} inserted paper into {ToPrettyString(receiver):receiver}"); } } diff --git a/Content.Server/Imperial/Revolution/ConsentRevolutionSystem.cs b/Content.Server/Imperial/Revolution/ConsentRevolutionSystem.cs index 1a851f64090..cfeb52e6c3b 100644 --- a/Content.Server/Imperial/Revolution/ConsentRevolutionSystem.cs +++ b/Content.Server/Imperial/Revolution/ConsentRevolutionSystem.cs @@ -87,7 +87,7 @@ private void HandleInnateVerb(EntityUid userUid, HeadRevolutionaryComponent head var alwaysConvertible = HasComp(verbArgs.Target); - if ((!HasComp(verbArgs.Target) || + if ((!HasComp(verbArgs.Target) || !_mindSystem.TryGetMind(verbArgs.Target, out var mindId, out var mind)) && !alwaysConvertible) { diff --git a/Content.Server/Imperial/SCP/ChaseMusic/Components/ImperialSCPChaseMusicComponent.cs b/Content.Server/Imperial/SCP/ChaseMusic/Components/ImperialSCPChaseMusicComponent.cs new file mode 100644 index 00000000000..cac1def88eb --- /dev/null +++ b/Content.Server/Imperial/SCP/ChaseMusic/Components/ImperialSCPChaseMusicComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Map; +using Robust.Shared.Audio; +using Content.Shared.Chat.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.Damage; +using Robust.Shared.Prototypes; +namespace Content.Server.Imperial.SCP.ChaseMusic.Components; + +[RegisterComponent] +public sealed partial class ImperialSCPChaseMusicComponent : Component +{ + [ViewVariables, DataField] + public SoundSpecifier ChaseSound = new SoundPathSpecifier("/Audio/Imperial/Seriozha/SCP/chase/chase1.ogg"); + [ViewVariables, DataField] + public EntProtoId ChaseMusicToggleAction = "ActionSCPChaseMusic"; + public EntityUid? ChaseMusicToggleActionEntity; + + [ViewVariables] + public EntityUid? PlayingStream; + + [ViewVariables] + public bool IsPlaying = false; +} diff --git a/Content.Server/Imperial/SCP/ChaseMusic/Systems/ImperialSCPChaseMusicSystem.cs b/Content.Server/Imperial/SCP/ChaseMusic/Systems/ImperialSCPChaseMusicSystem.cs new file mode 100644 index 00000000000..44bd2ad1e0d --- /dev/null +++ b/Content.Server/Imperial/SCP/ChaseMusic/Systems/ImperialSCPChaseMusicSystem.cs @@ -0,0 +1,100 @@ +using Content.Server.Imperial.SCP.ChaseMusic.Components; +using Content.Shared.Actions; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Content.Shared.Imperial.SCP.ChaseMusic.Events; + +namespace Content.Server.Imperial.SCP.ChaseMusic.Systems; + +public sealed class ImperialSCPChaseMusicSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnToggleChaseMusic); + SubscribeLocalEvent(OnMobStateChanged); + } + + private void OnInit(EntityUid uid, ImperialSCPChaseMusicComponent comp, ComponentInit args) + { + _actions.AddAction(uid, ref comp.ChaseMusicToggleActionEntity, comp.ChaseMusicToggleAction); + } + + private void OnRemove(EntityUid uid, ImperialSCPChaseMusicComponent comp, ComponentRemove args) + { + StopChaseMusic(uid, comp); + + if (comp.ChaseMusicToggleActionEntity != null) + _actions.RemoveAction(uid, comp.ChaseMusicToggleActionEntity.Value); + } + + private void OnToggleChaseMusic(EntityUid uid, ImperialSCPChaseMusicComponent comp, ImperialSCPChaseMusicActionEvent args) + { + if (args.Handled) + return; + + if (comp.IsPlaying) + { + StopChaseMusic(uid, comp); + } + else + { + StartChaseMusic(uid, comp); + } + + args.Handled = true; + } + + public void StartChaseMusic(EntityUid uid, ImperialSCPChaseMusicComponent comp) + { + if (comp.IsPlaying) + return; + comp.PlayingStream = _audio.PlayPvs( + comp.ChaseSound, + uid, + AudioParams.Default.WithLoop(true) + )?.Entity; + + comp.IsPlaying = true; + } + + public void StopChaseMusic(EntityUid uid, ImperialSCPChaseMusicComponent comp) + { + if (!comp.IsPlaying) + return; + if (comp.PlayingStream != null) + { + _audio.Stop(comp.PlayingStream.Value); + comp.PlayingStream = null; + } + comp.IsPlaying = false; + } + private void OnMobStateChanged(EntityUid uid, ImperialSCPChaseMusicComponent comp, MobStateChangedEvent args) + { + if (_mobStateSystem.IsDead(uid) || _mobStateSystem.IsCritical(uid)) + { + StopChaseMusic(uid, comp); + if (comp.ChaseMusicToggleActionEntity != null) + { + _actions.RemoveAction(uid, comp.ChaseMusicToggleActionEntity); + comp.ChaseMusicToggleActionEntity = null; + } + } + else if (_mobStateSystem.IsAlive(uid)) + { + if (comp.ChaseMusicToggleActionEntity == null && !string.IsNullOrEmpty(comp.ChaseMusicToggleAction)) + { + _actions.AddAction(uid, ref comp.ChaseMusicToggleActionEntity, comp.ChaseMusicToggleAction); + } + } + } +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/NothingThere/Components/ImperialControlledNothingThereComponent.cs b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialControlledNothingThereComponent.cs new file mode 100644 index 00000000000..e3e538d80f7 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialControlledNothingThereComponent.cs @@ -0,0 +1,30 @@ +using Robust.Shared.Map; +using Robust.Shared.Audio; +using Content.Shared.Chat.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.Damage; +using Robust.Shared.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Mobs.Systems; +namespace Content.Server.Imperial.SCP.NothingThere.Components; + +[RegisterComponent] +public sealed partial class ImperialControlledNothingThereComponent : Component +{ + [DataField("gibBodyAction")] + public EntProtoId GibBodyAction = "ImperialNothingThereGibBodyAction"; + [ViewVariables] + public EntityUid? GibBodyEntity; + [ViewVariables] + public EntityUid OriginalBody = EntityUid.Invalid; + [ViewVariables] + public SoundSpecifier ExitSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_gibbody.ogg"); + [ViewVariables] + public int KillCount = 0; + [ViewVariables] + public SoundSpecifier GibSound = new SoundCollectionSpecifier("gib", AudioParams.Default.WithVariation(0.025f)); + [ViewVariables] + public float GibletLaunchImpulse = 150; + [ViewVariables] + public float GibletLaunchImpulseVariance = 3; +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereComponent.cs b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereComponent.cs new file mode 100644 index 00000000000..2e18c9ca986 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereComponent.cs @@ -0,0 +1,94 @@ +using Robust.Shared.Map; +using Robust.Shared.Audio; +using Content.Shared.Chat.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.Damage; +using Robust.Shared.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Mobs.Systems; +using Content.Shared.Mobs; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Polymorph; +namespace Content.Server.Imperial.SCP.NothingThere.Components; + +[RegisterComponent] +public sealed partial class ImperialNothingThereComponent : Component +{ + [DataField] + public SoundSpecifier ChaseSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_idle.ogg"); + + [ViewVariables] + public EntityUid? PlayingStream; + + [ViewVariables] + public bool IsPlaying = false; + [DataField("enterBodyAction")] + public EntProtoId EnterBodyAction = "ImperialNothingThereEnterBodyAction"; + public EntityUid? EnterBodyEntity; + + [DataField] + public TimeSpan EnterBodyWindUp = TimeSpan.FromSeconds(2); + [DataField("threshold", required: true)] + public FixedPoint2 Threshold = new(); + [DataField("killsRequired")] + public int KillsRequired = 5; + + [DataField("killCount")] + public int KillCount = 0; + + [DataField("phase")] + public NothingTherePhase Phase = NothingTherePhase.Original; + public EntProtoId? EggEntityProto = "ImperialSCPNothingThereEgg"; + [DataField("transformEggAction")] + public EntProtoId TransformEggAction = "ImperialNothingThereEggAction"; + [DataField("transformEggEntity")] + public EntityUid? TransformEggEntity; + [DataField("enterSound")] + public SoundSpecifier EnterSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_enter.ogg"); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan EggTransformDuration = TimeSpan.FromSeconds(30); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan EggTransformEnd = TimeSpan.Zero; + [ViewVariables] + public EntProtoId? TrueEntityProto = "ImperialSCPNothingThereTrue"; + [DataField("hatchSound")] + public SoundSpecifier HatchSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_hatch.ogg"); + [ViewVariables] + public bool Empowered = false; + [DataField("empowerAction")] + public EntProtoId EmpowerAction = "ImperialNothingThereHelloAction"; + [DataField("empowerEntity")] + public EntityUid? EmpowerEntity; + [DataField("empowerSound")] + public SoundSpecifier EmpowerSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_empower.ogg"); + [DataField(required: true), ViewVariables] + public DamageSpecifier EmpowerDamage = new(); + [ViewVariables] + public ProtoId EggMorph = "NothingThereEggMorph"; + [ViewVariables] + public ProtoId TrueMorph = "NothingThereTrueMorph"; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [ViewVariables] + public TimeSpan GoodbyeDelay = TimeSpan.FromSeconds(0.5f); + [ViewVariables] + public EntProtoId GoodbyeProto = "ImperialNothingThereEmpowerHit"; + [ViewVariables] + public EntProtoId HitProto = "ImperialNothingThereHit"; + [DataField] + public bool NeedItems = false; + [ViewVariables] + public bool NeedGoodbye = false; + [DataField("projectileAction")] + public EntProtoId ProjectileAction = "ImperialNothingThereProjectileAction"; + [DataField("projectileEntity")] + public EntityUid? ProjectileEntity; +} + +public enum NothingTherePhase : byte +{ + Original = 0, + Egg = 1, + True = 2 +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereGoodbyeComponent.cs b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereGoodbyeComponent.cs new file mode 100644 index 00000000000..9f634442fd6 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Components/ImperialNothingThereGoodbyeComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +[RegisterComponent] +public sealed partial class ImperialNothingThereGoodbyeComponent : Component +{ + [ViewVariables] + public bool Used = false; + [ViewVariables] + public EntProtoId HitProto = "ImperialNothingThereHit"; + [DataField("empowerSound")] + public SoundSpecifier EmpowerSound = new SoundPathSpecifier("/Audio/Imperial/SCP/nothingthere_empower.ogg"); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan AttackDuration = TimeSpan.FromSeconds(1.5); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan AttackEnd = TimeSpan.Zero; + [ViewVariables] + public EntityUid WeaponUser = EntityUid.Invalid; +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThere.ChaseMusic.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThere.ChaseMusic.cs new file mode 100644 index 00000000000..d1b574196f3 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThere.ChaseMusic.cs @@ -0,0 +1,60 @@ +using Content.Server.Imperial.SCP.NothingThere.Components; +using Robust.Shared.Audio; +using Content.Shared.Mobs; + +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem +{ + + private void InitializeChaseMusic() + { + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnMobStateChanged); + } + private void OnRemove(EntityUid uid, ImperialNothingThereComponent comp, ComponentRemove args) + { + StopChaseMusic(uid, comp); + } + + public void StartChaseMusic(EntityUid uid, ImperialNothingThereComponent comp) + { + if (comp.IsPlaying) + return; + if (comp.Phase == NothingTherePhase.True) + return; + comp.PlayingStream = _audio.PlayPvs( + comp.ChaseSound, + uid, + AudioParams.Default.WithLoop(true) + )?.Entity; + + comp.IsPlaying = true; + } + + public void StopChaseMusic(EntityUid uid, ImperialNothingThereComponent comp) + { + if (!comp.IsPlaying) + return; + if (comp.Phase == NothingTherePhase.True) + return; + if (comp.PlayingStream != null) + { + _audio.Stop(comp.PlayingStream.Value); + comp.PlayingStream = null; + } + comp.IsPlaying = false; + } + private void OnMobStateChanged(EntityUid uid, ImperialNothingThereComponent comp, MobStateChangedEvent args) + { + if (_mobStateSystem.IsDead(uid) || _mobStateSystem.IsCritical(uid)) + { + StopChaseMusic(uid, comp); + } + else if (_mobStateSystem.IsAlive(uid)) + { + StartChaseMusic(uid, comp); + } + } +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Arsenal.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Arsenal.cs new file mode 100644 index 00000000000..be6ac394bac --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Arsenal.cs @@ -0,0 +1,128 @@ + +using Content.Server.Imperial.SCP.NothingThere.Components; +using Content.Shared.Imperial.SCP.NothingThere.Events; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Hands.Components; +using Content.Shared.Popups; +using Content.Shared.DoAfter; +using Content.Shared.Mobs.Components; +using Robust.Shared.Timing; +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem +{ + #region Initialize + private void InitializeArsenal() + { + SubscribeLocalEvent(OnGoodbyeAction); + SubscribeLocalEvent(OnGoodbyeDoAfter); + SubscribeLocalEvent(OnGoodbyeAttack); + } + #endregion + #region Item provision + private void UpdateItemProvide() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.NeedItems) + continue; + var hands = EnsureComp(uid); + if (_hands.TryGetEmptyHand((uid, hands), out var emptyHand)) + { + EntityUid? hit; + if (comp.NeedGoodbye) + { + hit = Spawn(comp.GoodbyeProto, Transform(uid).Coordinates); + comp.NeedGoodbye = false; + } + else + { + hit = Spawn(comp.HitProto, Transform(uid).Coordinates); + } + if (!_hands.TryForcePickup(uid, hit ?? EntityUid.Invalid, emptyHand, checkActionBlocker: false, handsComp: hands)) + { + QueueDel(hit); + return; + } + else + comp.NeedItems = false; + } + } + var weaponquery = EntityQueryEnumerator(); + while (weaponquery.MoveNext(out var wpnuid, out var wpncomp)) + { + var curTime = _gameTiming.CurTime; + if (wpncomp.WeaponUser == EntityUid.Invalid || wpncomp.AttackEnd < curTime) + continue; + var user = wpncomp.WeaponUser; + if (!TryComp(user, out var hands)) + continue; + wpncomp.AttackEnd = TimeSpan.Zero; + wpncomp.WeaponUser = EntityUid.Invalid; + var handd = _hands.GetActiveHand((user, hands)); + _hands.DoDrop((user, hands), handd!, false, false); + if (Exists(wpnuid)) + QueueDel(wpnuid); + if (TryComp(user, out var scp)) + { + scp.NeedItems = true; + } + } + } + #endregion + #region Goodbye Handle + private void OnGoodbyeAction(Entity ent, ref ImperialNothingThereGoodbyeEvent args) + { + if (args.Handled) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, ent, ent.Comp.GoodbyeDelay, new ImperialNothingThereGoodbyeDoAfterEvent(), ent) + { + BreakOnMove = true, + BreakOnDamage = false, + NeedHand = false + }; + if (!TryComp(args.Performer, out var mob)) + return; + if (!TryComp(args.Performer, out var comp)) + return; + if (_doAfterSystem.TryStartDoAfter(doAfterArgs)) + { + args.Handled = true; + _popupSystem.PopupEntity(Loc.GetString("nothing-there-goodbye-windup"), ent, PopupType.MediumCaution); + } + args.Handled = true; + } + private void OnGoodbyeDoAfter(Entity ent, ref ImperialNothingThereGoodbyeDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + var hands = EnsureComp(args.User); + var handd = _hands.GetActiveHand((args.User, hands)); + var wpn = _hands.GetActiveItem((args.User, hands)); + var user = args.User; + _hands.DoDrop((args.User, hands), handd!, false, false); + QueueDel(wpn); + ent.Comp.NeedGoodbye = true; + ent.Comp.NeedItems = true; + } + + private void OnGoodbyeAttack(EntityUid uid, ImperialNothingThereGoodbyeComponent comp, ref MeleeHitEvent args) + { + if (comp.Used || args.Handled) + return; + if (!TryComp(args.User, out var hands)) + return; + args.Handled = true; + _audio.PlayPvs(comp.EmpowerSound, args.User); + comp.Used = true; + var user = args.User; + var handd = _hands.GetActiveHand((user, hands)); + comp.WeaponUser = user; + var curTime = _gameTiming.CurTime; + comp.AttackEnd = curTime + comp.AttackDuration; + } + #endregion +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.BodyControl.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.BodyControl.cs new file mode 100644 index 00000000000..36527381b50 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.BodyControl.cs @@ -0,0 +1,205 @@ +using Content.Server.Imperial.SCP.NothingThere.Components; +using Content.Shared.Imperial.SCP.NothingThere.Events; +using Content.Shared.Mobs; +using Content.Shared.Atmos.Rotting; +using Content.Shared.Popups; +using Content.Shared.DoAfter; +using Content.Shared.Mobs.Components; +using Content.Shared.Hands.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Damage.Components; +using Content.Shared.Humanoid; +using Robust.Shared.Audio; +using Content.Shared.Chat; +using Content.Shared.Gibbing; +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem +{ + private void InitializeBodyControl() + { + SubscribeLocalEvent(OnControlledMobStateChanged); + SubscribeLocalEvent(OnEnterBodyAction); + SubscribeLocalEvent(OnEnterBodyDoAfter); + SubscribeLocalEvent(OnGibBodyAction); + SubscribeLocalEvent(OnInitControlled); + } + private void OnInitControlled(EntityUid uid, ImperialControlledNothingThereComponent comp, ComponentInit args) + { + if (comp.GibBodyEntity == null) + { + _actions.AddAction(uid, + ref comp.GibBodyEntity, + comp.GibBodyAction); + } + } + private void OnEnterBodyAction(Entity ent, ref ImperialNothingThereEnterBodyEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + var target = args.Target; + + if (target == ent.Owner) + return; // no dude + if (!TryComp(target, out var mobstate)) + { + _popupSystem.PopupEntity( + Loc.GetString("nothingthere-hammaggotson-notalive"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + return; + } + if (mobstate.CurrentState != MobState.Dead) + { + _popupSystem.PopupEntity( + Loc.GetString("nothingthere-hammaggotson-notdead"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + return; + } + if (HasComp(target)) + { + _popupSystem.PopupEntity( + Loc.GetString("nothingthere-hammaggotson-rotting"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + return; + } + if (!HasComp(target)) + { + _popupSystem.PopupEntity( + Loc.GetString("nothingthere-hammaggotson-nonhuman"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + return; + } + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.EnterBodyWindUp, new ImperialNothingThereEnterBodyDoAfterEvent(), ent, target: target, used: ent) + { + BreakOnMove = true, + BlockDuplicate = true, + DuplicateCondition = DuplicateConditions.None, + }); + var othersMessage = Loc.GetString("nothingthere-hammaggotson-windup-others"); + _popupSystem.PopupEntity( + othersMessage, + args.Performer, + args.Performer, + PopupType.MediumCaution); + } + + private void OnEnterBodyDoAfter(Entity entity, ref ImperialNothingThereEnterBodyDoAfterEvent args) + { + var target = args.Target ?? EntityUid.Invalid; + if (!_mind.TryGetMind(args.User, out var mindId, out var mind)) + return; + if (args.Cancelled || args.Handled || entity.Comp.Deleted) + return; + if (!TryComp(args.User, out var comp)) + return; + if (!TryComp(target, out var thrd)) + return; + if (!TryComp(target, out var mob)) + return; + var control = EnsureComp(target); + if (TryComp(target, out var hands)) + { + foreach (var hand in hands.Hands.Keys) + { + _hands.TryDrop((target, hands), hand!); + } + RemComp(target); + } + if (!_mobThresholdSystem.TryGetDeadThreshold(target, out var ddde)) + return; + RemComp(target); + var nddde = ddde + comp.Threshold ?? FixedPoint2.New(400.0f); + _mobThresholdSystem.SetMobStateThreshold(target, nddde, MobState.Dead, thrd); + _mobThresholdSystem.SetMobStateThreshold(target, FixedPoint2.MaxValue, MobState.Critical, thrd); + _mobStateSystem.ChangeMobState(target, MobState.Alive, mob, args.User); + _mind.TransferTo(mindId, target); + control.KillCount = comp.KillCount; + control.KillCount++; + if (_playerManager.TryGetSessionById(mind.UserId, out var sessionscp) && control.KillCount == comp.KillsRequired) + { + var message = Loc.GetString("nothingthere-hammaggotson-haha"); + _chatM.ChatMessageToOne(ChatChannel.Server, message, Loc.GetString("chat-manager-server-wrap-message", ("message", message)), default, false, sessionscp.Channel); + } + _audio.PlayPvs(comp.EnterSound, target); + var transformA = Transform(args.User); + EnsurePausedMap(); + StopChaseMusic(entity.Owner, entity.Comp); + _transform.SetParent(args.User, transformA, PausedMap!.Value); + control.OriginalBody = args.User; + } + + private void OnControlledMobStateChanged(EntityUid uid, ImperialControlledNothingThereComponent comp, MobStateChangedEvent args) + { + if (!_mind.TryGetMind(uid, out var mindId, out var mind)) + return; + if (_mobStateSystem.IsDead(uid) || _mobStateSystem.IsCritical(uid)) + { + var transformB = Transform(uid); + var coords = transformB.Coordinates; + var newb = comp.OriginalBody; + var transformA = Transform(newb); + _transform.SetParent(newb, transformA, transformB.ParentUid); + _transform.SetCoordinates(newb, transformA, transformB.Coordinates, transformB.LocalRotation); + _mind.TransferTo(mindId, newb); + _audio.PlayPvs(comp.ExitSound, newb); + Gib(uid, comp.GibSound, comp.GibletLaunchImpulse, comp.GibletLaunchImpulseVariance, true); + if (TryComp(newb, out var nt)) + { + nt.KillCount = comp.KillCount; + StartChaseMusic(newb, nt); + } + } + } + public HashSet Gib(EntityUid ent, SoundSpecifier gibSound, float gibletLaunchImpulse, float gibletLaunchImpulseVariance, bool dropGiblets = true, EntityUid? user = null) + { + if (!_destructible.DestroyEntity(ent)) + return new(); + + _audio.PlayPvs(gibSound, ent); + + var gibbed = new HashSet(); + var beingGibbed = new BeingGibbedEvent(gibbed); + RaiseLocalEvent(ent, ref beingGibbed); + + if (dropGiblets) + { + foreach (var giblet in gibbed) + { + _transform.DropNextTo(giblet, ent); + FlingDroppedEntity(giblet, gibletLaunchImpulse, gibletLaunchImpulseVariance); + } + } + + var beforeDeletion = new GibbedBeforeDeletionEvent(gibbed); + RaiseLocalEvent(ent, ref beforeDeletion); + + return gibbed; + } + + private void FlingDroppedEntity(EntityUid target, float gibletLaunchImpulse, float gibletLaunchImpulseVariance) + { + var impulse = gibletLaunchImpulse + _random.NextFloat(gibletLaunchImpulseVariance); + var scatterVec = _random.NextAngle().ToVec() * impulse; + _physics.ApplyLinearImpulse(target, scatterVec); + } + private void OnGibBodyAction(Entity ent, ref ImperialNothingThereGibBodyEvent args) + { + if (args.Handled) + return; + if (!TryComp(args.Performer, out var mob)) + return; + args.Handled = true; + _mobStateSystem.ChangeMobState(args.Performer, MobState.Dead, mob, args.Performer); + } +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Egg.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Egg.cs new file mode 100644 index 00000000000..696c23a529d --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.Egg.cs @@ -0,0 +1,57 @@ +using Content.Shared.Imperial.SCP.NothingThere.Events; +using Content.Shared.Popups; +using Content.Shared.Mobs.Components; +using Content.Server.Imperial.SCP.NothingThere.Components; + +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem +{ + + private void InitializeEgg() + { + SubscribeLocalEvent(OnEggAction); + } + + private void OnEggAction(Entity ent, ref ImperialNothingThereEggEvent args) + { + if (args.Handled) + return; + if (!TryComp(args.Performer, out var mob)) + return; + if (!TryComp(args.Performer, out var comp)) + return; + if (!_mind.TryGetMind(args.Performer, out var mindId, out var mind)) + return; + args.Handled = true; + if (comp.KillCount < comp.KillsRequired) + { + _popupSystem.PopupEntity( + Loc.GetString("nothingthere-hammaggotson-morekills"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + return; + } + else + { + _polymorph.PolymorphEntity(args.Performer, comp.EggMorph); + } + } + + private void UpdateEgg() + { + var curTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entity, out var egg)) + { + if (curTime >= egg.EggTransformEnd && egg.Phase == NothingTherePhase.Egg) + { + if (!_mind.TryGetMind(entity, out var mindId, out var mind)) + continue; + var newb = _polymorph.PolymorphEntity(entity, egg.TrueMorph) ?? EntityUid.Invalid; + _audio.PlayPvs(egg.HatchSound, newb); + } + } + } +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.PausedMap.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.PausedMap.cs new file mode 100644 index 00000000000..e82705c292d --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.PausedMap.cs @@ -0,0 +1,31 @@ +using Content.Shared.GameTicking; + +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem +{ + public EntityUid? PausedMap { get; private set; } + + private void InitializeMap() + { + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnRoundRestart(RoundRestartCleanupEvent _) + { + if (PausedMap == null || !Exists(PausedMap)) + return; + + Del(PausedMap.Value); + } + private void EnsurePausedMap() + { + if (PausedMap != null && Exists(PausedMap)) + return; + + var mapUid = _map.CreateMap(); + _metaData.SetEntityName(mapUid, Loc.GetString("polymorph-paused-map-name")); + _map.SetPaused(mapUid, true); + PausedMap = mapUid; + } +} diff --git a/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.cs b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.cs new file mode 100644 index 00000000000..215f7080d83 --- /dev/null +++ b/Content.Server/Imperial/SCP/NothingThere/Systems/ImperialNothingThereSystem.cs @@ -0,0 +1,98 @@ +using Content.Shared.Actions; +using Robust.Shared.Audio.Systems; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.DoAfter; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Robust.Server.GameObjects; +using Content.Server.Mind; +using Content.Server.Imperial.SCP.NothingThere.Components; +using Robust.Shared.Timing; +using Content.Server.Polymorph.Systems; +using Content.Shared.Destructible; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Random; +using Robust.Server.Player; +using Content.Server.Chat.Managers; +using System.Linq.Expressions; +namespace Content.Server.Imperial.SCP.NothingThere.Systems; + +public sealed partial class ImperialNothingThereSystem : EntitySystem +{ + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly PolymorphSystem _polymorph = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedDestructibleSystem _destructible = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly IChatManager _chatM = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + public override void Initialize() + { + base.Initialize(); + InitializeMap(); + InitializeArsenal(); + InitializeBodyControl(); + InitializeChaseMusic(); + InitializeEgg(); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + UpdateItemProvide(); + UpdateEgg(); + } + private void OnInit(EntityUid uid, ImperialNothingThereComponent comp, MapInitEvent args) + { + StartChaseMusic(uid, comp); + switch (comp.Phase) + { + case NothingTherePhase.Original: + if (comp.EnterBodyEntity == null) + { + _actions.AddAction(uid, + ref comp.EnterBodyEntity, + comp.EnterBodyAction); + _actions.AddAction(uid, + ref comp.TransformEggEntity, + comp.TransformEggAction); + } + break; + case NothingTherePhase.Egg: + var curTime = _gameTiming.CurTime; + comp.EggTransformEnd = curTime + comp.EggTransformDuration; + break; + case NothingTherePhase.True: + _actions.AddAction(uid, + ref comp.EmpowerEntity, + comp.EmpowerAction); + _actions.AddAction(uid, + ref comp.ProjectileEntity, + comp.ProjectileAction); + var hands = EnsureComp(uid); + if (_hands.TryGetEmptyHand((uid, hands), out var emptyHand) && comp.NeedItems == true) + { + var hit = Spawn(comp.HitProto, Transform(uid).Coordinates); + if (!_hands.TryForcePickup(uid, hit, emptyHand, checkActionBlocker: false, handsComp: hands)) + { + QueueDel(hit); + return; + } + else + comp.NeedItems = false; + } + break; + } + } +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106BedComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106BedComponent.cs new file mode 100644 index 00000000000..a58ab604954 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106BedComponent.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Audio; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Damage; +using Robust.Shared.Prototypes; +using Content.Shared.DeviceLinking; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] + +public sealed partial class SCP106BedComponent : Component +{ + [DataField, ViewVariables] + public SoundSpecifier ContainmentSound = new SoundPathSpecifier("/Audio/Imperial/Seriozha/SCP/event/recontaime106.ogg"); + [DataField, ViewVariables] + public SoundSpecifier GlobalTeleportSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_teleported_global.ogg"); + [DataField, ViewVariables] + public ProtoId PrototypeScream = "Scream"; + [DataField(required: true), ViewVariables] + public DamageSpecifier Damage = new(); + [DataField("triggerPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string TriggerPort = "Trigger"; + public EntityUid Victim = EntityUid.Invalid; + [DataField] + public float StaminaDamage = 100f; + [ViewVariables] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan DelayDamage { get; set; } = TimeSpan.FromSeconds(3.7); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan DamageEnd = TimeSpan.Zero; + public bool Started = false; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan DelayContainment { get; set; } = TimeSpan.FromSeconds(31); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan ContainmentEnd = TimeSpan.Zero; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan Cooldown { get; set; } = TimeSpan.FromSeconds(300); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan CooldownEnd = TimeSpan.Zero; + +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106Component.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106Component.cs new file mode 100644 index 00000000000..c0ea2583f11 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106Component.cs @@ -0,0 +1,116 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Content.Shared.Polymorph; +using Content.Shared.Chat.Prototypes; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106Component : Component +{ + [DataField(customTypeSerializer: typeof(ResPathSerializer))] + public ResPath PocketMapPath = new("/Maps/Imperial/Maps/SCP/STARIC.yml"); + + [ViewVariables] + public MapId? PocketMapId; + + [DataField("puddleSpawnAction"), ViewVariables] + public EntProtoId PuddleSpawnAction = "ActionSCP106SpawnPuddle"; + + [DataField("puddleId"), ViewVariables] + public EntProtoId PuddleID = "PortalSCP106"; + + [DataField("puddleSpawnActionEntity")] + public EntityUid? PuddleSpawnActionEntity; + + [DataField("warpId")] + public EntProtoId WarpID = "PocketDimensionWarp"; + + public EntityUid? LastAttackedPerson; + + [DataField(required: true), ViewVariables] + public DamageSpecifier FirstHitDamage = new(); + + [DataField("teleportSound")] + public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_teleported_local.ogg"); + + [DataField("globalTeleportSound")] + public SoundSpecifier GlobalTeleportSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_teleported_global.ogg"); + + [DataField("puddleSpawnSound")] + public SoundSpecifier PuddleSpawnSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_puddle_spawn.ogg"); + [DataField("puddleExitSound")] + public SoundSpecifier ExitSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_puddle_exit.ogg"); + [ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan Delay = TimeSpan.FromSeconds(3.0f); + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("maxPuddles")] + public int MaxPuddles = 10; + + [ViewVariables] + public List Puddles = new(); + [ViewVariables] + public ProtoId GhostMorph = "SCP106GhostMorph"; + [ViewVariables] + public ProtoId PhysicalMorph = "SCP106PhysicalMorph"; + [ViewVariables] + public ProtoId PhysicalDMorph = "SCP106PhysicalDMorph"; + [DataField, ViewVariables] + public bool InDimension = false; + [DataField, ViewVariables] + public bool InPocketDimension = false; + [ViewVariables] + public EntityCoordinates? PastPosition; + [DataField(readOnly: true)] + public EntProtoId? RemoteEntityProto = "ImperialSCP106Ghost"; + [DataField(readOnly: true)] + public EntProtoId? OriginalEntityProto = "ImperialSCPDed"; + + [DataField] + public ProtoId PrototypeScream = "Scream"; + + [DataField("puddleEnterDimensionAction")] + public EntProtoId PuddleEnterDimensionAction = "ActionSCP106GoIntoDimension"; + [DataField("puddleEnterDimensionEntity")] + public EntityUid? PuddleEnterDimensionEntity; + [DataField("puddleExitDimensionAction")] + public EntProtoId PuddleExitDimensionAction = "ActionSCP106ExitDimension"; + [DataField("puddleExitDimensionEntity")] + public EntityUid? PuddleExitDimensionEntity; + [ViewVariables] + public MapId? PastMapId; + [DataField("laughSound")] + public SoundSpecifier LaughSound = new SoundPathSpecifier("/Audio/Imperial/Seriozha/SCP/106/Laugh.ogg"); + [DataField("minDistance")] + public float MinDistance = 10.0f; + + [DataField(required: true), ViewVariables] + public DamageSpecifier DamagePerSecond = new(); + [DataField("puddleExitDistance")] + public float PuddleExitDistance = 0.5f; + + [DataField("puddleEnterPuddleAction")] + public EntProtoId PuddleEnterPuddleAction = "ActionSCP106EnterPuddle"; + [DataField("puddleEnterPuddleEntity")] + public EntityUid? PuddleEnterPuddleEntity; + [DataField("puddleExitPuddleAction")] + public EntProtoId PuddleExitPuddleAction = "ActionSCP106ExitPuddle"; + [DataField("puddleExitPuddleEntity")] + public EntityUid? PuddleExitPuddleEntity; + [DataField("puddleDestroyPuddleAction")] + public EntProtoId PuddleDestroyPuddleAction = "ActionSCP106DestroyPuddle"; + [DataField("puddleDestroyPuddleEntity")] + public EntityUid? PuddleDestroyPuddleEntity; + public List RandomCoordinatesX = new List() {150.0f, 75.0f, 0.0f, -75.0f, -150.0f, -75.0f, 0.0f, 0.0f}; + public List RandomCoordinatesY = new List() {0.0f, 75.0f, 150.0f, 75.0f, 0.0f, -75.0f, -150.0f, -75.0f}; + [DataField] + public bool SleepOnAttack = false; + [DataField] + public bool CanPlaceOverLimit = true; +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106DimensionDebuffComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106DimensionDebuffComponent.cs new file mode 100644 index 00000000000..4d1a2ac08d8 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106DimensionDebuffComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Content.Shared.Damage; +using Robust.Shared.Map; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106DimensionDebuffComponent : Component +{ + [DataField(required: true), ViewVariables] + public DamageSpecifier DamagePerSecond = new(); + [ViewVariables] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextDamage = TimeSpan.Zero; + [ViewVariables] + public MapId? PastMapId; +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106PocketDimensionMarkerComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PocketDimensionMarkerComponent.cs new file mode 100644 index 00000000000..7f984673eb0 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PocketDimensionMarkerComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106PocketDimensionMarkerComponent : Component +{ +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleComponent.cs new file mode 100644 index 00000000000..05493d2ef75 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleComponent.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Map; +using Robust.Shared.Audio; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Damage; +using Robust.Shared.Prototypes; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106PuddleComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + public MapId? TargetMap; + [DataField("teleportSound")] + public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_teleported_local.ogg"); + [DataField("globalTeleportSound")] + public SoundSpecifier GlobalTeleportSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_teleported_global.ogg"); + [DataField] + public ProtoId PrototypeScream = "Scream"; + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier DamagePerSecond = new(); +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleImmunityComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleImmunityComponent.cs new file mode 100644 index 00000000000..1c70a1de137 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleImmunityComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106PuddleImmunityComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan ImmunityEnd = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleStunComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleStunComponent.cs new file mode 100644 index 00000000000..3c876544763 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106PuddleStunComponent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106PuddleStunComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan Delay { get; set; } = TimeSpan.FromSeconds(3); + [ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan StunEnd = TimeSpan.Zero; + [ViewVariables(VVAccess.ReadWrite)] + public EntityUid Puddle = EntityUid.Invalid; +} diff --git a/Content.Server/Imperial/SCP/SCP106/Components/SCP106SkullComponent.cs b/Content.Server/Imperial/SCP/SCP106/Components/SCP106SkullComponent.cs new file mode 100644 index 00000000000..1991cafe8c6 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Components/SCP106SkullComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +namespace Content.Server.Imperial.SCP.SCP106.Components; + +[RegisterComponent] +public sealed partial class SCP106SkullComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(3); + [DataField("puddleExitSound")] + public SoundSpecifier ExitSound = new SoundPathSpecifier("/Audio/Imperial/SCP/scp106_puddle_exit.ogg"); +} diff --git a/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Bed.cs b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Bed.cs new file mode 100644 index 00000000000..28424f9aaa9 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Bed.cs @@ -0,0 +1,138 @@ +using System.Linq; +using Content.Server.Imperial.SCP.SCP106.Components; +using Content.Shared.Buckle.Components; +using Content.Shared.DeviceLinking; +using Content.Shared.DeviceLinking.Events; +using Content.Shared.DeviceNetwork; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Content.Shared.Damage.Components; +using Content.Shared.Interaction.Components; +using Content.Shared.Movement.Pulling.Components; +using System.Numerics; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using System.Threading.Tasks.Dataflow; +using Content.Shared.Popups; +using Robust.Shared.Network; +using Robust.Shared.Random; +using Robust.Shared.Serialization; + +namespace Content.Server.Imperial.SCP.SCP106.Systems; + +public sealed partial class SCP106System : EntitySystem +{ + private void InitializeBed() + { + SubscribeLocalEvent(OnBedInit); + SubscribeLocalEvent(OnSignalReceived); + } + private void OnBedInit(EntityUid uid, SCP106BedComponent component, ComponentStartup args) + { + + _signalSystem.EnsureSinkPorts(uid, component.TriggerPort); + } + + private void OnSignalReceived(EntityUid uid, SCP106BedComponent component, ref SignalReceivedEvent args) + { + if (!TryComp(uid, out var strap)) + return; + + var state = SignalState.Momentary; + args.Data?.TryGetValue(DeviceNetworkConstants.LogicState, out state); + + + if (args.Port == component.TriggerPort) + { + if (state == SignalState.High || state == SignalState.Momentary) + { + ActivateBed(uid); + } + } + } + + private void ActivateBed(EntityUid uid) + { + var transformTable = Transform(uid); + if (!TryComp(uid, out var comp)) + return; + if (!TryComp(uid, out var strapComponent)) + return; + if (comp.Started) + return; + var victims = strapComponent.BuckledEntities; + if (victims.Count == 0) + return; + var victim = victims.FirstOrDefault(); + EnsureComp(victim); + RemComp(victim); + //_buckleSystem.TryUnbuckle(victim, victim, popup: false); + _audio.PlayGlobal(comp.ContainmentSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f)); + var curTime = _gameTiming.CurTime; + comp.Victim = victim; + comp.DamageEnd = curTime + comp.DelayDamage; + comp.ContainmentEnd = curTime + comp.DelayContainment; + comp.Started = true; + //_chat.DispatchFilteredAnnouncement(Filter.Broadcast(), ) + } + + private void UpdateBed() + { + var curTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.CooldownEnd != TimeSpan.Zero) + { + if (comp.CooldownEnd < curTime) + continue; + else + comp.CooldownEnd = TimeSpan.Zero; + } + if (comp.Started == false) + continue; + if (comp.DamageEnd <= curTime && comp.DamageEnd != TimeSpan.Zero) + { + if (!HasComp(uid)) + continue; + _damageable.TryChangeDamage(comp.Victim, comp.Damage); + _chat.TryEmoteWithChat(comp.Victim, comp.PrototypeScream); + comp.DamageEnd = TimeSpan.Zero; + } + if (comp.ContainmentEnd <= curTime && comp.ContainmentEnd != TimeSpan.Zero) + { + RemoveAllPuddles(); + comp.Started = false; + comp.CooldownEnd = curTime + comp.Cooldown; + var puddle = EntityUid.Invalid; + var xquery = EntityQueryEnumerator(); + var transformTable = Transform(uid); + if (transformTable == null) + { + continue; + } + while (xquery.MoveNext(out var entity, out var scp, out var transform, out var metadata)) + { + var map = transformTable.MapID; + if (!_mapSystem.TryGetMap(map, out var mapEnt)) + { + continue; + } + _transform.SetCoordinates((entity, transform, metadata), transformTable.Coordinates.Offset(new Vector2(0, -1))); + _transform.SetParent(entity, transform, mapEnt.Value); + var newb = TransformInto(entity, scp.GhostMorph); + var newscp = EnsureComp(newb); + newscp.InDimension = true; + newscp.InPocketDimension = false; + _actions.RemoveAction(newb, newscp.PuddleEnterDimensionEntity); + if (puddle == EntityUid.Invalid) + { + puddle = Spawn(scp.PuddleID, transformTable.Coordinates.Offset(new Vector2(0, -1))); + var compp = EnsureComp(puddle); + compp.TargetMap = scp.PocketMapId; + } + scp.Puddles.Add(puddle); + } + } + } + } +} diff --git a/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Puddle.cs b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Puddle.cs new file mode 100644 index 00000000000..a881b9e6b52 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Puddle.cs @@ -0,0 +1,313 @@ +using Content.Server.Imperial.SCP.SCP106.Components; +using Content.Shared.Imperial.SCP.SCP106.Events; +using Robust.Shared.Map; +using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Mobs.Components; +using Robust.Shared.Audio; +using Content.Shared.DoAfter; +using Content.Shared.Bed.Sleep; +using Content.Shared.Popups; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Interaction.Components; +namespace Content.Server.Imperial.SCP.SCP106.Systems; + +public sealed partial class SCP106System +{ + #region Puddle Startup + + private void InitializePuddle() + { + SubscribeLocalEvent(OnPuddleCollide); + SubscribeLocalEvent(OnPuddleInit); + } + private void OnPuddleInit(EntityUid uid, SCP106PuddleComponent component, ComponentStartup args) + { + //Whener a puddle is spawned (e.g. via admin panel of some sort), it automatically is configured to teleport to the dimension, if one exists. Otherwise, it does nothing + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entity, out var comp)) + { + component.TargetMap = Transform(entity).MapID; + break; // we only need one of them so yeah, i am extremely sorry for this + } + foreach (var scp in EntityQuery()) + { + if (!scp.Puddles.Contains(uid)) + scp.Puddles.Add(uid); + } + } + + #endregion + + #region Colliding + + private void OnPuddleCollide(EntityUid uid, SCP106PuddleComponent component, ref StartCollideEvent args) + { + + var subject = args.OtherEntity; + + if (TryComp(subject, out var scpcomp)) + return; + if (Transform(subject).Anchored) + return; + if (TryComp(subject, out var imm)) + return; + if (TryComp(subject, out var pullable) && pullable.BeingPulled) + { + _pulling.TryStopPull(subject, pullable); + } + if (TryComp(subject, out var pullerComp) + && TryComp(pullerComp.Pulling, out var subjectPulling)) + { + _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling); + } + if (TryComp(subject, out var idksterling)) + return; + if (TryComp(subject, out var mobstate)) + { + if (component.TargetMap == MapId.Nullspace) + return; + + TryPuddleStun(subject, uid, component.PrototypeScream, component.TeleportSound); + } + } + + #endregion + + #region Puddle stun handling + private void TryPuddleStun(EntityUid subject, EntityUid uid, ProtoId emote, SoundSpecifier sound) + { + //It stuns, then adds a component, that after a certain duration teleports owner of the stun to the dimension (also putting to sleep) + _stun.TryAddStunDuration(subject, TimeSpan.FromSeconds(4)); + _chat.TryEmoteWithChat(subject, emote, ignoreActionBlocker: true); + var stunc = EnsureComp(subject); + _blindableSystem.UpdateIsBlind(subject); + var curTime = _gameTiming.CurTime; + var endTime = curTime + stunc.Delay; + stunc.StunEnd = endTime; + stunc.Puddle = uid; + EnsureComp(subject); + _godmode.ToggleGodmode(subject); //I was asked to do it, so the person won't get killed why he gets sucked to doom!! + _audio.PlayPredicted(sound, subject, uid); + } + + private void UpdatePuddleStun() + { + var curTime = _gameTiming.CurTime; + var duration = TimeSpan.FromSeconds(5); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var stun)) + { + if (curTime < stun.StunEnd) + continue; + if (!TryComp(stun.Puddle, out var puddl) || puddl.TargetMap == null) + { + RemComp(uid); + RemComp(uid); + _godmode.ToggleGodmode(uid); + _blindableSystem.UpdateIsBlind(uid); + _statusEffects.TryAddStatusEffectDuration(uid, SleepingSystem.StatusEffectForcedSleeping, duration); + continue; + } + TeleportEntity(stun.Puddle, uid, puddl.TargetMap ?? MapId.Nullspace, puddl.GlobalTeleportSound, puddl.DamagePerSecond); + _blindableSystem.UpdateIsBlind(uid); + RemComp(uid); + RemComp(uid); + _godmode.ToggleGodmode(uid); + _statusEffects.TryAddStatusEffectDuration(uid, SleepingSystem.StatusEffectForcedSleeping, duration); + } + } + #endregion + #region Puddle Placement + private void OnPuddleAction(SCP106SpawnPuddleActionEvent args) + { + //This action spawns a puddle underneath you, if it passed some checks + if (args.Handled || !TryComp(args.Performer, out var scp)) + return; + var coords = Transform(args.Performer).Coordinates; + if (IsTooCloseToPuddles(scp, coords, scp.MinDistance) != EntityUid.Invalid) //It can't be too close to other puddles for balancing issues + { + _popupSystem.PopupEntity( + Loc.GetString("scp106-hammaggotson-tooclose"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + args.Handled = true; + return; + } + if (IsOnFloor(coords) == false || NoWallsOrWindowsUnderneath(coords) == false || IsTooCloseToWeakling(coords, args.Performer)) // It can't be in space, it can't be on walls/windows/etc (but it can be on arcade machines, etc!), and it can't be right under a human! + { + _popupSystem.PopupEntity( + Loc.GetString("scp106-hammaggotson-dumbaz"), + args.Performer, + args.Performer, + PopupType.MediumCaution); + args.Handled = true; + return; + } + var doAfterArgs = new DoAfterArgs(EntityManager, + args.Performer, + scp.Delay, + new SCP106DoAfterPuddleEvent(), + eventTarget: args.Performer) + { + BreakOnMove = true, + BreakOnDamage = true, + MovementThreshold = 0.01f, + DistanceThreshold = 1.0f, + NeedHand = false, + }; + _doAfter.TryStartDoAfter(doAfterArgs); //We don't want SCP 106 to place puddles right away! + args.Handled = true; + } + + private void OnPuddleDoAfter(Entity entity, ref SCP106DoAfterPuddleEvent args) + { + if (args.Cancelled || args.Handled || entity.Comp.Deleted) + return; + if (!TryComp(args.User, out var scp)) + return; + if (scp.Puddles.Count >= scp.MaxPuddles) + { + if (scp.CanPlaceOverLimit == false) + { + return; + } + } + var coords = Transform(args.User).Coordinates; + if (IsTooCloseToPuddles(scp, coords, scp.MinDistance) != EntityUid.Invalid) //WE check again in case of some extra forces spawning a puddle + { + _popupSystem.PopupEntity( + Loc.GetString("scp106-hammaggotson-tooclose"), + args.User, + args.User, + PopupType.MediumCaution); + args.Handled = true; + return; + } + var puddle = Spawn(scp.PuddleID, coords); + if (TryComp(puddle, out var puddleComp)) + puddleComp.TargetMap = scp.PocketMapId; //We configure the puddle to actually teleport when colliding with it + scp.Puddles.Add(puddle); + //We have a maximum amount of puddles, so if we go over it, then we basically delete one. + if (scp.Puddles.Count > scp.MaxPuddles && scp.Puddles.Count > 0) + { + var oldestPuddle = scp.Puddles[0]; + if (Exists(oldestPuddle)) + { + scp.Puddles.RemoveAt(0); + QueueDel(oldestPuddle); + } + } + _audio.PlayPredicted(scp.PuddleSpawnSound, args.User, puddle, AudioParams.Default.WithVolume(2f)); + } + + private void OnDestroyPuddleAction(SCP106DestroyPuddleActionEvent args) + { + //We destroy the puddle right beneath us! + var user = args.Performer; + if (args.Handled || !TryComp(user, out var scp)) + return; + var coords = Transform(user).Coordinates; + var target = IsTooCloseToPuddles(scp, coords, scp.PuddleExitDistance); //1 tile away by default + if (target == EntityUid.Invalid) + { + _popupSystem.PopupEntity( + Loc.GetString("scp106-hammaggotson-pefar"), + user, + user, + PopupType.MediumCaution); + return; + } + if (scp.Puddles.Contains(target)) //we as well delete it from the list so things won't happen! + { + scp.Puddles.Remove(target); + QueueDel(target); + } + args.Handled = true; + } + + public void RemoveAllPuddles() + { + foreach (var scp in EntityQuery()) + { + foreach (var puddle in scp.Puddles) + { + if (Exists(puddle)) + { + QueueDel(puddle); + } + } + scp.Puddles.Clear(); + } + } + #endregion + + #region Puddle Transmission + private void OnTransmissionPuddleAction(SCP106TransmissionPuddleActionEvent args) + { + //When we press this near a puddle we begin a do after + //After it finishes, if we were in physical form, we go into "ghost" form (non-physical), where we fly as an AI and can interact with puddles and stuff + //Otherwise if we were in that ghost form we exit it and enter our physical body again + if (args.Handled || !TryComp(args.Performer, out var scp)) + return; + var coords = Transform(args.Performer).Coordinates; + var user = args.Performer; + var target = IsTooCloseToPuddles(scp, coords, scp.PuddleExitDistance); + PuddleTransmission(target, args.Performer, scp); + } + + private void PuddleTransmission(EntityUid target, EntityUid user, SCP106Component scp) + { + if (target == EntityUid.Invalid) + { + _popupSystem.PopupEntity( + Loc.GetString("scp106-hammaggotson-pefar"), + user, + user, + PopupType.MediumCaution); + return; + } + var doAfterArgs = new DoAfterArgs(EntityManager, + user, + scp.Delay, + new SCP106DoAfterGhostPuddleEvent(), + eventTarget: user) + { + BreakOnMove = true, + BreakOnDamage = true, + MovementThreshold = 0.01f, + DistanceThreshold = 1.0f, + NeedHand = false, + }; + _doAfter.TryStartDoAfter(doAfterArgs); + if (scp.InDimension == true) + { + _popupSystem.PopupEntity(Loc.GetString("scp106-hammaggotson-exit"), target, PopupType.SmallCaution); + } + } + + private void OnGhostPuddleDoAfter(Entity entity, ref SCP106DoAfterGhostPuddleEvent args) + { + if (args.Cancelled || args.Handled || entity.Comp.Deleted) + return; + if (!TryComp(args.User, out var scp)) + return; + if (scp.InDimension == false) + { + var newb = TransformInto(args.User, scp.GhostMorph); + var newscp = EnsureComp(newb); + newscp.InDimension = true; + _actions.RemoveAction(newb, newscp.PuddleEnterPuddleEntity); //IN CASE IT GETS ADDED + } + else + { + var newb = TransformInto(args.User, scp.PhysicalMorph); + var newscp = EnsureComp(newb); + newscp.InDimension = false; + _audio.PlayPvs(newscp.ExitSound, newb); + } + } + #endregion +} diff --git a/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.SCP106.cs b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.SCP106.cs new file mode 100644 index 00000000000..bdcf9a8dbbb --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.SCP106.cs @@ -0,0 +1,271 @@ +using System.Linq; +using Content.Server.Imperial.SCP.SCP106.Components; +using Content.Shared.Imperial.SCP.SCP106.Events; +using Robust.Shared.Map; +using System.Numerics; +using Robust.Shared.EntitySerialization; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Mobs.Components; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Audio; +using Content.Shared.Bed.Sleep; +using Content.Shared.Body.Components; +using Robust.Shared.Random; +using Content.Server.GameTicking; +using Content.Shared.Eye; +using Content.Shared.Body; +namespace Content.Server.Imperial.SCP.SCP106.Systems; + +public sealed partial class SCP106System +{ + #region Component Startup + + private void InitializeSCP106() + { + SubscribeLocalEvent(OnAttack); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnPuddleAction); + SubscribeLocalEvent(OnPuddleDoAfter); + SubscribeLocalEvent(OnGhostPuddleDoAfter); + SubscribeLocalEvent(_ => MakeVisible(true)); + SubscribeLocalEvent(OnSCP106GetVis); + } + private void OnInit(EntityUid uid, SCP106Component component, ComponentStartup args) + { + //Adding all already existing puddles, in case of SCP 106 being respawned if he dissapears for some reason, so he would keep all his puddles! How convenient! + if (!component.Puddles.Any()) + { + component.Puddles = new(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uiddd, out var puddll)) + { + component.Puddles.Add(uid); + } + } + + //This code chunk is designated to loading up the map and initing it and stuff... + if (component.PocketMapPath is { } path && component.PocketMapId == null) + { + var opts = DeserializationOptions.Default with { InitializeMaps = true }; //We want the map inited yeah + //I know how bad this is, but what we are doing in this chunk of code is running through ALL markers, and if there is one, then we can find the map of dimension via it! + var query = EntityQueryEnumerator(); + var markerEntity = EntityUid.Invalid; + SCP106PocketDimensionMarkerComponent? dimensionMarker = null; + while (query.MoveNext(out var entity, out var comp)) + { + markerEntity = entity; + dimensionMarker = comp; + break; // we only need one of them so yeah, i am extremely sorry for this + } + var spwarp = true; + if (dimensionMarker != null) //If there is a dimension marker, we just grab the map ID of where it is and we are fine! + { + spwarp = false; + var trs = Transform(markerEntity); + component.PocketMapId = trs.MapID; + } + else if (_mapLoader.TryLoadMap(path, out var map, out var gridSet, opts)) //Otherwise we just load it + { + component.PocketMapId = map.Value.Comp.MapId; + } + else + { + Log.Error($"Failed to load SCP 106 dimension map from {path}!"); + return; + } + if (_mapSystem.TryGetMap(component.PocketMapId, out var mapEnt) & spwarp) //We are placing a new dimension marker if we loaded a new map, so we can use it in future! + { + var mapEntNN = mapEnt ?? EntityUid.Invalid; + var warp = Spawn(component.WarpID); + var transform = _entityManager.GetComponent(warp); + _transform.SetWorldPosition((warp, transform), new Vector2(0, 0)); + _transform.SetParent(warp, transform, mapEntNN); + } + } + + //Loading up the actions + if (component.PuddleSpawnActionEntity == null) + { + _actions.AddAction(uid, + ref component.PuddleSpawnActionEntity, + component.PuddleSpawnAction); + } + if (component.InPocketDimension == false) + { + if (component.InDimension == false) + { + _actions.AddAction(uid, + ref component.PuddleEnterPuddleEntity, + component.PuddleEnterPuddleAction); + } + else + { + if (component.PuddleExitPuddleEntity == null) + { + _actions.AddAction(uid, + ref component.PuddleExitPuddleEntity, + component.PuddleExitPuddleAction); + } + if (component.PuddleEnterDimensionEntity == null) + { + _actions.AddAction(uid, + ref component.PuddleEnterDimensionEntity, + component.PuddleEnterDimensionAction); + } + } + if (component.PuddleDestroyPuddleEntity == null) + { + _actions.AddAction(uid, + ref component.PuddleDestroyPuddleEntity, + component.PuddleDestroyPuddleAction); + } + } + else + { + if (component.PuddleExitDimensionEntity == null) + { + _actions.AddAction(uid, + ref component.PuddleExitDimensionEntity, + component.PuddleExitDimensionAction); + } + } + } + #endregion + #region Attack handling + private void OnAttack(EntityUid entity, SCP106Component comp, ref MeleeHitEvent args) + { + //On attack if we hit the person only the first time, we deal additional damage to him. If we hit him second time in a row, we teleport him to the dimension (BUT IF WE AREN'T IN DIMENSION) + //And then if we hit him in the dimension, we teleport him to one of the coords + if (args.HitEntities is []) + return; + if (!TryComp(args.HitEntities[0], out var mobstateCom)) + { + return; + } + if (comp.InPocketDimension == false) + { + if (comp.LastAttackedPerson == args.HitEntities[0]) + { + comp.LastAttackedPerson = EntityUid.Invalid; + if (TryComp(args.HitEntities[0], out var pullerComp) + && TryComp(pullerComp.Pulling, out var subjectPulling)) + { + _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling); + } + var duration = TimeSpan.FromSeconds(5); + _audio.PlayPredicted(comp.TeleportSound, entity, args.HitEntities[0]); + TeleportEntity(entity, args.HitEntities[0], comp.PocketMapId ?? MapId.Nullspace, comp.GlobalTeleportSound, comp.DamagePerSecond); + _audio.PlayPvs(comp.LaughSound, entity, AudioParams.Default.WithVolume(2f)); + _statusEffects.TryAddStatusEffectDuration(args.HitEntities[0], SleepingSystem.StatusEffectForcedSleeping, duration); + } + else + { + comp.LastAttackedPerson = args.HitEntities[0]; + _damageable.TryChangeDamage(args.HitEntities[0], comp.FirstHitDamage); + } + } + else + { + if (TryComp(args.HitEntities[0], out var body)) + { + _chat.TryEmoteWithChat(args.HitEntities[0], comp.PrototypeScream, ignoreActionBlocker: true); //SCREAM!! + if (comp.SleepOnAttack == true) + { + _statusEffects.TryAddStatusEffectDuration(args.HitEntities[0], SleepingSystem.StatusEffectForcedSleeping, TimeSpan.FromSeconds(90.0f)); //This was debug, don't mind it + } + else + { + _statusEffects.TryAddStatusEffectDuration(args.HitEntities[0], SleepingSystem.StatusEffectForcedSleeping, TimeSpan.FromSeconds(5.0f)); + var transform = _entityManager.GetComponent(args.HitEntities[0]); + var randn = _random.Pick(new List() { 0, 1, 2, 3, 4, 5, 6, 7 }); //I am sorry + _transform.SetWorldPosition((args.HitEntities[0], transform), new Vector2(comp.RandomCoordinatesX[randn], comp.RandomCoordinatesY[randn])); //I am very sorry + } + } + } + } + #endregion + + #region Dimension Transmission + private void OnSwitchDimensionAction(SCP106DimensionSwitchActionEvent args) + { + if (args.Handled || !TryComp(args.Performer, out var scp)) + return; + SwitchDimensions(args.Performer); + if (scp.InPocketDimension == true) + { + var newb = TransformInto(args.Performer, scp.PhysicalDMorph); + var newscp = EnsureComp(newb); + _actions.RemoveAction(newb, newscp.PuddleSpawnActionEntity); + _actions.RemoveAction(newb, newscp.PuddleEnterPuddleEntity); + _actions.RemoveAction(newb, newscp.PuddleDestroyPuddleEntity); + } + else + { + var newb = TransformInto(args.Performer, scp.GhostMorph); + } + args.Handled = true; + } + private void SwitchDimensions(EntityUid user) + { + if (!TryComp(user, out var scp)) + return; + if (!_mind.TryGetMind(user, out var mindId, out var mind)) + return; + var coords = Transform(user).Coordinates; + scp.InPocketDimension = !scp.InPocketDimension; + if (scp.InPocketDimension == false) + { + var oldcords = scp.PastPosition ?? EntityCoordinates.Invalid; + var oldmap = scp.PastMapId; + scp.InDimension = true; + scp.PastPosition = coords; + + var transform = Transform(user); + if (_mapSystem.TryGetMap(oldmap, out var mapEnt)) + { + _transform.SetCoordinates(user, oldcords); + _transform.SetParent(user, transform, mapEnt.Value); + } + } + else + { + scp.PastPosition = coords; + var transform = Transform(user); + + scp.PastMapId = transform.MapID; + + if (_mapSystem.TryGetMap(scp.PocketMapId, out var mapEnt)) + { + _transform.SetWorldPosition((user, transform), new Vector2(0, 0)); + _transform.SetParent(user, transform, mapEnt ?? EntityUid.Invalid); + } + } + } + #endregion + + #region Vision + private void OnSCP106GetVis(Entity ent, ref GetVisMaskEvent args) + { + args.VisibilityMask |= (int)VisibilityFlags.Ghost; + } + + public void MakeVisible(bool visible) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var vis)) + { + if (visible) + { + _visibility.AddLayer((uid, vis), (int)VisibilityFlags.Normal, false); + _visibility.RemoveLayer((uid, vis), (int)VisibilityFlags.Ghost, false); + } + else + { + _visibility.AddLayer((uid, vis), (int)VisibilityFlags.Ghost, false); + _visibility.RemoveLayer((uid, vis), (int)VisibilityFlags.Normal, false); + } + _visibility.RefreshVisibility(uid, vis); + } + } + #endregion +} diff --git a/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Skull.cs b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Skull.cs new file mode 100644 index 00000000000..6efb5f801e3 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.Skull.cs @@ -0,0 +1,110 @@ +using Content.Server.Imperial.SCP.SCP106.Components; +using Robust.Shared.Physics.Events; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Mobs.Components; +using Content.Shared.Bed.Sleep; +using Robust.Shared.Random; +using System.Numerics; +using Content.Shared.Bed.Cryostorage; + +namespace Content.Server.Imperial.SCP.SCP106.Systems; + +public sealed partial class SCP106System +{ + private void InitializeSkull() + { + SubscribeLocalEvent(OnSkullCollide); + } + #region Collision handling + private void OnSkullCollide(EntityUid uid, SCP106SkullComponent component, ref StartCollideEvent args) + { + //when we touch a skull, we get teleported to a random puddle, and we are granted puddle immunity + var subject = args.OtherEntity; + + if (TryComp(subject, out var scpcomp)) + return; + if (Transform(subject).Anchored) + return; + if (TryComp(subject, out var pullable) && pullable.BeingPulled) + { + _pulling.TryStopPull(subject, pullable); + } + if (TryComp(subject, out var pullerComp) + && TryComp(pullerComp.Pulling, out var subjectPulling)) + { + _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling); + } + if (TryComp(subject, out var mobstate)) + { + if (!TryComp(subject, out var ddb)) + return; + var allPuddls = new HashSet(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uidd, out var pddl)) + { + allPuddls.Add(uidd); + } + var transform = _entityManager.GetComponent(subject); + if (allPuddls.Count == 0) + { + //_transform.SetWorldPosition((subject, transform), new Vector2(0, 0)); + var qquery = EntityQueryEnumerator(); + while (qquery.MoveNext(out var uiddd, out var cryopod)) + { + if (Transform(uiddd).MapID == ddb.PastMapId) + allPuddls.Add(uiddd); + } + //if (TryComp(subject, out var ddb) && _mapSystem.TryGetMap(ddb.PastMapId, out var mapEnt1)) + // { + // _transform.SetParent(subject, transform, mapEnt1.Value); + // RemComp(subject); + // return; + // } + } + if (allPuddls.Count == 0) + { + _transform.SetWorldPosition((subject, transform), new Vector2(0, 0)); + if(_mapSystem.TryGetMap(ddb.PastMapId, out var mapEnt1)) + { + _transform.SetParent(subject, transform, mapEnt1.Value); + RemComp(subject); + return; + } + } + var puddl = _random.Pick(allPuddls); + var ourCoords = Transform(puddl).Coordinates; + + var transformPuddl = _entityManager.GetComponent(puddl); + if (_mapSystem.TryGetMap(transformPuddl.MapID, out var mapEnt)) + { + var imm = EnsureComp(subject); + var curTime = _gameTiming.CurTime; + var endTime = curTime + component.Duration; + imm.ImmunityEnd = endTime; + _transform.SetCoordinates(subject, ourCoords); + _transform.SetParent(subject, transform, mapEnt.Value); + RemComp(subject); + _statusEffects.TryRemoveStatusEffect(subject, SleepingSystem.StatusEffectForcedSleeping); //IF HE IS ASLEEP FOR SOME REASON + if (TryComp(subject, out var sleeep)) + { + _sleep.TryWaking(new Entity(subject, sleeep), true); + } + _audio.PlayPvs(component.ExitSound, puddl); + } + } + } + #endregion + #region Puddle Immunity Update + private void UpdatePuddleImmunity() + { + var curTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entity, out var imm)) + { + if (curTime < imm.ImmunityEnd) + continue; + RemComp(entity); + } + } + #endregion +} diff --git a/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.cs b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.cs new file mode 100644 index 00000000000..6f6bf5de096 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP106/Systems/SCP106System.cs @@ -0,0 +1,301 @@ +using Content.Shared.Actions; +using Content.Server.Imperial.SCP.SCP106.Components; +using Content.Shared.Imperial.SCP.SCP106.Events; +using Robust.Shared.Map; +using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; +using Robust.Shared.Player; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Systems; +using System.Numerics; +using Robust.Shared.EntitySerialization.Systems; +using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Mobs.Components; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Server.Player; +using Content.Shared.DoAfter; +using Content.Shared.Stunnable; +using Content.Shared.Eye.Blinding.Systems; +using Robust.Shared.Timing; +using Content.Shared.Bed.Sleep; +using Content.Server.Chat.Systems; +using Content.Server.Polymorph.Systems; +using Content.Shared.Polymorph; +using Content.Server.Mind; +using Content.Server.Popups; +using Content.Shared.Tag; +using Robust.Shared.Random; +using Content.Shared.Chat; +using Content.Server.Chat.Managers; +using Content.Shared.StatusEffectNew; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage.Components; +using Content.Server.DeviceLinking.Systems; +using Content.Server.Buckle.Systems; +using Content.Shared.DeviceLinking.Events; +using Content.Shared.Interaction.Components; +using Content.Shared.Movement.Pulling.Components; +using Robust.Server.GameObjects; +namespace Content.Server.Imperial.SCP.SCP106.Systems; + +public sealed partial class SCP106System : EntitySystem +{ + #region Dependencies + [Dependency] private readonly DeviceLinkSystem _signalSystem = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IChatManager _chatM = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MapLoaderSystem _mapLoader = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly PullingSystem _pulling = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly BlindableSystem _blindableSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly PolymorphSystem _polymorph = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedGodmodeSystem _godmode = default!; + [Dependency] private readonly SleepingSystem _sleep = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly VisibilitySystem _visibility = default!; + #endregion + + #region Init and Stuff + private readonly List _blacklistedTags = ["Wall", "Window"]; + public override void Initialize() + { + base.Initialize(); + InitializeSCP106(); + InitializePuddle(); + InitializeSkull(); + InitializeBed(); + + SubscribeLocalEvent(OnTransmissionPuddleAction); + SubscribeLocalEvent(OnDestroyPuddleAction); + SubscribeLocalEvent(OnSwitchDimensionAction); + } + #endregion + + #region Update + public override void Update(float frameTime) + { + base.Update(frameTime); + + UpdatePuddleStun(); + UpdatePuddleImmunity(); + UpdateDimensionDebuff(); + UpdateBed(); + } + #endregion + + #region Position checks + private bool IsTooCloseToWeakling(EntityCoordinates coords, EntityUid scp) + { + //It looks up nearby mobs, and if there is one in distance (default: 1 tile) it returns true, otherwise false + var mapCoords = _transform.ToMapCoordinates(coords); + var radius = 1f; + foreach (var mob in _lookup.GetEntitiesInRange(coords, radius, LookupFlags.Dynamic)) + { + if (!TryComp(mob, out var mobd)) + continue; + if (mob == scp) + continue; + if (mobd.CurrentState == Shared.Mobs.MobState.Dead || + mobd.CurrentState == Shared.Mobs.MobState.Critical) + continue; + + var mobTransform = Transform(mob); + + if (mobTransform.MapID != mapCoords.MapId) + continue; + var mobPos = _transform.GetWorldPosition(mobTransform); + var distance = (mapCoords.Position - mobPos).Length(); + if (distance <= radius) + { + return true; + } + } + return false; + } + private EntityUid IsTooCloseToPuddles(SCP106Component scp, EntityCoordinates targetCoords, float dis) + { + //It goes through all puddles, calculates distance between them and if one is too close it returns THE PUDDLE UID, otherwise invalid entityuid because i feel like its right (default: 15 tiles) + var mindis = dis; + var mindisSquared = mindis * mindis; + var targetMap = _transform.GetMapId(targetCoords); + var targetPos = _transform.ToMapCoordinates(targetCoords).Position; + + foreach (var puddle in scp.Puddles) + { + if (!Exists(puddle)) + continue; + + var puddleTransform = Transform(puddle); + + if (puddleTransform.MapID != targetMap) + continue; + + var puddlePos = _transform.GetWorldPosition(puddle); + var distanceSquared = (targetPos - puddlePos).LengthSquared(); + + if (distanceSquared < mindisSquared) + { + return puddle; + } + } + + return EntityUid.Invalid; + } + + private bool IsOnFloor(EntityCoordinates coords) + { + //Returns true if there is a floor underneath us, otherwise FALSE (because we don't want puddles to be in space!!!) + var mapCoords = _transform.ToMapCoordinates(coords); + var gridUid = _transform.GetGrid(coords) ?? EntityUid.Invalid; + if (gridUid == EntityUid.Invalid) + { + return false; + } + var grid = Comp(gridUid); + var gridEnt = new Entity(gridUid, grid); + var tile = _mapSystem.GetTileRef(gridEnt, coords); + if (tile.Tile.IsEmpty) + { + return false; + } + return true; + } + + private bool NoWallsOrWindowsUnderneath(EntityCoordinates coords) + { + //So i found out walls and windows have tags, named after them, sooo what basically this method does is + //It checks entities anchored to the tile underneath us, if there is an entity with a Wall or Window tag it returns false + var mapCoords = _transform.ToMapCoordinates(coords); + var gridUid = _transform.GetGrid(coords) ?? EntityUid.Invalid; + if (gridUid == EntityUid.Invalid) + { + return true; + } + var grid = Comp(gridUid); + var gridEnt = new Entity(gridUid, Comp(gridUid)); + var anchovies = _mapSystem.GetAnchoredEntities(gridUid, grid, mapCoords); + bool hasSolidWall = false; + + foreach (var anchovy in anchovies) + { + foreach (string tag in _blacklistedTags) + { + if (_tag.HasTag(anchovy, tag)) + { + hasSolidWall = true; + } + } + } + return !hasSolidWall; + } + #endregion + #region Send To Dimension Handle + private void TeleportEntity(EntityUid hole, EntityUid subject, MapId map, SoundSpecifier globaltpsound, DamageSpecifier damage) + { + //We banish a person to the realm, put him to sleep (ig?) and apply a debuff (thats just like bleeding) + var transform = Transform(subject); + if (_mapSystem.TryGetMap(map, out var mapEnt)) + { + if (_mind.TryGetMind(subject, out _, out var mindComponent)) + { + if (_playerManager.TryGetSessionById(mindComponent.UserId, out var session)) + { + _chatM.ChatMessageToOne(ChatChannel.Server, Loc.GetString("scp106-hammaggotson-urdamned"), Loc.GetString("chat-manager-server-wrap-message", ("message", Loc.GetString("scp106-hammaggotson-urdamned"))), default, false, session.Channel); + } + } + var query = EntityQueryEnumerator(); + var uid = EntityUid.Invalid; + SCP106Component? scp = null; + while (query.MoveNext(out var entity, out var comp)) + { + uid = entity; + scp = comp; + break; // we only need one of them so yeah, i am extremely sorry for this + } + if (scp != null) + { + if (_mind.TryGetMind(uid, out _, out var mindComponentscp)) + { + if (_playerManager.TryGetSessionById(mindComponentscp.UserId, out var sessionscp)) + { + var message = Loc.GetString("scp106-hammaggotson-wildhunt", ("name", Comp(subject).EntityName)); + _chatM.ChatMessageToOne(ChatChannel.Server, message, Loc.GetString("chat-manager-server-wrap-message", ("message", message)), default, false, sessionscp.Channel); + } + } + } + var mapid = transform.MapID; + ApplyDimensionDebuff(subject, damage, mapid); + _transform.SetWorldPosition((subject, transform), new Vector2(0, 0)); + _transform.SetParent(subject, transform, mapEnt.Value); + RemComp(subject); + EnsureComp(subject); + + } + _audio.PlayPredicted(globaltpsound, hole, subject, AudioParams.Default.WithMaxDistance(30)); //i love this + _audio.PlayPredicted(globaltpsound, subject, hole, AudioParams.Default.WithMaxDistance(30)); + } + #endregion + #region Dimension Debuff Handling + private void ApplyDimensionDebuff(EntityUid uid, DamageSpecifier damage, MapId mapid) + { + //So basically it deals a constant DPS while in dimension, so ppl won't spend their whole life there + var dimensiondebuff = EnsureComp(uid); + dimensiondebuff.DamagePerSecond = damage; + dimensiondebuff.PastMapId = mapid; + dimensiondebuff.NextDamage = _gameTiming.CurTime + TimeSpan.FromSeconds(1.0f); + } + + private void UpdateDimensionDebuff() + { + var curTime = _gameTiming.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entity, out var debuff)) + { + if (curTime >= debuff.NextDamage) + { + _damageable.TryChangeDamage(entity, debuff.DamagePerSecond); + debuff.NextDamage += TimeSpan.FromSeconds(2.0f); + } + } + } + #endregion + #region Transformation + private EntityUid TransformInto(EntityUid uid, ProtoId configuration) + { + if (!TryComp(uid, out var scp)) + return EntityUid.Invalid; + if (!TryComp(uid, out var dmgable)) + return EntityUid.Invalid; + var newb = _polymorph.PolymorphEntity(uid, configuration); + var newscp = EnsureComp(newb ?? EntityUid.Invalid); + if (!TryComp(newb, out var newdmgable)) + return EntityUid.Invalid; + newscp.Puddles = scp.Puddles; + newscp.SleepOnAttack = scp.SleepOnAttack; + newscp.PocketMapId = scp.PocketMapId; + newscp.InDimension = scp.InDimension; + newscp.InPocketDimension = scp.InPocketDimension; + newscp.PastMapId = scp.PastMapId; + newscp.PastPosition = scp.PastPosition; + newscp.MaxPuddles = scp.MaxPuddles; + return newb ?? EntityUid.Invalid; + } + #endregion +} diff --git a/Content.Server/Imperial/Seriozha/Administration/Commands/PlayMapSoundCommand.cs b/Content.Server/Imperial/Seriozha/Administration/Commands/PlayMapSoundCommand.cs index 2c2a56c6600..41fc58291e6 100644 --- a/Content.Server/Imperial/Seriozha/Administration/Commands/PlayMapSoundCommand.cs +++ b/Content.Server/Imperial/Seriozha/Administration/Commands/PlayMapSoundCommand.cs @@ -1,116 +1,114 @@ using Content.Server.Audio; using Content.Server.Administration; using Content.Shared.Administration; -using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Console; using Robust.Shared.ContentPack; -using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Server.GameObjects; namespace Content.Server.Imperial.Seriozha.Administration.Commands; [AdminCommand(AdminFlags.Fun)] public sealed class PlayMapSoundCommand : IConsoleCommand { - [Dependency] private readonly IEntityManager _entManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly IResourceManager _res = default!; - [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IResourceManager _res = default!; - public string Command => "playmapsound"; - public string Description => "Plays a sound for all players on a single map."; - public string Help => "playmapsound [volume] [mapId]\nIf mapId is omitted, uses the executor's current map."; + public string Command => "playmapsound"; + public string Description => "Plays a sound for all players on a single map."; + public string Help => "playmapsound [volume] [mapId]\nIf mapId is omitted, uses the executor's current map."; - public void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length == 0) - { - shell.WriteLine(Help); - return; - } - var path = args[0]; - var audio = AudioParams.Default; - if (args.Length >= 2) - { - if (int.TryParse(args[1], out var volume)) - { - audio = audio.WithVolume(volume); - } - else - { - shell.WriteError($"Invalid volume of {args[1]} specified."); - return; - } - } - MapId? targetMap = null; - if (args.Length >= 3) - { - if (int.TryParse(args[2], out var mapInt)) - { - var mapId = new MapId(mapInt); - if (_mapManager.MapExists(mapId)) - targetMap = mapId; - else - { - shell.WriteError($"Map with id {mapInt} does not exist."); - return; - } - } - else - { - shell.WriteError($"Invalid map id: {args[2]}"); - return; - } - } - if (targetMap == null) - { - if (shell.Player is not { AttachedEntity: { } ent }) - { - shell.WriteError("No map specified and could not infer executor's map."); - return; - } + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length == 0) + { + shell.WriteLine(Help); + return; + } + var path = args[0]; + var audio = AudioParams.Default; + if (args.Length >= 2) + { + if (int.TryParse(args[1], out var volume)) + { + audio = audio.WithVolume(volume); + } + else + { + shell.WriteError($"Invalid volume of {args[1]} specified."); + return; + } + } + MapId? targetMap = null; + var mapSystem = _entManager.System(); + if (args.Length >= 3) + { + if (int.TryParse(args[2], out var mapInt)) + { + var mapId = new MapId(mapInt); + if (mapSystem.MapExists(mapId)) + targetMap = mapId; + else + { + shell.WriteError($"Map with id {mapInt} does not exist."); + return; + } + } + else + { + shell.WriteError($"Invalid map id: {args[2]}"); + return; + } + } + if (targetMap == null) + { + if (shell.Player is not { AttachedEntity: { } ent }) + { + shell.WriteError("No map specified and could not infer executor's map."); + return; + } - if (_entManager.TryGetComponent(ent, out TransformComponent? xform)) - { - targetMap = xform!.MapID; - } - else - { - shell.WriteError("No map specified and could not infer executor's map."); - return; - } - } - audio = audio.AddVolume(-8); - var filter = Filter.BroadcastMap(targetMap.Value); - _entManager.System().PlayAdminGlobal(filter, path, audio, true); - } + if (_entManager.TryGetComponent(ent, out TransformComponent? xform)) + { + targetMap = xform!.MapID; + } + else + { + shell.WriteError("No map specified and could not infer executor's map."); + return; + } + } + audio = audio.AddVolume(-8); + var filter = Filter.BroadcastMap(targetMap.Value); + _entManager.System().PlayAdminGlobal(filter, new ResolvedPathSpecifier(path), audio, true); + } - public CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - { - var hint = ""; - var options = CompletionHelper.AudioFilePath(args[0], _protoManager, _res); - return CompletionResult.FromHintOptions(options, hint); - } + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var hint = ""; + var options = CompletionHelper.AudioFilePath(args[0], _protoManager, _res); + return CompletionResult.FromHintOptions(options, hint); + } - if (args.Length == 2) - { - return CompletionResult.FromHint("[volume]"); - } + if (args.Length == 2) + { + return CompletionResult.FromHint("[volume]"); + } - if (args.Length == 3) - { - var options = CompletionHelper.MapIds(_entManager); - return CompletionResult.FromHintOptions(options, "[mapId]"); - } + if (args.Length == 3) + { + var options = CompletionHelper.MapIds(_entManager); + return CompletionResult.FromHintOptions(options, "[mapId]"); + } - return CompletionResult.Empty; - } + return CompletionResult.Empty; + } } diff --git a/Content.Server/Imperial/Seriozha/Fredik21/callmtf/MTFCallCommand.cs b/Content.Server/Imperial/Seriozha/Fredik21/callmtf/MTFCallCommand.cs new file mode 100644 index 00000000000..c5c160dd955 --- /dev/null +++ b/Content.Server/Imperial/Seriozha/Fredik21/callmtf/MTFCallCommand.cs @@ -0,0 +1,100 @@ +using Robust.Shared.Console; +using Content.Shared.Administration; +using Content.Server.Administration; +using Robust.Shared.Prototypes; +using System.Linq; +using Robust.Shared.Audio.Systems; +using Content.Server.Chat.Systems; +using Robust.Shared.Player; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.EntitySerialization.Systems; +using Robust.Shared.EntitySerialization; +using Content.Shared.Imperial.MTFCall; + +namespace Content.Server.Imperial.MTFCall; + +[AdminCommand(AdminFlags.Fun)] +public sealed class CallMTF : LocalizedCommands +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + public override string Command => "callMTF"; + public override string Description => Loc.GetString("callertcommand-desc"); + public override string Help => Loc.GetString("callertcommand-help"); + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var options = _protoManager + .EnumeratePrototypes() + .Select(p => new CompletionOption(p.ID, Loc.GetString(p.Desc))); + + return CompletionResult.FromHintOptions(options.OrderBy(x => x.Value).ToArray(), Loc.GetString("callertcommand-id-preset")); + } + return CompletionResult.Empty; + } + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("callertcommand-error-args0")); + return; + } + + var protoId = args[0]; + if (!_protoManager.TryIndex(protoId, out var proto)) + { + shell.WriteError(Loc.GetString("callertcommand-error-prest-not-found", ("protoid", protoId))); + return; + } + + // 1. Анонсы (Звук и Текст) + if (proto.AnnouncementSound != null) + _entManager.System().PlayGlobal(proto.AnnouncementSound, Filter.Broadcast(), true); + + if (proto.AnnouncementMessage != null) + { + var sender = proto.AnnouncementSender != null + ? Loc.GetString(proto.AnnouncementSender) + : Loc.GetString("announcementSender-MTF-NDA"); + + _entManager.System().DispatchGlobalAnnouncement( + Loc.GetString(proto.AnnouncementMessage), + sender: sender, + playSound: false, + colorOverride: Color.Gold); + } + + // 2. Логика загрузки грида + var mapSystem = _entManager.System(); + var mapLoader = _entManager.System(); + + // Создаем новую карту + var shuttleMapUid = mapSystem.CreateMap(); + + if (!_entManager.TryGetComponent(shuttleMapUid, out var mapComp)) + { + shell.WriteError("Failed to get MapComponent from created map."); + _entManager.DeleteEntity(shuttleMapUid); + return; + } + + var mapId = mapComp.MapId; + var options = new DeserializationOptions { InitializeMaps = true }; + + // Загружаем грид из прототипа + if (mapLoader.TryLoadGrid(mapId, proto.Path, out _, options)) + { + shell.WriteLine(Loc.GetString("callertcommand-success", ("protoid", protoId))); + } + else + { + shell.WriteError(Loc.GetString("callertcommand-error-when-load-grid")); + mapSystem.DeleteMap(mapId); + } + } +} diff --git a/Content.Server/Imperial/ShockWave/ShockWaveComponent.cs b/Content.Server/Imperial/ShockWave/ShockWaveComponent.cs index 383c0b659f7..8df15d70a16 100644 --- a/Content.Server/Imperial/ShockWave/ShockWaveComponent.cs +++ b/Content.Server/Imperial/ShockWave/ShockWaveComponent.cs @@ -22,7 +22,7 @@ public sealed partial class ShockWaveComponent : Component /// List of effects that should be applied. /// [DataField] - public List Effects = new(); + public EntityEffect[] Effects = []; [ViewVariables] diff --git a/Content.Server/Imperial/ShockWave/ShockWaveSystem.cs b/Content.Server/Imperial/ShockWave/ShockWaveSystem.cs index 96dce5f6fd0..f428d764890 100644 --- a/Content.Server/Imperial/ShockWave/ShockWaveSystem.cs +++ b/Content.Server/Imperial/ShockWave/ShockWaveSystem.cs @@ -17,6 +17,7 @@ public sealed class ShockWaveSystem : SharedShockWaveSystem [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; public override void Initialize() @@ -60,15 +61,7 @@ public override void Update(float frameTime) RaiseLocalEvent(uid, new ShockWaveEntityCollideEvent(uid, entity)); RaiseLocalEvent(uid, ref triggerEv, true); - foreach (var effect in component.Effects) - { - var args = new EntityEffectBaseArgs(entity, EntityManager); - var canApplyEffect = effect.Conditions?.Aggregate(true, (acc, condition) => condition.Condition(args) && acc) ?? true; - - if (!canApplyEffect) continue; - - effect.Effect(args); - } + _entityEffects.ApplyEffects(entity, component.Effects, user: entity); } } } diff --git a/Content.Server/Imperial/Sponsors/SponsorsManager.cs b/Content.Server/Imperial/Sponsors/SponsorsManager.cs index ac871e45839..4b4da7443ad 100644 --- a/Content.Server/Imperial/Sponsors/SponsorsManager.cs +++ b/Content.Server/Imperial/Sponsors/SponsorsManager.cs @@ -42,16 +42,7 @@ public bool TryGetInfo(NetUserId userId, [NotNullWhen(true)] out SponsorInfo? sp public async Task OnConnecting(NetConnectingArgs e) //Imperial We need call this before cheking new player on pass info { - var info = await LoadSponsorInfo(e.UserId); - //if (info?.Tier == null) - if (info == null) - { - _sawmill.Debug("no info found"); - //_cachedSponsors.Remove(e.UserId); // Remove from cache if sponsor expired - return; - } - DebugTools.Assert(!_cachedSponsors.ContainsKey(e.UserId), "Cached data was found on client connect"); - _cachedSponsors[e.UserId] = info; + } private void OnConnected(object? sender, NetChannelArgs e) diff --git a/Content.Server/Imperial/Traits/ImperialLegsParalyzedSystem.cs b/Content.Server/Imperial/Traits/ImperialLegsParalyzedSystem.cs new file mode 100644 index 00000000000..2a35bc44f65 --- /dev/null +++ b/Content.Server/Imperial/Traits/ImperialLegsParalyzedSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.Traits; + +namespace Content.Server.Imperial.Traits; + +public sealed class ImperialLegsParalyzedSystem : SharedImperialLegsParalyzedSystem +{ +} diff --git a/Content.Server/Imperial/TranslateTools/Commands.cs b/Content.Server/Imperial/TranslateTools/Commands.cs index 583723760bb..d3041d78ca0 100644 --- a/Content.Server/Imperial/TranslateTools/Commands.cs +++ b/Content.Server/Imperial/TranslateTools/Commands.cs @@ -1,149 +1,144 @@ - - -using System.Globalization; -using System.IO; -using System.Linq; -using Content.Server.Administration; -using Content.Shared.Administration; -using Linguini.Bundle.Builder; -using Linguini.Syntax.Parser; -using Robust.Shared.Console; -using Robust.Shared.ContentPack; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Server.Imperial.TranslateTools; - -[AdminCommand(AdminFlags.Host)] -public sealed class GetLocalData : LocalizedCommands -{ - public string Description => Loc.GetString("callertcommand-desc"); - public string Help => Loc.GetString("callertcommand-help"); - [Dependency] private readonly IEntityManager _entity = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IResourceManager resourceManager = default!; - public override string Command => "getLocaleData"; - private List cultures = new() - { - "ru-RU", - "en-US" - }; - private List mode = new() - { - "list", - "table" - }; - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Count() == 1) - { - return CompletionResult.FromOptions(cultures); - } - if (args.Count() == 2) - { - return CompletionResult.FromOptions(mode); - } - - return CompletionResult.Empty; - } - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Count() < 1) return; - var culture = args[0]; - var root = new ResPath($"/Locale/{culture}/"); - - var files = resourceManager.ContentFindFiles(root) - .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)) - .ToArray(); - var resources = files.AsParallel().Select(path => - { - string contents; - - using (var fileStream = resourceManager.ContentFileRead(path)) - using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8)) - { - contents = reader.ReadToEnd(); - } - - var parser = new LinguiniParser(contents); - var resource = parser.Parse(); - return (path, resource, contents); - }); - List result = new(); - foreach (var (path, resource, content) in resources) - { - if (args.Count() == 1 || args.Count() == 2 && args[1] == "list") - { - shell.WriteLine($"{path}"); - shell.WriteLine($"{content}"); - } - if (args.Count() == 2 && args[1] == "table") - { - var record = content.Split(new char[] { '\n', '\r' }); - foreach (var str in record) - { - if (str != "") - result.Add($"{path}鎰{str}\n"); - } - } - } - var savePath = new ResPath($"{args[0]}").ToRootedPath(); - resourceManager.UserData.CreateDir(savePath.Directory); - using var writer = resourceManager.UserData.OpenWriteText(savePath); - foreach (var str in result) - writer.Write(str); - shell.WriteLine("Complite!"); - shell.WriteLine($"data load to file {args[0]} in data directory"); - } -} - -[AdminCommand(AdminFlags.Host)] -public sealed class GetAllEntityes : LocalizedCommands -{ - public string Description => Loc.GetString("callertcommand-desc"); - public string Help => Loc.GetString("callertcommand-help"); - [Dependency] private readonly IEntityManager _entity = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IResourceManager resourceManager = default!; - [Dependency] private readonly ILocalizationManager _localizationManager = default!; - public override string Command => "getAllEntityes"; - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Count() == 1) - { - return CompletionResult.FromHint($"file"); - } - if (args.Count() == 2) - { - return CompletionResult.FromOptions(new List() { "true", "false" }); - } - - return CompletionResult.Empty; - } - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - var query = _prototype.EnumeratePrototypes(); - List result = new(); - foreach (var record in query) - { - var data = _localizationManager.GetEntityData(record.ID); - if (_localizationManager.HasString($"ent-{record.ID}") && args[1] == "false" || record.Abstract || record.HideSpawnMenu) continue; - result.Add($"ent-{record.ID} = 鎰{data.Name}\n"); - result.Add($".desc = 鎰{data.Desc}\n"); - result.Add($".suffix = 鎰{data.Suffix}\n"); - } - var savePath = new ResPath($"{args[0]}").ToRootedPath(); - resourceManager.UserData.CreateDir(savePath.Directory); - using var writer = resourceManager.UserData.OpenWriteText(savePath); - foreach (var str in result) - writer.Write(str); - shell.WriteLine("Complite!"); - shell.WriteLine($"data load to file {args[0]} in data directory"); - - } -} +// using System.IO; +// using System.Linq; +// using Content.Server.Administration; +// using Content.Shared.Administration; +// using Robust.Shared.Console; +// using Robust.Shared.ContentPack; +// using Robust.Shared.Prototypes; +// using Robust.Shared.Utility; + +// namespace Content.Server.Imperial.TranslateTools; + +// [AdminCommand(AdminFlags.Host)] +// public sealed class GetLocalData : LocalizedCommands +// { +// public string Description => Loc.GetString("callertcommand-desc"); +// public string Help => Loc.GetString("callertcommand-help"); +// [Dependency] private readonly IEntityManager _entity = default!; +// [Dependency] private readonly IPrototypeManager _prototype = default!; +// [Dependency] private readonly IResourceManager resourceManager = default!; +// public override string Command => "getLocaleData"; +// private List cultures = new() +// { +// "ru-RU", +// "en-US" +// }; +// private List mode = new() +// { +// "list", +// "table" +// }; + +// public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) +// { +// if (args.Count() == 1) +// { +// return CompletionResult.FromOptions(cultures); +// } +// if (args.Count() == 2) +// { +// return CompletionResult.FromOptions(mode); +// } + +// return CompletionResult.Empty; +// } + +// public override void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// if (args.Count() < 1) return; +// var culture = args[0]; +// var root = new ResPath($"/Locale/{culture}/"); + +// var files = resourceManager.ContentFindFiles(root) +// .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)) +// .ToArray(); +// var resources = files.AsParallel().Select(path => +// { +// string contents; + +// using (var fileStream = resourceManager.ContentFileRead(path)) +// using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8)) +// { +// contents = reader.ReadToEnd(); +// } + +// var parser = new LinguiniParser(contents); +// var resource = parser.Parse(); +// return (path, resource, contents); +// }); +// List result = new(); +// foreach (var (path, resource, content) in resources) +// { +// if (args.Count() == 1 || args.Count() == 2 && args[1] == "list") +// { +// shell.WriteLine($"{path}"); +// shell.WriteLine($"{content}"); +// } +// if (args.Count() == 2 && args[1] == "table") +// { +// var record = content.Split(new char[] { '\n', '\r' }); +// foreach (var str in record) +// { +// if (str != "") +// result.Add($"{path}鎰{str}\n"); +// } +// } +// } +// var savePath = new ResPath($"{args[0]}").ToRootedPath(); +// resourceManager.UserData.CreateDir(savePath.Directory); +// using var writer = resourceManager.UserData.OpenWriteText(savePath); +// foreach (var str in result) +// writer.Write(str); +// shell.WriteLine("Complite!"); +// shell.WriteLine($"data load to file {args[0]} in data directory"); +// } +// } + +// [AdminCommand(AdminFlags.Host)] +// public sealed class GetAllEntityes : LocalizedCommands +// { +// public string Description => Loc.GetString("callertcommand-desc"); +// public string Help => Loc.GetString("callertcommand-help"); +// [Dependency] private readonly IEntityManager _entity = default!; +// [Dependency] private readonly IPrototypeManager _prototype = default!; +// [Dependency] private readonly IResourceManager resourceManager = default!; +// [Dependency] private readonly ILocalizationManager _localizationManager = default!; +// public override string Command => "getAllEntityes"; + +// public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) +// { +// if (args.Count() == 1) +// { +// return CompletionResult.FromHint($"file"); +// } +// if (args.Count() == 2) +// { +// return CompletionResult.FromOptions(new List() { "true", "false" }); +// } + +// return CompletionResult.Empty; +// } + +// public override void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// var query = _prototype.EnumeratePrototypes(); +// List result = new(); +// foreach (var record in query) +// { +// var data = _localizationManager.GetEntityData(record.ID); +// if (_localizationManager.HasString($"ent-{record.ID}") && args[1] == "false" || record.Abstract || record.HideSpawnMenu) continue; +// result.Add($"ent-{record.ID} = 鎰{data.Name}\n"); +// result.Add($".desc = 鎰{data.Desc}\n"); +// result.Add($".suffix = 鎰{data.Suffix}\n"); +// } +// var savePath = new ResPath($"{args[0]}").ToRootedPath(); +// resourceManager.UserData.CreateDir(savePath.Directory); +// using var writer = resourceManager.UserData.OpenWriteText(savePath); +// foreach (var str in result) +// writer.Write(str); +// shell.WriteLine("Complite!"); +// shell.WriteLine($"data load to file {args[0]} in data directory"); + +// } +// } diff --git a/Content.Server/Imperial/Vampire/VampireDeadEui.cs b/Content.Server/Imperial/Vampire/VampireDeadEui.cs new file mode 100644 index 00000000000..9daefe350ba --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampireDeadEui.cs @@ -0,0 +1,6 @@ +using Content.Server.EUI; + +namespace Content.Server.Imperial.Vampire; + +public sealed class VampireDeadEui : BaseEui +{ } diff --git a/Content.Server/Imperial/Vampire/VampirePurposesSystem.cs b/Content.Server/Imperial/Vampire/VampirePurposesSystem.cs new file mode 100644 index 00000000000..d7facc67735 --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampirePurposesSystem.cs @@ -0,0 +1,63 @@ +using Content.Server.Objectives.Systems; +using Content.Shared.Imperial.Vampire; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Imperial.Vampire; + +public sealed class VampirePurposesSystem : EntitySystem +{ + [Dependency] private readonly NumberObjectiveSystem _number = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDrinkBloodProgress); + SubscribeLocalEvent(OnConvertedGhoulsProgress); + } + + private void OnDrinkBloodProgress(EntityUid uid, VampireDrinkBloodPurposesComponent comp, ref ObjectiveGetProgressEvent args) + { + args.Progress = GetDrinkBloodProgress(args.MindId, _number.GetTarget(uid)); + } + + private void OnConvertedGhoulsProgress(EntityUid uid, VampireConvertedGhoulsPurposesComponent comp, ref ObjectiveGetProgressEvent args) + { + args.Progress = GetConvertedGhoulsProgress(args.MindId, _number.GetTarget(uid)); + } + + private float GetDrinkBloodProgress(EntityUid mindId, int target) + { + if (!TryComp(mindId, out var mindComp)) + return 0f; + + if (mindComp.OwnedEntity == null) + return 0f; + + if (!TryComp(mindComp.OwnedEntity.Value, out var vamp)) + return 0f; + + if (target == 0) + return 1f; + + return MathF.Min(vamp.TotalDrunk / target, 1f); + } + + private float GetConvertedGhoulsProgress(EntityUid mindId, int target) + { + if (!TryComp(mindId, out var mindComp)) + return 0f; + + if (mindComp.OwnedEntity == null) + return 0f; + + if (!TryComp(mindComp.OwnedEntity.Value, out var vamp)) + return 0f; + + if (target == 0) + return 1f; + + return MathF.Min((float)vamp.GhoulQuantity / target, 1f); + } +} diff --git a/Content.Server/Imperial/Vampire/VampireRequestedEui.cs b/Content.Server/Imperial/Vampire/VampireRequestedEui.cs new file mode 100644 index 00000000000..91bdbc537b5 --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampireRequestedEui.cs @@ -0,0 +1,87 @@ +using Content.Server.EUI; +using Content.Shared.Actions; +using Content.Shared.Eui; +using Content.Shared.Imperial.Vampire; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.Vampire; + +public sealed class VampireRequestedEui : BaseEui +{ + private readonly IEntityManager _entityManager = default!; + private readonly EntityUid _uid; + private readonly SharedActionsSystem _actions = default!; + private readonly SharedVampireSystem _vampireSystem = default!; + private readonly IPrototypeManager _prototypeManager = default!; + + public VampireRequestedEui(EntityUid uid, IEntityManager entityManager, SharedActionsSystem actions, + SharedVampireSystem vampireSystem, IPrototypeManager prototypeManager) + { + _uid = uid; + _entityManager = entityManager; + _actions = actions; + _vampireSystem = vampireSystem; + _prototypeManager = prototypeManager; + } + + public override void HandleMessage(EuiMessageBase msg) + { + if (msg is not VampireRequestedEuiMessage request) + return; + + if (!_entityManager.TryGetComponent(_uid, out VampireComponent? vamp)) + return; + + GrantAbilities(_uid, request.Selection); + Close(); + _actions.RemoveAction(_uid, vamp.SelectingSubgroupActionEntity); + } + + public void GrantAbilities(EntityUid uid, VampireAbilityType selection) + { + if (uid != _uid) + return; + + if (!_entityManager.TryGetComponent(_uid, out VampireComponent? vamp)) + return; + + if (!_prototypeManager.TryIndex(vamp.VampireAbilitiesID[VampireAbilityType.Hemomancer], out var hemomancer) || + !_prototypeManager.TryIndex(vamp.VampireAbilitiesID[VampireAbilityType.Umbrae], out var umbrae) || + !_prototypeManager.TryIndex(vamp.VampireAbilitiesID[VampireAbilityType.Gargantua], out var gargantua)) + return; + + var selected = selection switch + { + VampireAbilityType.Hemomancer => hemomancer, + VampireAbilityType.Umbrae => umbrae, + VampireAbilityType.Gargantua => gargantua, + _ => null + }; + + if (selected == null) + return; + + vamp.DirectionSelected = true; + vamp.SelectedSubgroup = selection; + _vampireSystem.SetBloodCounterAlert(uid); + + for (var i = 0; i < selected.Abilities.Count; i++) + { + if (!selected.Thresholds.TryGetValue(i, out var threshold) || vamp.TotalDrunk < threshold + || vamp.UnlockedAbilityIndices.Contains(i)) + continue; + + EntityUid? actionEnt = null; + _actions.AddAction(uid, ref actionEnt, selected.Abilities[i]); + + if (actionEnt != null) + { + vamp.GrantedActions.Add(actionEnt.Value); + vamp.UnlockedAbilityIndices.Add(i); + } + } + + _entityManager.Dirty(uid, vamp); + } + +} diff --git a/Content.Server/Imperial/Vampire/VampireSystem.Abilities.cs b/Content.Server/Imperial/Vampire/VampireSystem.Abilities.cs new file mode 100644 index 00000000000..18e6c917ea3 --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampireSystem.Abilities.cs @@ -0,0 +1,442 @@ +using Content.Shared.Imperial.Vampire; +using Content.Shared.Popups; +using Content.Server.Polymorph.Components; +using System.Numerics; +using Content.Server.NPC.HTN; +using Content.Shared.NPC; +using Content.Server.NPC; +using System.Linq; +using Content.Server.Stunnable; +using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; +using Content.Shared.Bed.Sleep; +using Content.Shared.DoAfter; +using Content.Shared.StatusEffectNew; +using Content.Shared.Mobs.Components; +using Content.Shared.Flash; +using Content.Shared.Actions.Components; +using System.Runtime.CompilerServices; + +namespace Content.Server.Imperial.Vampire; + + +public partial class VampireSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly EntityManager _entityManager = default!; + + private void VampireAbilitiesInitialize() + { + SubscribeLocalEvent(OnTransformToBat); + SubscribeLocalEvent(OnClone); + SubscribeLocalEvent(OnTeleport); + SubscribeLocalEvent(OnTurn); + SubscribeLocalEvent(OnSelectingSubgroup); + + SubscribeLocalEvent(OnStartSleep); + SubscribeLocalEvent(OnSleep); + } + + private void OnTransformToBat(VampireBatTransformEvent args) + { + if (!TryComp(args.Performer, out var vampireComponent) && !TryComp(args.Performer, out _)) + return; + + var vamp = EnsureComp(args.Performer); + + if (!_vampireSystem.HasEnoughBloodShared(args.Performer, args.CostBlood)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-not-enough-blood"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + if (vamp.BuffBlocked) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-warning-turning-bat"), + args.Performer, args.Performer, PopupType.Medium); + + return; + } + + if (vamp.DisguiseIsActive) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-disguise-on"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + var playerCoords = Transform(args.Performer).Coordinates; + + for (int i = 0; i < args.BatCount; i++) + { + var bat = Spawn(args.BatID, playerCoords.Offset(new Vector2(i, 0))); + vamp.BatsUid.Add(bat); + } + + _polymorph.PolymorphEntity(args.Performer, args.BatConfig); + + vamp.VampireIsBat = true; + vamp.DisguiseIsActive = true; + + _vampireSystem.DealAbilityBloodDamageShared(args.Performer, args.CostBlood); + + Dirty(args.Performer, vamp); + args.Handled = true; + } + + private void OnTeleport(VampireTeleportEvent args) + { + if (!TryComp(args.Performer, out var vampireComponent) && !TryComp(args.Performer, out _)) + return; + + var vamp = EnsureComp(args.Performer); + + if (!_vampireSystem.HasEnoughBloodShared(args.Performer, args.CostBlood)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-not-enough-blood"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + var target = vamp.TargetUser ? args.Performer : args.Performer; + + var fromCoords = Transform(target).Coordinates; + var toCoords = _vampireSystem.VampireRandomTileInRange(Transform(target), args.TeleportRadius); + if (toCoords == null) + return; + + _vampireSystem.SpawnSmokeEffect(vamp, fromCoords); + + _audio.PlayPvs(vamp.TeleportSound, args.Performer); + + _transformSystem.SetCoordinates(target, toCoords.Value); + + _vampireSystem.DealAbilityBloodDamageShared(args.Performer, args.CostBlood); + args.Handled = true; + } + + private void OnStartSleep(VampireSleepEvent args) + { + if (!TryComp(args.Performer, out var vampireComponent) && !TryComp(args.Performer, out _)) + return; + + var vamp = EnsureComp(args.Performer); + + if (!_vampireSystem.HasEnoughBloodShared(args.Performer, args.CostBlood)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-not-enough-blood"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + // получаем все сущности перед игроком + var transform = Transform(args.Performer); + var direction = transform.LocalRotation.GetCardinalDir(); + var frontPos = transform.Coordinates.Offset(direction.ToVec()); + var target = _lookup.GetEntitiesInRange(frontPos, 0.5f, LookupFlags.Uncontained | LookupFlags.Dynamic) + .FirstOrDefault(el => el != args.Performer && HasComp(el)); + + if (!_mobState.IsAlive(target)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-no-one-ahead"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + // вампир не может усыпить нося маску/солнцезащитные очки + var flashVampireAttempt = new FlashAttemptEvent(args.Performer, args.Performer, null); + RaiseLocalEvent(args.Performer, ref flashVampireAttempt, true); + + if (flashVampireAttempt.Cancelled) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-vampire-has-eye-protection"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + // вампир не может усыпить людей с маской/солнцезащитными очками + var flashAttempt = new FlashAttemptEvent(target, args.Performer, null); + RaiseLocalEvent(target, ref flashAttempt, true); + + if (flashAttempt.Cancelled) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-has-eye-protection"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + if (TryComp(vamp.SleepUid, out var sleep)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-asleep"), + args.Performer, args.Performer, PopupType.Medium); + vamp.SleepUid = EntityUid.Invalid; + return; + } + + // станим цель на время doAfterArgs + _stunSystem.TryAddStunDuration(target, args.DoAfterBeforeEuthanasia); + vamp.SleepUid = target; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.Performer, args.DoAfterBeforeEuthanasia, + new VampireSleepDoAfterEvent { SleepingTime = args.SleepingTime }, args.Performer) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + BlockDuplicate = true, + Hidden = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + _vampireSystem.DealAbilityBloodDamageShared(args.Performer, args.CostBlood); + } + + private void OnSleep(Entity vamp, ref VampireSleepDoAfterEvent args) + { + if (!TryComp(vamp, out var vampireComponent) && !TryComp(vamp, out var ghoul)) + return; + + if (args.Cancelled || args.Handled) + { + vamp.Comp.SleepUid = EntityUid.Invalid; + return; + } + + if (vamp.Comp.SleepUid == EntityUid.Invalid || vamp.Comp.SleepUid == vamp.Owner) + { + vamp.Comp.SleepUid = EntityUid.Invalid; + return; + } + + _statusEffects.TryAddStatusEffectDuration(vamp.Comp.SleepUid, SleepingSystem.StatusEffectForcedSleeping, args.SleepingTime); + + Dirty(vamp, vamp.Comp); + args.Handled = true; + } + + private void OnClone(VampireCloneEvent args) + { + if (!TryComp(args.Performer, out var vampireComponent) && !TryComp(args.Performer, out _)) + return; + + var vamp = EnsureComp(args.Performer); + + if (!_prototypeManager.TryIndex(args.Settings, out var settings)) + return; + + if (!_vampireSystem.HasEnoughBloodShared(args.Performer, args.CostBlood)) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-not-enough-blood"), + args.Performer, args.Performer, PopupType.Medium); + return; + } + + if (vamp.BuffBlocked) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-warning-ability-buff"), + args.Performer, args.Performer, PopupType.Medium); + + return; + } + + // получаем местоположение вампира, чтобы рядом с ним заспавнить клона + var mapCoords = _transformSystem.GetMapCoordinates(args.Performer); + + if (!_cloning.TryCloning(args.Performer, mapCoords, settings, out vamp.CloneUid)) + return; + + // добавляем ai клону + var htn = EnsureComp(vamp.CloneUid.Value); + htn.RootTask = new HTNCompoundTask() { Task = "IdleCompound" }; + htn.Blackboard.SetValue(NPCBlackboard.Owner, vamp.CloneUid.Value); + + EnsureComp(vamp.CloneUid.Value); + + _vampireSystem.VampireInvisible(args.Performer); + vamp.BuffBlockedUntil = _gameTiming.CurTime + args.InvisibilityCloneTime; + vamp.VampireCloneIsActive = true; + + _vampireSystem.DealAbilityBloodDamageShared(args.Performer, args.CostBlood); + + // ссылаемся на VampireInvisibleAction. см VampireBaseAbilities, VampireUmbrae + _actions.SetCooldown(vampireComponent!.GrantedActions[6], args.InvisibilityCloneTime * 2); + + Dirty(args.Performer, vamp); + args.Handled = true; + } + + private void OnTurn(VampireTurnEvent args) + { + if (!TryComp(args.Performer, out var vampireComponent)) + return; + + var vamp = EnsureComp(args.Performer); + + if (vamp.VampireTurned) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-warning-vampire-turned"), + args.Performer, args.Performer, PopupType.Medium); + + return; + } + + vamp.HaloUid = Spawn(vamp.HaloEffect, Transform(args.Performer).Coordinates); + _transform.SetParent(vamp.HaloUid.Value, args.Performer); + + foreach (var ghoul in vampireComponent.Ghouls) + { + var ghoulAbilityComp = EnsureComp(ghoul); + if (!TryComp(ghoul, out var ghoulComponent)) continue; + + EnsureComp(ghoul); + + ghoulAbilityComp.HaloUid = Spawn(ghoulAbilityComp.HaloEffect, Transform(ghoul).Coordinates); + _transform.SetParent(ghoulAbilityComp.HaloUid.Value, ghoul); + + var usedIndices = new HashSet(); + for (int i = 0; i < vamp.GhoulBaseAbility; i++) + { + int abilityNumber; + do + { + abilityNumber = _random.Next(0, 3); + } while (usedIndices.Contains(abilityNumber) || abilityNumber == 2); + + usedIndices.Add(abilityNumber); + + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Base], out var baseAbilities)) + { + var action = _actions.AddAction(ghoul, baseAbilities.Abilities[abilityNumber]); + if (action != null) + { + // см BaseAbilities. Ссылаемся на "Кровавая катана" + if (abilityNumber == 0) ghoulComponent.GhoulVampireSwordAction = _entityManager.GetNetEntity(action); + ghoulComponent.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + } + } + + for (int i = 0; i < vamp.GhoulGroupAbility; i++) + { + var abilityNumber = _random.Next(0, 3); + + switch (vampireComponent.SelectedSubgroup) + { + case VampireAbilityType.Hemomancer: + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Hemomancer], out var hemomancerAbilities)) + { + var action = _actions.AddAction(ghoul, hemomancerAbilities.Abilities[abilityNumber]); + // см BaseAbilities. Ссылаемся на "Кровавые щупальца" + if (abilityNumber == 5) ghoulComponent.GhoulVampireTentaclesAction = _entityManager.GetNetEntity(action); + if (action != null) ghoulComponent.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + + case VampireAbilityType.Umbrae: + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Umbrae], out var umbraeAbilities)) + { + var action = _actions.AddAction(ghoul, umbraeAbilities.Abilities[abilityNumber]); + // см BaseAbilities. Ссылаемся на "Кровавый якорь" + if (abilityNumber == 7) ghoulComponent.GhoulVampireBloodAnchorAction = _entityManager.GetNetEntity(action); + if (action != null) ghoulComponent.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + + case VampireAbilityType.Gargantua: + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Gargantua], out var gargantuaAbilities)) + { + var action = _actions.AddAction(ghoul, gargantuaAbilities.Abilities[abilityNumber]); + if (action != null) ghoulComponent.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + } + } + + Dirty(ghoul, ghoulComponent); + } + + switch (vampireComponent.SelectedSubgroup) + { + case VampireAbilityType.Hemomancer: + // см BaseAbilities, VampireAbilityLists.Hemomancer. Удаляется "Кровавая катана" + _actions.RemoveAction(args.Performer, vampireComponent.GrantedActions[0]); + + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Hemomancer], out var hemomancerAbilities)) + _actions.AddAction(args.Performer, hemomancerAbilities.Upgrades[0]); + + break; + + case VampireAbilityType.Umbrae: + // см BaseAbilities, VampireAbilityLists.Umbrae. Удаляется: "Переключить режим невидимости" + _actions.RemoveAction(args.Performer, vampireComponent.GrantedActions[6]); + + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Umbrae], out var umbraeAbilities)) + _actions.AddAction(args.Performer, umbraeAbilities.Upgrades[0]); + + break; + + case VampireAbilityType.Gargantua: + // см BaseAbilities, VampireAbilityLists.Gargantua. Удаляется "Гнев Носферату" + _actions.RemoveAction(args.Performer, vampireComponent.GrantedActions[8]); + + if (_prototypeManager.TryIndex( + vampireComponent.VampireAbilitiesID[VampireAbilityType.Gargantua], out var gargantuaAbilities)) + _actions.AddAction(args.Performer, gargantuaAbilities.Upgrades[0]); + + break; + } + + vamp.VampireTurned = true; + Dirty(args.Performer, vamp); + args.Handled = true; + } + + public void AbilitiesUpdate() + { + var queryVampBat = EntityQueryEnumerator(); + while (queryVampBat.MoveNext(out var uid, out var vamp)) + { + if (!TryComp(uid, out var vampireComponent) && !TryComp(uid, out var ghoul)) + return; + + if (!HasComp(uid) && vamp.VampireIsBat) + { + foreach (var bats in vamp.BatsUid.ToList()) + { + QueueDel(bats); + vamp.BatsUid.Remove(bats); + } + + vamp.VampireIsBat = false; + vamp.DisguiseIsActive = false; + Dirty(uid, vamp); + } + } + + var queryClone = EntityQueryEnumerator(); + while (queryClone.MoveNext(out var uid, out var vamp)) + { + if (!TryComp(uid, out var vampireComponent) && !TryComp(uid, out var ghoul)) + return; + + if (_gameTiming.CurTime >= vamp.BuffBlockedUntil && vamp.VampireCloneIsActive) + { + _vampireSystem.VampireInvisible(uid); + QueueDel(vamp.CloneUid); + vamp.VampireCloneIsActive = false; + Dirty(uid, vamp); + } + } + } +} diff --git a/Content.Server/Imperial/Vampire/VampireSystem.Ghoul.cs b/Content.Server/Imperial/Vampire/VampireSystem.Ghoul.cs new file mode 100644 index 00000000000..0019e24a71a --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampireSystem.Ghoul.cs @@ -0,0 +1,283 @@ +using Content.Shared.Body.Components; +using Content.Shared.Imperial.Vampire; +using Content.Shared.Verbs; +using Robust.Shared.Utility; +using Content.Shared.Popups; +using Content.Shared.DoAfter; +using Content.Shared.Roles.Components; +using Robust.Shared.Prototypes; +using Content.Shared.Prayer; +using Content.Server.Bible.Components; +using Content.Shared.Interaction; +using Content.Shared.Radio.Components; +using Content.Shared.Radio; +using Content.Shared.Actions.Components; + +namespace Content.Server.Imperial.Vampire; + +public partial class VampireSystem : EntitySystem +{ + private void OnGetDrinkingGhoul(EntityUid uid, GhoulComponent comp, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || uid == args.Target || !_mobState.IsAlive(args.Target)) + return; + + // верб для питья крови + if (!HasComp(args.Target) && !HasComp(args.Target) && HasComp(args.Target)) + { + var verbDrinkBloodGhoul = new InnateVerb + { + Act = () => + { + StartDrinking(args.User, args.Target); + }, + Text = Loc.GetString("vampire-drinking-envelope-text"), + Message = Loc.GetString("vampire-drinking-envelope-message"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Imperial/Stellark/Vampire/verbs/drinkBlood.png")), + Priority = 0 + }; + args.Verbs.Add(verbDrinkBloodGhoul); + } + } + + private void OnCureGhoulStart(EntityUid uid, GhoulComponent comp, InteractUsingEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.Used, out var prayable)) + return; + + if (prayable.BibleUserOnly && !TryComp(args.User, out _)) + return; + + if (TryComp(comp.Vampire, out var vamp) && TryComp(comp.Vampire, out var abilityComponent)) + { + if (abilityComponent.VampireTurned) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-vampire-turned"), + args.User, args.User, PopupType.Medium); + + return; + } + } + + _popup.PopupEntity(Loc.GetString("vampire-popup-ghoul-rite"), + args.User, args.User, PopupType.Medium); + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.GhoulCure, + new VampireCureGhoulDoAfterEvent(), uid, target: uid) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true, + BlockDuplicate = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + private void OnCureGhoul(Entity ent, ref VampireCureGhoulDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) + return; + + RemoveMindFromGhoul(ent); + RemComp(args.Target.Value); + _vampireSystem.SetGhoulBloodAlert(ent, ent.Comp); + + // обновляем данные у вампира + if (TryComp(ent.Comp.Vampire, out var vamp) && TryComp(ent.Comp.Vampire, out var abilityComponent)) + { + var transmitter = EnsureComp(args.Target.Value); + transmitter.Channels.Remove(new ProtoId(abilityComponent.VampireRadioID)); + + var activeRadio = EnsureComp(args.Target.Value); + activeRadio.Channels.Remove(new ProtoId(abilityComponent.VampireRadioID)); + + if (vamp.Ghouls.Remove(args.Target.Value)) + { + vamp.GhoulQuantity = Math.Max(0, vamp.GhoulQuantity - 1); + Dirty(ent.Comp.Vampire, vamp); + } + } + + _popup.PopupEntity(Loc.GetString("vampire-popup-successfully-cure-priest"), + args.User, args.User, PopupType.Medium); + + _popup.PopupEntity(Loc.GetString("vampire-popup-successfully-cure-ghoul"), + args.Target.Value, args.Target.Value, PopupType.Medium); + + args.Handled = true; + } + + /// + /// обработчик DoAfter для упыря + /// + private void OnDrinkingCompleteGhoul(Entity ent, ref VampireDrinkingDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target is not { } target) + return; + + DrinkingComplete(args.User, target, ent.Comp.BloodPerTick); + args.Handled = true; + } + + private void ConvertToGhoul(EntityUid vampire, EntityUid target) + { + var ghoulComp = EnsureComp(target); + ghoulComp.Vampire = vampire; + + EnsureComp(target); + EnsureComp(target); + + var masterThralls = EnsureComp(vampire); + var abilityComponent = EnsureComp(vampire); + masterThralls.Ghouls.Add(target); + masterThralls.GhoulQuantity++; + AppealGhoulsCooldown(vampire); + + Dirty(vampire, masterThralls); + Dirty(target, ghoulComp); + + // добавляем рацию + var transmitter = EnsureComp(target); + transmitter.Channels ??= new HashSet>(); + transmitter.Channels.Add(new ProtoId(abilityComponent.VampireRadioID)); + + var activeRadio = EnsureComp(target); + activeRadio.Channels ??= new HashSet>(); + activeRadio.Channels.Add(new ProtoId(abilityComponent.VampireRadioID)); + + EnsureComp(target); + + _popup.PopupEntity(Loc.GetString("vampire-verb-envelope-vampire-complete", + ("target", MetaData(target).EntityName)), + vampire, vampire, PopupType.LargeCaution); + + if (_mind.TryGetMind(target, out var mindId, out var mind)) + { + // добавление роли и базовых эффектов для игрока + if (!_roleSystem.MindHasRole(mindId)) + _roleSystem.MindAddRole(mindId, ghoulComp.MindRoleGhoulID, mind: mind); + + if (_player.TryGetSessionById(mind.UserId, out var session)) + _chatMan.DispatchServerMessage(session, Loc.GetString("vampire-verb-envelope-ghoul-greeting")); + } + + if (abilityComponent.VampireTurned) + { + var ghoulAbilityComp = EnsureComp(target); + EnsureComp(target); + + ghoulAbilityComp.HaloUid = Spawn(ghoulAbilityComp.HaloEffect, Transform(target).Coordinates); + _transform.SetParent(ghoulAbilityComp.HaloUid.Value, target); + + var usedIndices = new HashSet(); + for (int i = 0; i < abilityComponent.GhoulBaseAbility; i++) + { + int abilityNumber; + do + { + abilityNumber = _random.Next(0, 3); + } while (usedIndices.Contains(abilityNumber) || abilityNumber == 2); + + usedIndices.Add(abilityNumber); + + if (_prototypeManager.TryIndex( + masterThralls.VampireAbilitiesID[VampireAbilityType.Base], out var baseAbilities)) + { + var action = _actions.AddAction(target, baseAbilities.Abilities[abilityNumber]); + if (action != null) + { + // см BaseAbilities. Ссылаемся на "Кровавая катана" + if (abilityNumber == 0) ghoulComp.GhoulVampireSwordAction = _entityManager.GetNetEntity(action); + ghoulComp.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + } + } + + for (int i = 0; i < abilityComponent.GhoulGroupAbility; i++) + { + var abilityNumber = _random.Next(0, 3); + + switch (masterThralls.SelectedSubgroup) + { + case VampireAbilityType.Hemomancer: + if (_prototypeManager.TryIndex( + masterThralls.VampireAbilitiesID[VampireAbilityType.Hemomancer], out var hemomancerAbilities)) + { + var action = _actions.AddAction(target, hemomancerAbilities.Abilities[abilityNumber]); + // см BaseAbilities. Ссылаемся на "Кровавые щупальца" + if (abilityNumber == 5) ghoulComp.GhoulVampireTentaclesAction = _entityManager.GetNetEntity(action); + if (action != null) ghoulComp.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + + case VampireAbilityType.Umbrae: + if (_prototypeManager.TryIndex( + masterThralls.VampireAbilitiesID[VampireAbilityType.Umbrae], out var umbraeAbilities)) + { + var action = _actions.AddAction(target, umbraeAbilities.Abilities[abilityNumber]); + // см BaseAbilities. Ссылаемся на "Кровавый якорь" + if (abilityNumber == 7) ghoulComp.GhoulVampireBloodAnchorAction = _entityManager.GetNetEntity(action); + if (action != null) ghoulComp.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + + case VampireAbilityType.Gargantua: + if (_prototypeManager.TryIndex( + masterThralls.VampireAbilitiesID[VampireAbilityType.Gargantua], out var gargantuaAbilities)) + { + var action = _actions.AddAction(target, gargantuaAbilities.Abilities[abilityNumber]); + if (action != null) ghoulComp.GhoulGrantedActions.Add(_entityManager.GetNetEntity(action.Value)); + } + + break; + } + } + + Dirty(target, ghoulComp); + } + + _vampireSystem.SetGhoulBloodAlert(target, ghoulComp); + } + + private void RemoveMindFromGhoul(EntityUid uid) + { + if (_mind.TryGetMind(uid, out var mindId, out var mind)) + { + _roleSystem.MindRemoveRole((mindId, mind)); + } + + if (!TryComp(uid, out var ghoulComponent)) return; + + var comp = EnsureComp(uid); + if (comp.HaloUid != null) QueueDel(comp.HaloUid); + if (comp.InvisibleIsActive) _vampireSystem.VampireInvisible(uid); + if (comp.ItemIssued) _vampireSystem.OnIssuingSword(uid); + + var transmitter = EnsureComp(uid); + transmitter.Channels.Remove(new ProtoId(comp.VampireRadioID)); + + var activeRadio = EnsureComp(uid); + activeRadio.Channels.Remove(new ProtoId(comp.VampireRadioID)); + + if (ghoulComponent.GhoulGrantedActions.Count > 0) + { + foreach (var action in ghoulComponent.GhoulGrantedActions) + { + _actions.RemoveAction(uid, _entityManager.GetEntity(action)); + } + } + + RemComp(uid); + + _alert.ClearAlert(uid, comp.AdjacentChaplainAlert); + _vampireSystem.SetGhoulBloodAlert(uid, ghoulComponent); + + RemComp(uid); + } +} diff --git a/Content.Server/Imperial/Vampire/VampireSystem.cs b/Content.Server/Imperial/Vampire/VampireSystem.cs new file mode 100644 index 00000000000..d10742bf1a5 --- /dev/null +++ b/Content.Server/Imperial/Vampire/VampireSystem.cs @@ -0,0 +1,521 @@ +using Content.Shared.Verbs; +using Content.Shared.Imperial.Vampire; +using Robust.Shared.Utility; +using Content.Shared.Popups; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Player; +using Content.Server.Chat.Managers; +using Content.Shared.DoAfter; +using Robust.Shared.Timing; +using Content.Shared.Roles; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.Chemistry.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Mind; +using Content.Shared.Body.Components; +using Content.Server.Polymorph.Systems; +using Content.Server.Cloning; +using Robust.Shared.Prototypes; +using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; +using Content.Server.EUI; +using Content.Shared.Actions; +using Content.Shared.Interaction; +using Robust.Server.Audio; +using Content.Shared.Mindshield.Components; +using Content.Shared.Mind.Components; +using Content.Shared.Mobs; +using Content.Server.Bible.Components; +using Content.Shared.Alert; +using Content.Server.Body; +using System.Linq; +using Content.Shared.Radio.Components; +using Content.Shared.Radio; +using Robust.Server.GameObjects; +using Robust.Shared.Random; +using Content.Shared.Prayer; +using Content.Shared.Roles.Components; + +namespace Content.Server.Imperial.Vampire; + + +public sealed partial class VampireSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly IChatManager _chatMan = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedRoleSystem _roleSystem = default!; + [Dependency] private readonly DamageableSystem _damage = default!; + [Dependency] private readonly SharedVampireSystem _vampireSystem = default!; + [Dependency] private readonly PolymorphSystem _polymorph = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly CloningSystem _cloning = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly EuiManager _eui = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly AlertsSystem _alert = default!; + [Dependency] private readonly VisualBodySystem _visualBodySystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + + private void VampireInitialize() + { + SubscribeLocalEvent>(OnGetVerbsCombined); + SubscribeLocalEvent>(OnGetDrinkingGhoul); + SubscribeLocalEvent(OnCureGhoulStart); + SubscribeLocalEvent(OnCureVampireStart); + SubscribeLocalEvent(OnCureGhoul); + SubscribeLocalEvent(OnCureVampire); + SubscribeLocalEvent(OnEnvelopeCompleteVampire); + SubscribeLocalEvent(OnDrinkingCompleteGhoul); + SubscribeLocalEvent(OnDrinkingCompleteVampire); + + SubscribeNetworkEvent(OnMindRemoved); + SubscribeLocalEvent(OnDead); + } + + public override void Initialize() + { + base.Initialize(); + + VampireInitialize(); + VampireAbilitiesInitialize(); + } + + private void OnGetVerbsCombined(EntityUid uid, VampireComponent vamp, GetVerbsEvent args) + { + var comp = EnsureComp(uid); + if (!args.CanAccess || !args.CanInteract || comp.InvisibleIsActive || args.Target == uid + || !_mobState.IsAlive(args.Target)) + return; + + // верб для превращения цели в упыря + if (!HasComp(args.Target) && !HasComp(args.Target) && HasComp(args.Target) + && !HasComp(args.Target) && !_statusEffects.HasStatusEffect(uid, vamp.CooldownStatusEffectAppealGhouls) + && HasComp(args.Target) && HasComp(args.Target)) + { + var verbConvert = new InnateVerb + { + Act = () => StartConversion(args.User, args.Target), + Text = Loc.GetString("vampire-verb-envelope-ghoul-text"), + Message = Loc.GetString("vampire-verb-envelope-ghoul-message"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Imperial/Stellark/Vampire/verbs/appeal.png")), + Priority = 0 + }; + args.Verbs.Add(verbConvert); + } + + // верб для питья крови + if (!HasComp(args.Target) && !HasComp(args.Target) && HasComp(args.Target)) + { + var verbDrinkBlood = new InnateVerb + { + Act = () => StartDrinking(args.User, args.Target), + Text = Loc.GetString("vampire-drinking-envelope-text"), + Message = Loc.GetString("vampire-drinking-envelope-message"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Imperial/Stellark/Vampire/verbs/drinkBlood.png")), + Priority = 1 + }; + args.Verbs.Add(verbDrinkBlood); + } + } + + private void OnCureVampireStart(EntityUid uid, VampireComponent comp, InteractUsingEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.Used, out var prayable)) + return; + + if (prayable.BibleUserOnly && !TryComp(args.User, out _)) + return; + + if (TryComp(uid, out var vamp) && TryComp(uid, out var abilityComponent)) + { + if (abilityComponent.VampireTurned) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-vampire-turned"), + args.User, args.User, PopupType.Medium); + + return; + } + } + + _popup.PopupEntity(Loc.GetString("vampire-popup-ghoul-rite"), + args.User, args.User, PopupType.Medium); + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.VampireCure, + new VampireCureDoAfterEvent(), uid, target: uid) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true, + BlockDuplicate = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + private void OnCureVampire(Entity ent, ref VampireCureDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) return; + + VampireMindRemoved(ent); + } + + private void StartDrinking(EntityUid drinker, EntityUid target) + { + var doAfterArgs = new DoAfterArgs(EntityManager, drinker, TimeSpan.FromSeconds(2f), + new VampireDrinkingDoAfterEvent(), drinker, target: target) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + BlockDuplicate = true, + Hidden = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + private void StartConversion(EntityUid vampire, EntityUid target) + { + if (!TryComp(vampire, out var vamp)) + return; + + if (vamp.GhoulQuantity == vamp.MaxNumberGhouls) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-max-number-ghouls"), vampire, vampire, PopupType.Medium); + return; + } + + _popup.PopupEntity(Loc.GetString("vampire-verb-envelope-vampire-transform", + ("target", MetaData(target).EntityName)), + vampire, vampire, PopupType.Medium); + + _popup.PopupEntity(Loc.GetString("vampire-verb-envelope-ghoul-transform"), + target, target, PopupType.Medium); + + var doAfterArgs = new DoAfterArgs(EntityManager, vampire, vamp.ConversionGhoulTime, + new VampireEnvelopeDoAfterEvent(), vampire, target: target) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + BlockDuplicate = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + /// + /// обработчик DoAfter + /// + private void OnEnvelopeCompleteVampire(EntityUid uid, VampireComponent comp, ref VampireEnvelopeDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target is not { } target + || TryComp(target, out var ghoulComp)) + return; + + ConvertToGhoul(args.User, target); + args.Handled = true; + } + + /// + /// обработчик DoAfter для вампира + /// + private void OnDrinkingCompleteVampire(Entity ent, ref VampireDrinkingDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target is not { } target) + return; + + DrinkingComplete(args.User, target, ent.Comp.BloodPerTick); + args.Handled = true; + } + + private void DrinkingComplete(EntityUid drinker, EntityUid target, float amount) + { + // проверяем, есть ли кровь у цели + if (!TryComp(target, out var blood) || blood.BloodSolution == null || + !TryComp(blood.BloodSolution.Value.Owner, out var solutionComp)) + { + _popup.PopupEntity(Loc.GetString("vampire-drinking-no-blood"), drinker, drinker, PopupType.Medium); + return; + } + + var solution = solutionComp.Solution; + + if (solution.Volume.Float() < 20) + { + _popup.PopupEntity(Loc.GetString("vampire-drinking-no-blood"), drinker, drinker, PopupType.Medium); + return; + } + + TryComp(drinker, out var vamp); + TryComp(drinker, out var ghoul); + + var damage = new DamageSpecifier(); + + if (vamp == null && ghoul == null) + return; + + // вычисляем текущее количество крови + float currentBlood = vamp != null ? vamp.CritThreshold - vamp.BloodDamage : ghoul!.CritThreshold - ghoul.BloodDamage; + + if (currentBlood >= 100 || !HasComp(target) || !HasComp(target)) + { + if (vamp != null) + { + // мы просто засчитываем эту кровь в TotalDrunk, но BloodDamage не понижаем + if (currentBlood >= 100 && HasComp(target) && HasComp(target)) + { + vamp.TotalDrunk += amount; + _vampireSystem.SetBloodCounterAlert(drinker, vamp); + } + // мы просто понижаем BloodDamage, но в TotalDrunk не засчитываем + else if (!HasComp(target) || !HasComp(target)) + { + if (currentBlood >= 100) + { + _popup.PopupEntity(Loc.GetString("vampire-popup-target-no-mind-full"), drinker, drinker, PopupType.Medium); + return; + } + + vamp.BloodDamage = Math.Max(vamp.BloodDamage - amount / 2, 0); + _vampireSystem.SetBloodAlert(drinker, vamp); + _popup.PopupEntity(Loc.GetString("vampire-popup-target-no-mind"), drinker, drinker, PopupType.Medium); + } + + _audio.PlayPvs(vamp.DrinkSound, drinker); + + damage.DamageDict["Bloodloss"] = FixedPoint2.New(amount); + _damage.TryChangeDamage(target, damage); + + var eui = new VampireRequestedEui(drinker, EntityManager, _actions, _vampireSystem, _prototypeManager); + eui.GrantAbilities(drinker, vamp.SelectedSubgroup); + + // после того, как вампир выпивает кровь его глаза становятся красными + TrySetEntityEyeColor(drinker, Color.Red); + + if (_mobState.IsAlive(target)) + StartDrinking(drinker, target); + + return; + } + } + + // увеличиваем количество крови + if (vamp != null) + { + vamp.BloodDamage = Math.Max(vamp.BloodDamage - amount, 0f); + _vampireSystem.SetBloodCounterAlert(drinker, vamp); + _vampireSystem.SetBloodAlert(drinker, vamp); + vamp.TotalDrunk += amount; + _audio.PlayPvs(vamp.DrinkSound, drinker); + } + else if (ghoul != null) + { + if (ghoul.BloodDamage - amount < 0) + { + _popup.PopupEntity(Loc.GetString("vampire-drinking-full-blood"), drinker, drinker, PopupType.Medium); + return; + } + + if (!HasComp(target) || !HasComp(target)) + ghoul.BloodDamage = Math.Max(ghoul.BloodDamage - amount / 2, 0); + else + ghoul.BloodDamage = Math.Max(ghoul.BloodDamage - amount, 0f); + + _vampireSystem.SetGhoulBloodAlert(drinker, ghoul); + _audio.PlayPvs(ghoul.DrinkSound, drinker); + } + + // после того, как вампир выпивает кровь его глаза становятся красными + TrySetEntityEyeColor(drinker, Color.Red); + + // наносим жертве урон от кровопотери + damage.DamageDict["Bloodloss"] = FixedPoint2.New(amount * 2); + _damage.TryChangeDamage(target, damage); + + if (vamp != null) + { + var eui = new VampireRequestedEui(drinker, EntityManager, _actions, _vampireSystem, _prototypeManager); + eui.GrantAbilities(drinker, vamp.SelectedSubgroup); + } + + if (_mobState.IsAlive(target)) + StartDrinking(drinker, target); + } + + // если вампир сдох, то освобождаем упырей от проклятия + private void OnDead(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead) + return; + + foreach (var ghoul in ent.Comp.Ghouls) + { + if (!TryComp(ghoul, out var actor) || !TryComp(ghoul, out var ghoulComponent)) + continue; + + RemoveMindFromGhoul(ghoul); + _vampireSystem.SetGhoulBloodAlert(ghoul, ghoulComponent); + var comp = EnsureComp(ghoul); + + var transmitter = EnsureComp(ghoul); + transmitter.Channels.Remove(new ProtoId(comp.VampireRadioID)); + + var activeRadio = EnsureComp(ghoul); + activeRadio.Channels.Remove(new ProtoId(comp.VampireRadioID)); + + // обновляем данные у вампира + if (ent.Comp.Ghouls.Remove(ghoul)) + { + ent.Comp.GhoulQuantity = Math.Max(0, ent.Comp.GhoulQuantity - 1); + Dirty(ent, ent.Comp); + } + + var eui = new VampireDeadEui(); + _eui.OpenEui(eui, actor.PlayerSession); + RemComp(ghoul); + } + } + + // выдаем вампиру cooldown на обращение + public void AppealGhoulsCooldown(EntityUid uid) + { + if (!TryComp(uid, out var vampireComponent)) + return; + var comp = EnsureComp(uid); + + _statusEffects.TryAddStatusEffectDuration(uid, + vampireComponent.CooldownStatusEffectAppealGhouls, + out _, + comp.CooldownTimeAppealGhouls, + null); + } + + private void OnSelectingSubgroup(VampireSelectingSubgroupEvent args) + { + if (!TryComp(args.Performer, out var actor)) + return; + + if (!TryComp(args.Performer, out var vamp)) + return; + + var eui = new VampireRequestedEui(args.Performer, EntityManager, _actions, _vampireSystem, _prototypeManager); + _eui.OpenEui(eui, actor.PlayerSession); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + BaseUpdate(frameTime); + AbilitiesUpdate(); + } + + public void BaseUpdate(float frameTime) + { + var querySearch = EntityQueryEnumerator(); + while (querySearch.MoveNext(out var uid, out var comp)) + { + if (!TryComp(uid, out var vamp) && !TryComp(uid, out var ghoul)) + continue; + + comp.UpdateDelay += frameTime; + var priests = _lookup.GetEntitiesInRange(Transform(uid).Coordinates, 5).FirstOrNull(); + + if (priests == null || _mobState.IsDead(priests.Value)) + { + _alert.ClearAlert(uid, comp.AdjacentChaplainAlert); + comp.UpdateDelay = 0f; + continue; + } + + if (comp.UpdateDelay < 1) + continue; + + _damage.TryChangeDamage(uid, comp.DivineDamage); + _audio.PlayPvs(comp.DivineDamageSound, uid); + _popup.PopupEntity(Loc.GetString("vampire-popup-chaplain-closely"), uid, uid, PopupType.Medium); + _alert.ShowAlert(uid, comp.AdjacentChaplainAlert); + + comp.UpdateDelay = 0; + } + } + + private void OnMindRemoved(VampireMindRemovedEvent ev) + { + if (_entityManager.GetEntity(ev.Uid) != null) VampireMindRemoved((EntityUid)_entityManager.GetEntity(ev.Uid)!); + } + + public void VampireMindRemoved(EntityUid uid) + { + if (!_mind.TryGetMind(uid, out var mindId, out var mind)) + return; + + _roleSystem.MindRemoveRole((mindId, mind)); + + var comp = EnsureComp(uid); + if (!TryComp(uid, out var vampireComponent)) return; + + foreach (var ghoul in vampireComponent.Ghouls) + { + if (!TryComp(ghoul, out var ghoulComponent)) continue; + + RemoveMindFromGhoul(ghoul); + + // обновляем данные у вампира + if (vampireComponent.Ghouls.Remove(ghoul)) + { + vampireComponent.GhoulQuantity = Math.Max(0, vampireComponent.GhoulQuantity - 1); + Dirty(uid, vampireComponent); + } + + if (TryComp(ghoul, out var actor)) + { + var eui = new VampireDeadEui(); + _eui.OpenEui(eui, actor.PlayerSession); + } + } + + if (comp.HaloUid != null) QueueDel(comp.HaloUid); + if (comp.InvisibleIsActive) _vampireSystem.VampireInvisible(uid); + if (comp.ItemIssued) _vampireSystem.OnIssuingSword(uid); + + if (vampireComponent.GrantedActions.Count > 0) + { + foreach (var action in vampireComponent.GrantedActions) + { + _actions.RemoveAction(uid, action); + } + } + + RemComp(uid); + + _alert.ClearAlert(uid, comp.AdjacentChaplainAlert); + _vampireSystem.SetBloodCounterAlert(uid, vampireComponent); + _vampireSystem.SetBloodAlert(uid, vampireComponent); + if (vampireComponent.SelectingSubgroupActionEntity != null) _actions.RemoveAction(uid, vampireComponent.SelectingSubgroupActionEntity); + + RemComp(uid); + } + + private bool TrySetEntityEyeColor(EntityUid uid, Color eyeColor) + { + if (!_visualBodySystem.TryGatherMarkingsData(uid, null, out var profiles, out _, out var markings)) return false; + + var coloredProfile = profiles.ToDictionary(pair => pair.Key, pair => pair.Value with { EyeColor = eyeColor }); + _visualBodySystem.ApplyProfiles(uid, coloredProfile); + + return true; + } +} diff --git a/Content.Server/Imperial/XenoGenetcs/Genes/RespiratorGeneSystem.cs b/Content.Server/Imperial/XenoGenetcs/Genes/RespiratorGeneSystem.cs new file mode 100644 index 00000000000..d7ac14d4bf8 --- /dev/null +++ b/Content.Server/Imperial/XenoGenetcs/Genes/RespiratorGeneSystem.cs @@ -0,0 +1,39 @@ +using Content.Shared.Imperial.XenoGenetics; +using Robust.Shared.Prototypes; +using System.Linq; +using Content.Shared.Imperial.XenoGenetics.Genes.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Imperial.XenoGenetics.Components; +using Content.Server.Body.Components; + +namespace Content.Server.Imperial.XenoGenetics.Genes; + +public sealed class RespiratorGeneSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGeneInsert); + SubscribeLocalEvent(OnGeneWithdraw); + } + private void OnGeneInsert(EntityUid uid, RespiratorGeneComponent component, ref GeneInsertedEvent args) + { + if (!TryComp(args.Target, out var respirator)) + return; + + var gene = EnsureComp(args.Gene); + if (gene.GeneMultiplier * 100f >= component.GeneNeeded) + { + RemComp(args.Target); + component.Active = true; + } + } + private void OnGeneWithdraw(EntityUid uid, RespiratorGeneComponent component, ref GeneWithdrawnEvent args) + { + if (component.Active == true) + { + EnsureComp(args.Target); + component.Active = false; + } + } +} \ No newline at end of file diff --git a/Content.Server/Imperial/XenoGenetcs/ServerXenoGeneticsSystem.cs b/Content.Server/Imperial/XenoGenetcs/ServerXenoGeneticsSystem.cs new file mode 100644 index 00000000000..3044ba76f09 --- /dev/null +++ b/Content.Server/Imperial/XenoGenetcs/ServerXenoGeneticsSystem.cs @@ -0,0 +1,215 @@ +using Content.Shared.Imperial.XenoGenetics; +using Robust.Shared.Prototypes; +using System.Linq; +using Content.Shared.Imperial.XenoGenetics.Components; +using Content.Shared.DoAfter; +using Content.Shared.Hands; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Content.Shared.Containers; +using Content.Shared.Containers.ItemSlots; +using Robust.Shared.Audio.Systems; +using Robust.Shared.GameObjects; +using Content.Shared.Verbs; +using Microsoft.VisualBasic; +using System.Collections; +using System.Collections.Generic; + +namespace Content.Server.Imperial.XenoGenetics; + +public sealed class ServerXenoGeneticsSystem : SharedXenoGeneticsSystem +{ + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGeneInsert); + SubscribeLocalEvent(OnGeneWithdraw); + SubscribeLocalEvent(OnSplicerUse); + + SubscribeLocalEvent>(AddAltVerb); + SubscribeLocalEvent(OnCombinerInteractUsing); + } + private void OnSplicerUse(EntityUid uid, GeneSplicerComponent component, UseInHandEvent args) + { + switch (component.InsertMode) + { + case GeneSplicerMode.Insert: + component.InsertMode = GeneSplicerMode.Withdraw; + _popup.PopupEntity(Loc.GetString("gene-splicer-mode-withdraw"), args.User, PopupType.Small); + break; + + case GeneSplicerMode.Withdraw: + component.InsertMode = GeneSplicerMode.Insert; + _popup.PopupEntity(Loc.GetString("gene-splicer-mode-insert"), args.User, PopupType.Small); + break; + + default: + break; + } + args.Handled = true; + } + private void OnGeneInsert(EntityUid uid, GeneSplicerComponent comp, DoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + if (!TryComp(args.Target, out var slots)) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-error"), args.User, PopupType.Small); + args.Handled = true; + return; + } + + var targetContainer = _containerSystem.EnsureContainer(args.Target.Value, comp.EntityGeneContainerID); + var geneContainer = _containerSystem.GetContainer(uid, comp.GeneContainerID); + + var genesInstalled = new List(targetContainer.ContainedEntities); + if (genesInstalled.Any()) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-already-modified"), args.User, PopupType.Small); + args.Handled = true; + return; + } + + var toInsert = new List(geneContainer.ContainedEntities); + if (!toInsert.Any()) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-gene-not-inserted"), args.User, PopupType.Small); + args.Handled = true; + return; + } + var insertedEv = new GeneInsertedEvent(toInsert[0], args.Target.Value); + RaiseLocalEvent(toInsert[0], insertedEv); + foreach (var geneUid in toInsert) + { + _containerSystem.Insert(geneUid, targetContainer); + } + args.Handled = true; + } + private void OnGeneWithdraw(EntityUid uid, GeneSplicerComponent comp, DoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + if (!TryComp(args.Target, out var slots)) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-error"), args.User, PopupType.Small); + args.Handled = true; + return; + } + + var targetContainer = _containerSystem.EnsureContainer(args.Target.Value, comp.EntityGeneContainerID); + var geneContainer = _containerSystem.GetContainer(uid, comp.GeneContainerID); + + var genesInstalled = new List(targetContainer.ContainedEntities); + if (!genesInstalled.Any()) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-has-no-gene"), args.User, PopupType.Small); + args.Handled = true; + return; + } + + var toInsert = new List(geneContainer.ContainedEntities); + if (toInsert.Any()) + { + _popup.PopupEntity(Loc.GetString("gene-splicer-slot-filled"), args.User, PopupType.Small); + args.Handled = true; + return; + } + var withdrawEv = new GeneWithdrawnEvent(genesInstalled[0], args.Target.Value); + RaiseLocalEvent(genesInstalled[0], withdrawEv); + foreach (var geneUid in genesInstalled) + { + _containerSystem.Insert(geneUid, geneContainer); + } + + args.Handled = true; + } + private void OnCombinerInteractUsing(EntityUid uid, GeneCombinerComponent comp, ref InteractUsingEvent args) + { + var entToInsert = args.Used; + + var geneContainer = _containerSystem.GetContainer(uid, comp.GeneContainerID); + var geneContainerEnt = new List(geneContainer.ContainedEntities); + + if (!HasComp(entToInsert) || geneContainerEnt.Count() >= comp.MaxGenes) + return; + + _containerSystem.Insert(entToInsert, geneContainer); + + args.Handled = true; + } + private void AddAltVerb(EntityUid uid, GeneCombinerComponent comp, ref GetVerbsEvent args) + { + var geneContainer = _containerSystem.GetContainer(uid, comp.GeneContainerID); + var geneContainerEnt = new List(geneContainer.ContainedEntities); + + if (geneContainerEnt.Count < 2) + return; + + if (!args.CanAccess || !args.CanInteract || args.Hands == null || !geneContainerEnt.Any()) + return; + + var user = args.User; + var verb = new AlternativeVerb + { + Text = Loc.GetString("gene-combiner-start"), + Priority = 1, + Act = () => OnCombinerInteract(uid, comp, user) + }; + args.Verbs.Add(verb); + } + private void OnCombinerInteract(EntityUid uid, GeneCombinerComponent comp, EntityUid user) + { + var geneContainer = _containerSystem.GetContainer(uid, comp.GeneContainerID); + var geneContainerEnt = new List(geneContainer.ContainedEntities); + + if (geneContainerEnt.Count < 2) + return; + + var geneProtoID1 = MetaData(geneContainerEnt[0]).EntityPrototype; + var geneProtoID2 = MetaData(geneContainerEnt[1]).EntityPrototype; + + if (geneProtoID1 == null || geneProtoID2 == null) + return; + + if (!geneContainerEnt.Any() || geneContainerEnt.Count() > comp.MaxGenes || geneProtoID1 != geneProtoID2) + { + _containerSystem.EmptyContainer(geneContainer); + _audioSystem.PlayPvs(comp.DeclineSound, uid); + return; + } + + var geneComp1 = EnsureComp(geneContainerEnt[0]); + var geneComp2 = EnsureComp(geneContainerEnt[1]); + + var newGeneFloat = geneComp1.GeneMultiplier + geneComp2.GeneMultiplier; + if (newGeneFloat > 1f) + newGeneFloat = 1f; + + var geneProto = MetaData(geneContainerEnt[0]).EntityPrototype; + if (geneProto == null) + return; + + var newGeneEnt = Spawn(geneProto.ID, Transform(uid).Coordinates); + AddComp(newGeneEnt); + var geneComp = EnsureComp(newGeneEnt); + geneComp.GeneMultiplier = newGeneFloat; + + foreach (var e in geneContainerEnt) + { + QueueDel(e); + } + + _audioSystem.PlayPvs(comp.CompleteSound, uid); + var outputContainer = _containerSystem.GetContainer(uid, comp.GeneContainerIDOutput); + _containerSystem.Insert(newGeneEnt, outputContainer); + } +} diff --git a/Content.Server/Imperial/XxRaay/079/NDA079AirlockAbilitySystem.cs b/Content.Server/Imperial/XxRaay/079/NDA079AirlockAbilitySystem.cs new file mode 100644 index 00000000000..1dc329b721c --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079AirlockAbilitySystem.cs @@ -0,0 +1,175 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.Imperial.XxRaay.Nda079.Events; +using Content.Shared.Popups; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +public sealed class NDA079AirlockAbilitySystem : SharedNDA079AirlockAbilitySystem +{ + [Dependency] private readonly AlertEnergySystem _energySystem = default!; + [Dependency] private readonly SharedDoorSystem _doorSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly NDA079CpuSystem _cpuSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(OnAirlockAction); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + var curTime = _gameTiming.CurTime; + + while (query.MoveNext(out var uid, out var timer, out var bolt)) + { + if (curTime < timer.UnboltTime) + continue; + + _doorSystem.TrySetBoltDown((uid, bolt), false, null, predicted: false); + RemComp(uid); + } + } + + protected override void OnAirlockVerbAct(EntityUid user, EntityUid target) + { + } + + private void OnAirlockAction(NDA079AirlockActionEvent ev, EntitySessionEventArgs args) + { + var user = args.SenderSession.AttachedEntity; + if (user == null) + return; + + if (!TryGetEntity(ev.Target, out var target)) + return; + + if (!TryComp(user, out var abilityComp)) + return; + + if (!TryComp(user, out var energyComp)) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-failed"), user.Value, user.Value); + return; + } + + if (energyComp.Energy < abilityComp.EnergyCost) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-energyfailed", + ("EnergyCost", abilityComp.EnergyCost), + ("Energy", energyComp.Energy.ToString("F1"))), + user.Value, + user.Value); + return; + } + + var curTime = _gameTiming.CurTime; + if (abilityComp.LastUsedTime.HasValue) + { + var timeSinceLastUse = curTime - abilityComp.LastUsedTime.Value; + if (timeSinceLastUse < abilityComp.Cooldown) + { + var remaining = abilityComp.Cooldown - timeSinceLastUse; + _popup.PopupEntity(Loc.GetString("nda079-ability-cooldown", + ("remaining", remaining.TotalSeconds.ToString("F1"))), + user.Value, + user.Value); + return; + } + } + + if (!_random.Prob(abilityComp.SuccessChance)) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-failed"), user.Value, user.Value); + _energySystem.ModifyEnergy(user.Value, -abilityComp.EnergyCost, energyComp); + abilityComp.LastUsedTime = curTime; + Dirty(user.Value, abilityComp); + _cpuSystem.AddCpuPoint(user.Value); + + if (TryComp(user.Value, out var nda079CompFail)) + { + nda079CompFail.AirlockAbilityLastUsedTime = curTime; + Dirty(user.Value, nda079CompFail); + } + return; + } + + + _energySystem.ModifyEnergy(user.Value, -abilityComp.EnergyCost, energyComp); + abilityComp.LastUsedTime = curTime; + Dirty(user.Value, abilityComp); + _cpuSystem.AddCpuPoint(user.Value); + + if (TryComp(user.Value, out var nda079Comp)) + { + nda079Comp.AirlockAbilityLastUsedTime = curTime; + Dirty(user.Value, nda079Comp); + } + + var success = false; + switch (ev.ActionType) + { + case NDA079AirlockActionType.Toggle: + success = HandleToggleDoor(target.Value, user.Value); + break; + case NDA079AirlockActionType.Bolt: + success = HandleBoltDoor(target.Value, user.Value, abilityComp); + break; + } + + if (!success) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-failed"), user.Value, user.Value); + } + } + + private bool HandleToggleDoor(EntityUid door, EntityUid user) + { + if (!TryComp(door, out var doorComp)) + return false; + + var isOpen = doorComp.State == DoorState.Open || doorComp.State == DoorState.Opening; + + if (isOpen) + { + _doorSystem.StartClosing(door, doorComp, user, predicted: true); + return true; + } + + if (doorComp.State == DoorState.Closed || doorComp.State == DoorState.Closing || doorComp.State == DoorState.Denying) + { + _doorSystem.StartOpening(door, doorComp, user, predicted: true); + return true; + } + + return false; + } + + private bool HandleBoltDoor(EntityUid door, EntityUid user, NDA079AirlockAbilityComponent abilityComp) + { + if (!TryComp(door, out var boltComp)) + return false; + + if (!_doorSystem.TrySetBoltDown((door, boltComp), true, user, predicted: true)) + return false; + + var timer = EnsureComp(door); + timer.UnboltTime = _gameTiming.CurTime + abilityComp.BoltDuration; + timer.WasBolted = true; + Dirty(door, timer); + + return true; + } +} diff --git a/Content.Server/Imperial/XxRaay/079/NDA079CableAbilitySystem.cs b/Content.Server/Imperial/XxRaay/079/NDA079CableAbilitySystem.cs new file mode 100644 index 00000000000..41925e27237 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079CableAbilitySystem.cs @@ -0,0 +1,108 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.Imperial.XxRaay.Nda079.Events; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Popups; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +public sealed class NDA079CableAbilitySystem : SharedNDA079CableAbilitySystem +{ + [Dependency] private readonly AlertEnergySystem _energySystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnSpawnCableAction); + SubscribeLocalEvent(OnMindAdded); + } + + private void OnMindAdded(EntityUid uid, NDA079CableAbilityComponent comp, ref MindAddedMessage args) + { + if (!_mind.TryGetMind(uid, out var mindId, out _)) + return; + + var mindActionContainer = EnsureComp(mindId); + var actionProto = comp.ActionProto; + + if (HasActionInContainer(mindActionContainer, actionProto.ToString())) + return; + + var action = _actionContainer.AddAction(mindId, actionProto.ToString(), mindActionContainer); + if (action != null) + { + _actions.GrantContainedAction((uid, null), (mindId, mindActionContainer), action.Value); + } + } + + private bool HasActionInContainer(ActionsContainerComponent container, string actionProto) + { + foreach (var action in container.Container.ContainedEntities) + { + var protoId = MetaData(action).EntityPrototype?.ID; + if (protoId != null && protoId.Equals(actionProto)) + return true; + } + + return false; + } + + private void OnSpawnCableAction(Entity entity, ref NDA079SpawnCableActionEvent ev) + { + var user = entity.Owner; + var abilityComp = entity.Comp; + + if (!TryComp(user, out var energyComp)) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-notenergy"), user, user); + return; + } + + if (energyComp.Energy < abilityComp.EnergyCost) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-energyfailed", + ("EnergyCost", abilityComp.EnergyCost), + ("Energy", energyComp.Energy.ToString("F1"))), + user, + user); + return; + } + + var curTime = _gameTiming.CurTime; + if (abilityComp.LastUsedTime.HasValue) + { + var timeSinceLastUse = curTime - abilityComp.LastUsedTime.Value; + var cooldown = abilityComp.Cooldown; + if (timeSinceLastUse < cooldown) + { + var remaining = cooldown - timeSinceLastUse; + _popup.PopupEntity(Loc.GetString("nda079-ability-cooldown", + ("remaining", remaining.TotalSeconds.ToString("F1"))), + user, + user); + return; + } + } + + _energySystem.ModifyEnergy(user, -abilityComp.EnergyCost, energyComp); + abilityComp.LastUsedTime = curTime; + Dirty(user, abilityComp); + + var transform = Transform(user); + var userCoords = transform.Coordinates; + SpawnAtPosition(abilityComp.CableProto, userCoords); + + _popup.PopupEntity(Loc.GetString("nda079-ability-cable-success"), user, user); + } +} + diff --git a/Content.Server/Imperial/XxRaay/079/NDA079CpuSystem.cs b/Content.Server/Imperial/XxRaay/079/NDA079CpuSystem.cs new file mode 100644 index 00000000000..37d8d225942 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079CpuSystem.cs @@ -0,0 +1,176 @@ +using Content.Server.Chat.Managers; +using Content.Shared.Chat; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.Imperial.XxRaay.Nda079.Events; +using Content.Shared.Imperial.XxRaay.Components; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +/// +/// Система, считающая CPU поинты 079 и повышающая уровень. +/// +public sealed class NDA079CpuSystem : EntitySystem +{ + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private const int MessageIntervalMs = 500; + private const int PercentStep = 10; + + public override void Initialize() + { + base.Initialize(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + var curTime = _gameTiming.CurTime; + + while (query.MoveNext(out var uid, out var hacking)) + { + if (curTime < hacking.NextMessageTime) + continue; + + if (!_playerManager.TryGetSessionByEntity(uid, out var session)) + { + RemComp(uid); + continue; + } + + if (session.Status != SessionStatus.InGame) + { + RemComp(uid); + continue; + } + + if (hacking.CurrentPercent > 100) + { + var endText = Loc.GetString("nda079-cpu-endtext"); + _chatManager.ChatMessageToOne( + ChatChannel.Server, + endText, + endText, + EntityUid.Invalid, + false, + session.Channel, + Color.Red + ); + RemComp(uid); + continue; + } + + var msg = Loc.GetString("nda079-cpu-hacking", ("percent", hacking.CurrentPercent)); + _chatManager.ChatMessageToOne( + ChatChannel.Server, + msg, + msg, + EntityUid.Invalid, + false, + session.Channel, + Color.Red + ); + + hacking.CurrentPercent += PercentStep; + hacking.NextMessageTime = curTime + TimeSpan.FromMilliseconds(MessageIntervalMs); + Dirty(uid, hacking); + } + } + + public void AddCpuPoint(EntityUid user) + { + if (!TryComp(user, out var cpu)) + return; + + if (cpu.CurrentLevel >= cpu.LvlMax) + return; + + cpu.CurrentCpu += 1; + Dirty(user, cpu); + + CheckLevelUp(user, cpu); + } + + private void CheckLevelUp(EntityUid user, NDA079CpuComponent cpu) + { + var currentLevel = cpu.CurrentLevel; + if (currentLevel >= cpu.LvlMax) + return; + + if (currentLevel < 1 || currentLevel >= cpu.LevelRequirements.Length) + return; + + var required = cpu.LevelRequirements[currentLevel]; + if (cpu.CurrentCpu < required) + return; + + cpu.CurrentCpu = 0; + cpu.CurrentLevel += 1; + Dirty(user, cpu); + + HandleLevelUp(user, cpu.CurrentLevel); + } + + private void HandleLevelUp(EntityUid user, int newLevel) + { + ForceToStationaryAndReport(user, newLevel); + } + + private void ForceToStationaryAndReport(EntityUid user, int newLevel) + { + if (!TryComp(user, out var ndaComp)) + { + SendReport(user, newLevel); + return; + } + + if (!ndaComp.InAIVisionMode) + { + SendReport(user, newLevel); + return; + } + + var target = user; + + if (ndaComp.OriginalEntity is { } original && Exists(original)) + target = original; + + var ev = new NDA079ToggleVisionModeEvent(); + RaiseLocalEvent(user, ev); + + SendReport(target, newLevel); + } + + private void SendReport(EntityUid target, int newLevel) + { + if (!_playerManager.TryGetSessionByEntity(target, out var session)) + return; + + var startText = Loc.GetString("nda079-cpu-starttext", ("newLevel", newLevel)); + _chatManager.ChatMessageToOne( + ChatChannel.Server, + startText, + startText, + EntityUid.Invalid, + false, + session.Channel, + Color.Red + ); + + // Start the hacking sequence using a component-based approach + var hacking = EnsureComp(target); + hacking.CurrentPercent = 0; + hacking.NextMessageTime = _gameTiming.CurTime + TimeSpan.FromMilliseconds(MessageIntervalMs); + hacking.TargetLevel = newLevel; + Dirty(target, hacking); + } + +} + + diff --git a/Content.Server/Imperial/XxRaay/079/NDA079GeneratorSystem.cs b/Content.Server/Imperial/XxRaay/079/NDA079GeneratorSystem.cs new file mode 100644 index 00000000000..4b85bf1e97e --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079GeneratorSystem.cs @@ -0,0 +1,151 @@ +using Content.Server.NodeContainer.EntitySystems; +using Content.Server.Power.Nodes; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.NodeContainer; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +public sealed class NDA079GeneratorSystem : EntitySystem +{ + [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNDA079Startup); + SubscribeLocalEvent(OnGeneratorStartup); + SubscribeLocalEvent(OnGeneratorShutdown); + SubscribeLocalEvent(OnNodeGroupsRebuilt); + } + + private void OnNDA079Startup(Entity entity, ref ComponentStartup args) + { + UpdateGeneratorRegen(entity.Owner); + } + + private void OnGeneratorStartup(Entity entity, ref ComponentStartup args) + { + UpdateAllNDA079Regen(); + } + + private void OnGeneratorShutdown(Entity entity, ref ComponentShutdown args) + { + UpdateAllNDA079Regen(); + } + + private void OnNodeGroupsRebuilt(Entity entity, ref NodeGroupsRebuilt args) + { + if (HasComp(entity.Owner) || HasComp(entity.Owner)) + { + UpdateAllNDA079Regen(); + } + } + + public void RefreshRegen(EntityUid nda079Entity) + { + UpdateGeneratorRegen(nda079Entity); + } + + private void UpdateAllNDA079Regen() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + UpdateGeneratorRegen(uid); + } + } + + private void UpdateGeneratorRegen(EntityUid nda079Entity) + { + if (!TryComp(nda079Entity, out var nda079Comp)) + return; + + EntityUid? stationaryEntity = null; + EntityUid? activeEntity = null; + + if (nda079Comp.InAIVisionMode) + { + activeEntity = nda079Entity; + stationaryEntity = nda079Comp.OriginalEntity; + } + else + { + activeEntity = nda079Entity; + stationaryEntity = nda079Entity; + } + + if (activeEntity == null || !Exists(activeEntity.Value)) + return; + + if (stationaryEntity == null || !Exists(stationaryEntity.Value)) + { + ApplyRegenBonus(activeEntity.Value, 0f); + return; + } + + var totalBonus = GetConnectedGeneratorsBonus(stationaryEntity.Value); + ApplyRegenBonus(activeEntity.Value, totalBonus); + + if (nda079Comp.InAIVisionMode && nda079Comp.OriginalEntity != null && Exists(nda079Comp.OriginalEntity.Value)) + { + ApplyRegenBonus(nda079Comp.OriginalEntity.Value, totalBonus); + } + } + + private float GetConnectedGeneratorsBonus(EntityUid stationaryEntity) + { + if (!TryComp(stationaryEntity, out _)) + return 0f; + + if (!_nodeContainer.TryGetNode(stationaryEntity, "power", out var stationaryNode)) + return 0f; + + var stationaryNodeGroup = stationaryNode.NodeGroup; + if (stationaryNodeGroup == null) + return 0f; + + var totalBonus = 0f; + var generatorQuery = EntityQueryEnumerator(); + while (generatorQuery.MoveNext(out var generatorUid, out var generatorComp, out _)) + { + if (!_nodeContainer.TryGetNode(generatorUid, "output", out var generatorNode)) + continue; + + if (generatorNode.NodeGroup == stationaryNodeGroup) + { + totalBonus += generatorComp.RegenBonusPerGenerator; + } + } + + return totalBonus; + } + + private void ApplyRegenBonus(EntityUid entity, float bonus) + { + if (!TryComp(entity, out var energyComp)) + return; + + var baseRegen = GetBaseRegen(entity, energyComp); + energyComp.RegenPerSecond = baseRegen + bonus; + Dirty(entity, energyComp); + } + + private float GetBaseRegen(EntityUid uid, AlertEnergyComponent component) + { + var proto = MetaData(uid).EntityPrototype; + if (proto == null) + return component.RegenPerSecond; + + var compFactory = IoCManager.Resolve(); + var compName = compFactory.GetComponentName(); + if (!proto.Components.TryGetValue(compName, out var entry)) + return component.RegenPerSecond; + + if (entry.Component is not AlertEnergyComponent protoComponent) + return component.RegenPerSecond; + + return protoComponent.RegenPerSecond; + } +} diff --git a/Content.Server/Imperial/XxRaay/079/NDA079LightFlickerAbilitySystem.cs b/Content.Server/Imperial/XxRaay/079/NDA079LightFlickerAbilitySystem.cs new file mode 100644 index 00000000000..8297c1bd2a8 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079LightFlickerAbilitySystem.cs @@ -0,0 +1,199 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.Imperial.XxRaay.Nda079.Events; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Popups; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +public sealed class NDA079LightFlickerAbilitySystem : SharedNDA079LightFlickerAbilitySystem +{ + [Dependency] private readonly AlertEnergySystem _energySystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedPointLightSystem _lightSystem = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly NDA079CpuSystem _cpuSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnLightFlickerAction); + SubscribeLocalEvent(OnMindAdded); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + var curTime = _gameTiming.CurTime; + + while (query.MoveNext(out var uid, out var timer)) + { + if (curTime < timer.RestoreTime) + continue; + + if (_lightSystem.TryGetLight(uid, out var lightComp)) + { + _lightSystem.SetEnabled(uid, timer.WasEnabled, lightComp); + } + + RemComp(uid); + } + } + + private void OnMindAdded(Entity entity, ref MindAddedMessage args) + { + if (!_mind.TryGetMind(entity.Owner, out var mindId, out _)) + return; + + var mindActionContainer = EnsureComp(mindId); + var actionProto = entity.Comp.ActionProto; + + if (HasActionInContainer(mindActionContainer, actionProto.ToString())) + return; + + var action = _actionContainer.AddAction(mindId, actionProto.ToString(), mindActionContainer); + if (action != null) + { + _actions.GrantContainedAction((entity.Owner, null), (mindId, mindActionContainer), action.Value); + } + } + + private bool HasActionInContainer(ActionsContainerComponent container, string actionProto) + { + foreach (var action in container.Container.ContainedEntities) + { + var protoId = MetaData(action).EntityPrototype?.ID; + if (protoId != null && protoId.Equals(actionProto)) + return true; + } + + return false; + } + + private void OnLightFlickerAction(Entity entity, + ref NDA079LightFlickerActionEvent ev) + { + var user = entity.Owner; + var abilityComp = entity.Comp; + + if (!TryComp(user, out var energyComp)) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-notenergy"), user, user); + return; + } + + if (energyComp.Energy < abilityComp.EnergyCost) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-energyfailed", + ("EnergyCost", abilityComp.EnergyCost), + ("Energy", energyComp.Energy.ToString("F1"))), + user, + user); + return; + } + + var curTime = _gameTiming.CurTime; + if (abilityComp.LastUsedTime.HasValue) + { + var timeSinceLastUse = curTime - abilityComp.LastUsedTime.Value; + if (timeSinceLastUse < abilityComp.Cooldown) + { + var remaining = abilityComp.Cooldown - timeSinceLastUse; + _popup.PopupEntity(Loc.GetString("nda079-ability-cooldown", + ("remaining", remaining.TotalSeconds.ToString("F1"))), + user, + user); + return; + } + } + + var transform = Transform(user); + if (transform.MapID == MapId.Nullspace) + return; + + _energySystem.ModifyEnergy(user, -abilityComp.EnergyCost, energyComp); + abilityComp.LastUsedTime = curTime; + Dirty(user, abilityComp); + + _cpuSystem.AddCpuPoint(user); + + if (TryComp(user, out var nda079Comp)) + { + nda079Comp.LightFlickerLastUsedTime = curTime; + Dirty(user, nda079Comp); + } + + var userCoords = transform.Coordinates; + var entitiesInRange = _lookup.GetEntitiesInRange(userCoords, abilityComp.Radius); + + var lightsFound = new List(); + foreach (var entityInRange in entitiesInRange) + { + if (_lightSystem.TryGetLight(entityInRange, out _)) + { + lightsFound.Add(entityInRange); + } + } + + if (lightsFound.Count == 0) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-light-notlamp"), user, user); + return; + } + + var success = _random.Prob(abilityComp.SuccessChance); + + if (success) + { + _popup.PopupEntity(Loc.GetString("nda079-ability-light-success"), user, user); + + foreach (var lightEntity in lightsFound) + { + if (!_lightSystem.TryGetLight(lightEntity, out var lightComp)) + continue; + + var wasEnabled = lightComp.Enabled; + _lightSystem.SetEnabled(lightEntity, false, lightComp); + + var timer = EnsureComp(lightEntity); + timer.RestoreTime = curTime + abilityComp.LightOffDuration; + timer.WasEnabled = wasEnabled; + Dirty(lightEntity, timer); + } + } + else + { + _popup.PopupEntity(Loc.GetString("nda079-ability-failed"), user, user); + + foreach (var lightEntity in lightsFound) + { + if (!_lightSystem.TryGetLight(lightEntity, out var lightComp)) + continue; + + var wasEnabled = lightComp.Enabled; + _lightSystem.SetEnabled(lightEntity, false, lightComp); + + var timer = EnsureComp(lightEntity); + timer.RestoreTime = curTime + abilityComp.FlickerDuration; + timer.WasEnabled = wasEnabled; + Dirty(lightEntity, timer); + } + } + } +} + diff --git a/Content.Server/Imperial/XxRaay/079/NDA079System.cs b/Content.Server/Imperial/XxRaay/079/NDA079System.cs new file mode 100644 index 00000000000..f923db0021b --- /dev/null +++ b/Content.Server/Imperial/XxRaay/079/NDA079System.cs @@ -0,0 +1,262 @@ +using Content.Server.Mind; +using Content.Server.Speech.Components; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Nda079; +using Content.Shared.Imperial.XxRaay.Nda079.Events; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Speech; +using Content.Shared.Speech.Muting; +using Content.Server.Imperial.XxRaay.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Nda079; + +public sealed class NDA079System : EntitySystem +{ + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly SharedMindSystem _sharedMind = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly SpeechSystem _speech = default!; + [Dependency] private readonly AlertEnergySystem _energySystem = default!; + [Dependency] private readonly NDA079GeneratorSystem _generatorSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnToggleVisionMode); + SubscribeLocalEvent(OnMindAdded); + } + + private void OnMindAdded(EntityUid uid, NDA079Component comp, ref MindAddedMessage args) + { + if (!_sharedMind.TryGetMind(uid, out var mindId, out _)) + return; + + var action = EnsureToggleActionInMind(mindId, comp.ToggleActionProto); + GrantActionIfPresent(uid, mindId, action); + } + + private void OnToggleVisionMode(EntityUid uid, NDA079Component comp, ref NDA079ToggleVisionModeEvent args) + { + if (args.Handled) + return; + + if (!comp.InAIVisionMode) + { + if (!_sharedMind.TryGetMind(uid, out var mindId, out var mind)) + return; + + var userId = mind.UserId; + if (userId == null) + return; + + var proto = comp.AIVisionFlyingEntityProto; + var coords = Transform(uid).Coordinates; + var newb = SpawnAtPosition(proto, coords); + var newcomp = EnsureComp(newb); + + newcomp.InAIVisionMode = true; + newcomp.OriginalEntity = uid; + newcomp.AIVisionEntity = null; + newcomp.OriginalEntityProto = MetaData(uid).EntityPrototype?.ID; + Dirty(newb, newcomp); + + comp.InAIVisionMode = true; + comp.AIVisionEntity = newb; + comp.OriginalEntity = uid; + comp.OriginalEntityProto = MetaData(uid).EntityPrototype?.ID; + Dirty(uid, comp); + + EnsureComp(newb); + _speech.SetSpeech(newb, true); + + if (TryComp(uid, out var sourceEnergy)) + { + if (TryComp(newb, out var targetEnergy)) + { + _energySystem.SetEnergy(newb, sourceEnergy.Energy, targetEnergy); + } + } + + if (TryComp(uid, out var originalCpu)) + { + var newCpu = EnsureComp(newb); + newCpu.CurrentLevel = originalCpu.CurrentLevel; + newCpu.CurrentCpu = originalCpu.CurrentCpu; + Dirty(newb, newCpu); + } + + if (TryComp(uid, out var cpu)) + { + var level = cpu.CurrentLevel; + + if (TryComp(newb, out var lightFlickerComp)) + { + lightFlickerComp.LastUsedTime = comp.LightFlickerLastUsedTime; + + if (_prototype.TryIndex($"NDA079LightFlickerLevel{level}", out var lightProto)) + { + lightFlickerComp.LightOffDuration = lightProto.LightOffDuration; + lightFlickerComp.Radius = lightProto.Radius; + lightFlickerComp.SuccessChance = lightProto.SuccessChance; + lightFlickerComp.Cooldown = lightProto.Cooldown; + } + + Dirty(newb, lightFlickerComp); + } + + if (TryComp(newb, out var airlockComp)) + { + airlockComp.LastUsedTime = comp.AirlockAbilityLastUsedTime; + + if (_prototype.TryIndex($"NDA079AirlockLevel{level}", out var airlockProto)) + { + airlockComp.BoltDuration = airlockProto.BoltDuration; + airlockComp.SuccessChance = airlockProto.SuccessChance; + } + + Dirty(newb, airlockComp); + } + } + + _actions.RemoveProvidedActions(uid, mindId); + + _mind.WipeMind(mindId); + var newMind = _mind.CreateMind(userId.Value, MetaData(uid).EntityName); + _sharedMind.SetUserId(newMind, userId.Value); + + _mind.TransferTo(newMind, newb); + var action = EnsureToggleActionInMind(newMind, comp.ToggleActionProto); + GrantActionIfPresent(newb, newMind, action); + + if (TryComp(newb, out var newLightFlickerComp)) + { + var protoId = newLightFlickerComp.ActionProto.ToString(); + var lightFlickerAction = EnsureActionInMind(newMind, protoId); + GrantActionIfPresent(newb, newMind, lightFlickerAction); + } + + _generatorSystem.RefreshRegen(newb); + } + else + { + if (!_sharedMind.TryGetMind(uid, out var mindId, out var mind)) + return; + + var userId = mind.UserId; + if (userId == null) + return; + + var originalEntity = comp.OriginalEntity; + if (originalEntity == null || !Exists(originalEntity.Value)) + return; + + var originalEntityValue = originalEntity.Value; + + if (!TryComp(originalEntityValue, out var originalComp)) + return; + + originalComp.InAIVisionMode = false; + originalComp.AIVisionEntity = null; + originalComp.OriginalEntity = null; + Dirty(originalEntityValue, originalComp); + + RemComp(originalEntityValue); + RemComp(originalEntityValue); + _speech.SetSpeech(originalEntityValue, true); + + if (TryComp(uid, out var sourceEnergy)) + { + if (TryComp(originalEntityValue, out var targetEnergy)) + { + _energySystem.SetEnergy(originalEntityValue, sourceEnergy.Energy, targetEnergy); + } + } + + if (TryComp(uid, out var flyingCpu)) + { + var statCpu = EnsureComp(originalEntityValue); + statCpu.CurrentLevel = flyingCpu.CurrentLevel; + statCpu.CurrentCpu = flyingCpu.CurrentCpu; + Dirty(originalEntityValue, statCpu); + } + + if (TryComp(uid, out var lightFlickerComp)) + { + originalComp.LightFlickerLastUsedTime = lightFlickerComp.LastUsedTime; + Dirty(originalEntityValue, originalComp); + } + + if (TryComp(uid, out var airlockComp)) + { + originalComp.AirlockAbilityLastUsedTime = airlockComp.LastUsedTime; + Dirty(originalEntityValue, originalComp); + } + + _actions.RemoveProvidedActions(uid, mindId); + + _mind.WipeMind(mindId); + var newMind = _mind.CreateMind(userId.Value, MetaData(originalEntityValue).EntityName); + _sharedMind.SetUserId(newMind, userId.Value); + + _mind.TransferTo(newMind, originalEntityValue); + var action = EnsureToggleActionInMind(newMind, originalComp.ToggleActionProto); + GrantActionIfPresent(originalEntityValue, newMind, action); + + Del(uid); + + _generatorSystem.RefreshRegen(originalEntityValue); + } + + args.Handled = true; + } + + private EntityUid? EnsureToggleActionInMind(EntityUid mindId, EntProtoId actionProto) + { + var mindActionContainer = EnsureComp(mindId); + if (HasActionInContainer(mindActionContainer, actionProto)) + return null; + + return _actionContainer.AddAction(mindId, actionProto.ToString(), mindActionContainer); + } + + private EntityUid? EnsureActionInMind(EntityUid mindId, string actionProto) + { + var mindActionContainer = EnsureComp(mindId); + if (HasActionInContainer(mindActionContainer, actionProto)) + return null; + + return _actionContainer.AddAction(mindId, actionProto, mindActionContainer); + } + + private bool HasActionInContainer(ActionsContainerComponent container, EntProtoId actionProto) + { + return HasActionInContainer(container, actionProto.ToString()); + } + + private bool HasActionInContainer(ActionsContainerComponent container, string actionProto) + { + foreach (var action in container.Container.ContainedEntities) + { + var protoId = MetaData(action).EntityPrototype?.ID; + if (protoId != null && protoId.Equals(actionProto)) + return true; + } + + return false; + } + + private void GrantActionIfPresent(EntityUid performer, EntityUid mindId, EntityUid? action) + { + if (action == null) + return; + + var container = EnsureComp(mindId); + _actions.GrantContainedAction((performer, null), (mindId, container), action.Value); + } +} diff --git a/Content.Server/Imperial/XxRaay/Components/AllAtOnceWormVentSpawnRuleComponent.cs b/Content.Server/Imperial/XxRaay/Components/AllAtOnceWormVentSpawnRuleComponent.cs new file mode 100644 index 00000000000..c02d8376fe5 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Components/AllAtOnceWormVentSpawnRuleComponent.cs @@ -0,0 +1,24 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Components; + +[RegisterComponent, Access(typeof(AllAtOnceWormVentSpawnRule))] +public sealed partial class AllAtOnceWormVentSpawnRuleComponent : Component +{ + [DataField] + public EntProtoId Prototype = "MobWormTier1"; + + [DataField] + public int Count = 6; + + [DataField] + public LocId? StartAnnouncement = "station-event-worm-vent-spawn-start-announcement"; + + [DataField] + public Color StartAnnouncementColor = Color.Gold; + + [DataField] + public SoundSpecifier? StartAudio = new SoundPathSpecifier("/Audio/Announcements/aliens.ogg"); +} diff --git a/Content.Server/Imperial/XxRaay/Components/MessageOnMindComponent.cs b/Content.Server/Imperial/XxRaay/Components/MessageOnMindComponent.cs new file mode 100644 index 00000000000..c24e834ccc6 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Components/MessageOnMindComponent.cs @@ -0,0 +1,17 @@ +namespace Content.Server.Imperial.XxRaay.Components; + +/// +/// Компонент для сообщения при добавлении сознания сущности +/// +[RegisterComponent] +public sealed partial class MessageOnMindComponent : Component +{ + /// + /// Текст сообщения + /// + [DataField("message")] + public string Message = string.Empty; + + [ViewVariables] + public bool SentMessage = false; +} diff --git a/Content.Server/Imperial/XxRaay/Components/OrbitalStrikePodComponent.cs b/Content.Server/Imperial/XxRaay/Components/OrbitalStrikePodComponent.cs new file mode 100644 index 00000000000..0f90da62464 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Components/OrbitalStrikePodComponent.cs @@ -0,0 +1,29 @@ +using Content.Server.Imperial.XxRaay.Systems; + +namespace Content.Server.Imperial.XxRaay.Components; + +/// +/// Marker component for supply pods created by orbital strike. +/// +[RegisterComponent, Access(typeof(OrbitalStrikeSystem))] +public sealed partial class OrbitalStrikePodComponent : Component +{ + /// + /// Explosion intensity. + /// + [DataField] + public float ExplosionIntensity = 100f; + + /// + /// Explosion slope. + /// + [DataField] + public float ExplosionSlope = 1f; + + /// + /// Explosion max tile intensity. + /// + [DataField] + public float ExplosionMaxTileIntensity = 12f; +} + diff --git a/Content.Server/Imperial/XxRaay/Components/WormDoorHidingComponent.cs b/Content.Server/Imperial/XxRaay/Components/WormDoorHidingComponent.cs new file mode 100644 index 00000000000..670c0d9cfb3 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Components/WormDoorHidingComponent.cs @@ -0,0 +1,40 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Robust.Shared.GameObjects; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Imperial.XxRaay.Components; + +[RegisterComponent] +[Access(typeof(WormDoorHideSystem))] +public sealed partial class WormDoorHidingComponent : Component +{ + [ViewVariables] + public EntityUid SourceDoor; + + [ViewVariables] + public bool WasCollidable = true; + + [ViewVariables] + public bool AddedStealth; + + [ViewVariables] + public bool PreviousStealthEnabled; + + [ViewVariables] + public float PreviousStealthVisibility = 1f; + + [ViewVariables] + public bool IgnoreNextMove; + + [ViewVariables] + public bool AppliedVisibility; + + [ViewVariables] + public bool AddedVisibility; + + [ViewVariables] + public ushort PreviousVisibilityLayer; + + [ViewVariables] + public bool Exiting; +} diff --git a/Content.Server/Imperial/XxRaay/Components/WormVentSpawnRuleComponent.cs b/Content.Server/Imperial/XxRaay/Components/WormVentSpawnRuleComponent.cs new file mode 100644 index 00000000000..c851f1bda0c --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Components/WormVentSpawnRuleComponent.cs @@ -0,0 +1,14 @@ +using Content.Server.Imperial.XxRaay.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Components; + +[RegisterComponent, Access(typeof(WormVentSpawnRule))] +public sealed partial class WormVentSpawnRuleComponent : Component +{ + [DataField] + public EntProtoId Prototype = "MobWormTier1"; + + [DataField] + public int Count = 6; +} diff --git a/Content.Server/Imperial/XxRaay/FactionShop/FactionShopSystem.cs b/Content.Server/Imperial/XxRaay/FactionShop/FactionShopSystem.cs index 293830dc881..d7194861858 100644 --- a/Content.Server/Imperial/XxRaay/FactionShop/FactionShopSystem.cs +++ b/Content.Server/Imperial/XxRaay/FactionShop/FactionShopSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Server.Store.Systems; using Content.Shared.Imperial.XxRaay.FactionShop; +using Content.Shared.Imperial.Helpers; using Content.Shared.Imperial.XxRaay.FlagSystem; using Content.Shared.Interaction.Events; using Content.Shared.NPC.Components; @@ -13,6 +14,7 @@ using Robust.Shared.Timing; using Robust.Shared.Localization; using System.Linq; +using Content.Shared.Chat; namespace Content.Server.Imperial.XxRaay.FactionShop; diff --git a/Content.Server/Imperial/XxRaay/FlagSystem/FlagCaptureSystem.cs b/Content.Server/Imperial/XxRaay/FlagSystem/FlagCaptureSystem.cs index 575890f8bf8..1467877587e 100644 --- a/Content.Server/Imperial/XxRaay/FlagSystem/FlagCaptureSystem.cs +++ b/Content.Server/Imperial/XxRaay/FlagSystem/FlagCaptureSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Mind; +using Content.Shared.Imperial.Helpers; using Content.Shared.Imperial.XxRaay.FlagSystem; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -18,6 +19,7 @@ using Content.Shared.DoAfter; using Robust.Shared.GameStates; +using System.Collections.Generic; using System.Linq; using Robust.Shared.Serialization; using Robust.Shared.Localization; @@ -31,6 +33,7 @@ public sealed class FlagCaptureSystem : SharedFlagCaptureSystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; public override void Initialize() { @@ -82,8 +85,8 @@ public override void Update(float frameTime) continue; // Игнорируем нейтральных игроков - var playerFaction = GetPlayerFaction(entity); - if (playerFaction == "NeutralFaction") + var playerFaction = GetPlayerFaction(entity, capture.NeutralFactionId); + if (playerFaction == capture.NeutralFactionId) continue; playersInRange.Add(entity); @@ -91,7 +94,7 @@ public override void Update(float frameTime) // Анализируем фракции игроков в радиусе var factionGroups = playersInRange - .GroupBy(player => GetPlayerFaction(player)) + .GroupBy(player => GetPlayerFaction(player, capture.NeutralFactionId)) .ToList(); // Никого нет — отменяем захват, выходим @@ -144,7 +147,7 @@ public override void Update(float frameTime) private void StartCapture(EntityUid flagUid, FlagCaptureComponent capture, EntityUid player) { // Проверяем, не пытается ли игрок захватить свой же флаг - var playerFaction = GetPlayerFaction(player); + var playerFaction = GetPlayerFaction(player, capture.NeutralFactionId); var currentFlagFaction = GetFlagFaction(flagUid); if (playerFaction == currentFlagFaction) @@ -158,22 +161,40 @@ private void StartCapture(EntityUid flagUid, FlagCaptureComponent capture, Entit var playerName = MetaData(player).EntityName; - // Запускаем DoAfter + if (capture.ActiveCaptureDoAfter != null) + { + _doAfter.Cancel(capture.ActiveCaptureDoAfter); + capture.ActiveCaptureDoAfter = null; + } + var doAfter = new DoAfterArgs(_entityManager, player, capture.CaptureTime, new FlagCaptureDoAfterEvent(), flagUid) { - BreakOnMove = false, + BreakOnMove = false, BreakOnDamage = true, - NeedHand = false + NeedHand = false, + DistanceThreshold = capture.CaptureRadius + 0.5f, // даём небольшой запас, чтобы сгладить сетевые рывки + Target = flagUid }; - if (!_entityManager.System().TryStartDoAfter(doAfter, out var doAfterId)) + if (!_doAfter.TryStartDoAfter(doAfter, out var doAfterId)) { capture.IsBeingCaptured = false; + Dirty(flagUid, capture); + return; } + + capture.ActiveCaptureDoAfter = doAfterId; + Dirty(flagUid, capture); } private void CancelCapture(EntityUid flagUid, FlagCaptureComponent capture) { + if (capture.ActiveCaptureDoAfter != null) + { + _doAfter.Cancel(capture.ActiveCaptureDoAfter); + capture.ActiveCaptureDoAfter = null; + } + capture.IsBeingCaptured = false; capture.CaptureProgress = TimeSpan.Zero; capture.LastCheckTime = TimeSpan.Zero; @@ -186,7 +207,13 @@ private void CancelCapture(EntityUid flagUid, FlagCaptureComponent capture) private void CompleteCapture(EntityUid flagUid, FlagCaptureComponent capture, EntityUid player) { - var playerFaction = GetPlayerFaction(player); + if (!IsCaptureStillValid(flagUid, capture, player)) + { + CancelCapture(flagUid, capture); + return; + } + + var playerFaction = GetPlayerFaction(player, capture.NeutralFactionId); var playerName = MetaData(player).EntityName; capture.CanBeCaptured = false; @@ -195,7 +222,7 @@ private void CompleteCapture(EntityUid flagUid, FlagCaptureComponent capture, En capture.LastCheckTime = TimeSpan.Zero; // Получаем позицию и поворот старого флага - var transform = _entityManager.GetComponent(flagUid); + var transform = Transform(flagUid); var position = transform.Coordinates; @@ -234,6 +261,8 @@ private void CompleteCapture(EntityUid flagUid, FlagCaptureComponent capture, En private void OnDoAfter(EntityUid uid, FlagCaptureComponent component, DoAfterEvent args) { + component.ActiveCaptureDoAfter = null; + if (args.Cancelled) { CancelCapture(uid, component); @@ -248,7 +277,53 @@ private void OnDoAfter(EntityUid uid, FlagCaptureComponent component, DoAfterEve CompleteCapture(uid, component, args.Args.User); } - private string GetPlayerFaction(EntityUid player) + private bool IsCaptureStillValid(EntityUid flagUid, FlagCaptureComponent capture, EntityUid player) + { + if (!_entityManager.EntityExists(player)) + return false; + + if (!_entityManager.TryGetComponent(player, out var playerMob)) + return false; + + if (playerMob.CurrentState == MobState.Dead || playerMob.CurrentState == MobState.Critical) + return false; + + var playerFaction = GetPlayerFaction(player, capture.NeutralFactionId); + if (playerFaction == capture.NeutralFactionId) + return false; + + var flagTransform = Transform(flagUid); + + var factions = new HashSet(); + var playerInRange = false; + + foreach (var entity in _lookup.GetEntitiesInRange(flagTransform.Coordinates, capture.CaptureRadius, LookupFlags.Dynamic | LookupFlags.Approximate)) + { + if (!_entityManager.TryGetComponent(entity, out var _)) + continue; + + if (!_entityManager.TryGetComponent(entity, out var mobState)) + continue; + + if (mobState.CurrentState == MobState.Dead || mobState.CurrentState == MobState.Critical) + continue; + + var faction = GetPlayerFaction(entity, capture.NeutralFactionId); + if (faction == capture.NeutralFactionId) + continue; + + factions.Add(faction); + + if (entity == player) + playerInRange = true; + } + + return playerInRange && + factions.Count == 1 && + factions.Contains(playerFaction); + } + + private string GetPlayerFaction(EntityUid player, string neutralFaction) { // Проверяем, есть ли у игрока компонент NpcFactionMember if (_entityManager.TryGetComponent(player, out var factionMember)) @@ -261,7 +336,7 @@ private string GetPlayerFaction(EntityUid player) } // Если у игрока нет фракции, возвращаем нейтральную - return "NeutralFaction"; + return neutralFaction; } private string GetFlagFaction(EntityUid flagUid) diff --git a/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRedemptionSystem.cs b/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRedemptionSystem.cs index f82c0fbfb9c..c9269796d5e 100644 --- a/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRedemptionSystem.cs +++ b/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRedemptionSystem.cs @@ -1,5 +1,4 @@ using Content.Shared.Interaction; -using Robust.Server.GameObjects; using Content.Shared.Popups; using Robust.Shared.Player; using Content.Server.Traitor.Uplink; @@ -12,9 +11,10 @@ namespace Content.Server.Imperial.XxRaay.SyndieBattle; public sealed class SyndieBattleRedemptionSystem : EntitySystem { [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + private readonly EntProtoId _telecrystalId = "Telecrystal1"; + public override void Initialize() { base.Initialize(); @@ -46,7 +46,7 @@ private void OnInteractUsing(Entity ent, ref In { if (store.Balance.TryGetValue(UplinkSystem.TelecrystalCurrencyPrototype, out var bal)) { - var value = (double)bal; + var value = (double)bal; payout = (int)Math.Floor(value * 0.45); } } @@ -56,7 +56,7 @@ private void OnInteractUsing(Entity ent, ref In for (var i = 0; i < payout; i++) { - if (!_prototype.TryIndex("Telecrystal1", out var itemProto)) + if (!_prototype.TryIndex(_telecrystalId, out var itemProto)) continue; Spawn(itemProto.ID, spawnPos); @@ -70,6 +70,6 @@ private void OnInteractUsing(Entity ent, ref In args.User); args.Handled = true; - EntityManager.DeleteEntity(args.Used); + QueueDel(args.Used); } } diff --git a/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRuleSystem.cs b/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRuleSystem.cs index 38e656a5c16..278d3abfc64 100644 --- a/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRuleSystem.cs +++ b/Content.Server/Imperial/XxRaay/SyndieBattle/SyndieBattleRuleSystem.cs @@ -54,8 +54,6 @@ public sealed class SyndieBattleRuleSystem : GameRuleSystem _energyQuery; + + public override void Initialize() + { + _energyQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + } + + private void OnStartup(EntityUid uid, AlertEnergyComponent component, ref ComponentStartup args) + { + UpdateEnergyAlert(uid, component); + } + + private void OnShutdown(EntityUid uid, AlertEnergyComponent component, ref ComponentShutdown args) + { + _alerts.ClearAlert(uid, component.AlertId); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out var uid, out var comp)) + { + if (comp.RegenPerSecond <= 0f || comp.Energy >= comp.MaxEnergy) + continue; + + var delta = comp.RegenPerSecond * frameTime; + SetEnergy(uid, comp.Energy + delta, comp); + } + } + + public void SetEnergy(EntityUid uid, float value, AlertEnergyComponent? component = null) + { + if (!_energyQuery.Resolve(uid, ref component)) + return; + + var clamped = Math.Clamp(value, 0f, component.MaxEnergy); + if (component.Energy == clamped) + return; + + component.Energy = clamped; + Dirty(uid, component); + UpdateEnergyAlert(uid, component); + } + + public void ModifyEnergy(EntityUid uid, float delta, AlertEnergyComponent? component = null) + { + if (!_energyQuery.Resolve(uid, ref component)) + return; + + SetEnergy(uid, component.Energy + delta, component); + } + + private void UpdateEnergyAlert(EntityUid uid, AlertEnergyComponent component) + { + var step = component.Step <= 0 ? 1f : component.Step; + var maxSeverity = (short) Math.Max(0, (int) Math.Floor(component.MaxEnergy / step)); + var severity = (short) Math.Clamp((int) Math.Floor(component.Energy / step), 0, maxSeverity); + + _alerts.UpdateAlert(uid, component.AlertId, severity); + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/CameraRelayChatSystem.cs b/Content.Server/Imperial/XxRaay/Systems/CameraRelayChatSystem.cs new file mode 100644 index 00000000000..facb6803478 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/CameraRelayChatSystem.cs @@ -0,0 +1,95 @@ +using Content.Server.Chat.Systems; +using Content.Server.Radio.EntitySystems; +using Content.Server.Speech; +using Content.Shared.Chat; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Speech; +using Content.Shared.StationAi; +using Robust.Shared.Map; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class CameraRelayChatSystem : EntitySystem +{ + [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly ChatSystem _chat = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnTransformSpeakerName); + SubscribeLocalEvent(OnTransformSpeech, after: new[] { typeof(AccentSystem) }); + SubscribeLocalEvent(OnEntitySpoke, before: new[] { typeof(HeadsetSystem) }); + } + + private void OnTransformSpeakerName(EntityUid uid, CameraRelayChatComponent component, ref TransformSpeakerNameEvent args) + { + if (!component.Enabled || args.Sender != uid) + return; + + if (!TryGetClosestVision(uid, component, out var closest)) + return; + + args.VoiceName = Name(closest); + } + + private void OnEntitySpoke(EntityUid uid, CameraRelayChatComponent component, EntitySpokeEvent args) + { + if (!component.Enabled) + return; + + if (args.Channel != null && args.ObfuscatedMessage != null) + { + args.Channel = null; + } + } + + private void OnTransformSpeech(TransformSpeechEvent args) + { + if (!TryComp(args.Sender, out CameraRelayChatComponent? relay) || !relay.Enabled) + return; + + if (args.Message.StartsWith('.') || args.Message.StartsWith(';') || args.Message.StartsWith(':')) + return; + + if (!TryGetClosestVision(args.Sender, relay, out var camera)) + return; + + EnsureComp(camera); + _chat.TrySendInGameICMessage(camera, args.Message, InGameICChatType.Speak, ChatTransmitRange.Normal, checkRadioPrefix: false, ignoreActionBlocker: true); + + args.Message = string.Empty; + } + + private bool TryGetClosestVision(EntityUid source, CameraRelayChatComponent relay, out EntityUid closest) + { + closest = default; + + var xformQuery = GetEntityQuery(); + if (!xformQuery.TryGetComponent(source, out var sourceXform)) + return false; + + var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery); + var maxRangeSq = relay.MaxRange > 0 ? relay.MaxRange * relay.MaxRange : float.PositiveInfinity; + + var closestDist = float.PositiveInfinity; + + var cameraQuery = EntityQueryEnumerator(); + while (cameraQuery.MoveNext(out var visionUid, out var vision, out var xform)) + { + if (!vision.Enabled) + continue; + + if (xform.MapID != sourceXform.MapID || xform.MapID == MapId.Nullspace) + continue; + + var distance = (_xforms.GetWorldPosition(xform, xformQuery) - sourcePos).LengthSquared(); + if (distance > maxRangeSq || distance >= closestDist) + continue; + + closest = visionUid; + closestDist = distance; + } + + return closest != default; + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/GroqApiSchemas.cs b/Content.Server/Imperial/XxRaay/Systems/GroqApiSchemas.cs new file mode 100644 index 00000000000..b5192e2f819 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/GroqApiSchemas.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Content.Server.Imperial.XxRaay.Systems; + +internal sealed class GroqApiResponse +{ + [JsonPropertyName("choices")] + public GroqApiChoice[]? Choices { get; set; } +} + +internal sealed class GroqApiChoice +{ + [JsonPropertyName("message")] + public GroqApiMessage? Message { get; set; } +} + +internal sealed class GroqApiMessage +{ + [JsonPropertyName("content")] + public string? Content { get; set; } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/LLMNPCSystem.cs b/Content.Server/Imperial/XxRaay/Systems/LLMNPCSystem.cs new file mode 100644 index 00000000000..54c6af83dd2 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/LLMNPCSystem.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Content.Server.Chat.Systems; +using Content.Server.Mind; +using Content.Server.Roles.Jobs; +using Content.Shared.Chat; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.IdentityManagement; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.DataDefinitions; +using Content.Shared.Inventory; +using Content.Shared.Mobs.Components; +using Content.Shared.Speech; +using Content.Shared.Speech.Components; +using Robust.Shared.Localization; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using Content.Shared.Damage.Systems; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class LLMNPCSystem : EntitySystem +{ + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly JobSystem _jobs = default!; + [Dependency] private readonly MindSystem _minds = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + + private readonly HttpClient _httpClient = new(); + private ISawmill _sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + _sawmill = Logger.GetSawmill("llmnpc"); + SubscribeLocalEvent(OnListen); + SubscribeLocalEvent(OnComponentInit); + } + + private void OnComponentInit(EntityUid uid, LLMNPCComponent component, ComponentInit args) + { + component.IsGenerating = false; + + var listener = EnsureComp(uid); + listener.Range = component.MaxDistanceTiles; + + if (string.IsNullOrWhiteSpace(component.NPCName)) + { + component.NPCName = MetaData(uid).EntityName; + } + } + + private string GetDefaultSystemPrompt(LLMNPCComponent component) + { + var name = string.IsNullOrWhiteSpace(component.NPCName) ? "NPC" : component.NPCName; + return Loc.GetString("llmnpc-default-system-prompt", ("name", name)); + } + + private void OnListen(Entity ent, ref ListenEvent args) + { + var npcUid = ent.Owner; + var npcComp = ent.Comp; + + if (npcUid == args.Source) + return; + + if (string.IsNullOrWhiteSpace(args.Message)) + return; + + if (npcComp.IsGenerating) + return; + + if (string.IsNullOrEmpty(npcComp.ApiKey)) + return; + + var userMessage = args.Message; + if (npcComp.IncludeContextInfo) + { + var contextInfo = GetContextInfo(args.Source, npcUid, npcComp); + if (!string.IsNullOrWhiteSpace(contextInfo)) + { + userMessage = Loc.GetString("llmnpc-context-message", + ("context", contextInfo), + ("message", args.Message)); + } + } + + npcComp.MessageHistory.Add(new LLMMessageHistoryItem { Role = "user", Content = userMessage }); + + while (npcComp.MessageHistory.Count > npcComp.MaxHistoryMessages) + { + npcComp.MessageHistory.RemoveAt(0); + } + + _ = GenerateResponseAsync(npcUid, npcComp, userMessage); + } + + private string GetContextInfo(EntityUid entity, EntityUid npcUid, LLMNPCComponent npcComp) + { + var infoParts = new List(); + + infoParts.Add(Loc.GetString("llmnpc-context-header-speaker")); + + var name = Identity.Name(entity, EntityManager); + infoParts.Add(Loc.GetString("llmnpc-context-speaker-name", ("name", name))); + + if (TryComp(entity, out var humanoid)) + { + if (_prototypeManager.TryIndex(humanoid.Species, out var species)) + { + infoParts.Add(Loc.GetString("llmnpc-context-speaker-species", ("species", species.Name))); + } + } + + if (TryComp(entity, out var damageable)) + { + var currentHealth = GetCurrentHealth(entity, damageable); + var maxHealth = GetMaxHealth(entity, damageable); + infoParts.Add(Loc.GetString("llmnpc-context-speaker-health", + ("current", currentHealth.ToString("F1")), + ("max", maxHealth.ToString("F1")))); + } + + if (_minds.TryGetMind(entity, out var mindId, out _)) + { + if (_jobs.MindTryGetJobName(mindId, out var jobName)) + { + infoParts.Add(Loc.GetString("llmnpc-context-speaker-job", ("job", jobName))); + } + } + + var equipment = new List(); + if (TryComp(entity, out var inventory)) + { + var enumerator = _inventory.GetSlotEnumerator((entity, inventory)); + while (enumerator.NextItem(out var item, out var slot)) + { + var itemName = MetaData(item).EntityName; + equipment.Add($"{slot.Name}: {itemName}"); + } + } + + if (equipment.Count > 0) + { + infoParts.Add(Loc.GetString("llmnpc-context-speaker-equipment", + ("equipment", string.Join(", ", equipment)))); + } + + var handsItems = new List(); + if (TryComp(entity, out var hands)) + { + var handsEntity = (entity, hands); + foreach (var handItem in _hands.EnumerateHeld(handsEntity)) + { + var itemName = MetaData(handItem).EntityName; + handsItems.Add(itemName); + } + } + + if (handsItems.Count > 0) + { + infoParts.Add(Loc.GetString("llmnpc-context-speaker-hands", + ("items", string.Join(", ", handsItems)))); + } + + infoParts.Add("\n" + Loc.GetString("llmnpc-context-header-npc")); + + if (!string.IsNullOrWhiteSpace(npcComp.NPCName)) + { + infoParts.Add(Loc.GetString("llmnpc-context-npc-name", ("name", npcComp.NPCName))); + } + + if (TryComp(npcUid, out var npcDamageable)) + { + var npcCurrentHealth = GetCurrentHealth(npcUid, npcDamageable); + var npcMaxHealth = GetMaxHealth(npcUid, npcDamageable); + infoParts.Add(Loc.GetString("llmnpc-context-npc-health", + ("current", npcCurrentHealth.ToString("F1")), + ("max", npcMaxHealth.ToString("F1")))); + } + + return string.Join("\n", infoParts); + } + + private float GetCurrentHealth(EntityUid entity, DamageableComponent damageable) + { + var maxHealth = GetMaxHealth(entity, damageable); + var currentHealth = maxHealth - (float)_damageableSystem.GetTotalDamage(entity); + return Math.Max(0f, currentHealth); + } + + private float GetMaxHealth(EntityUid entity, DamageableComponent damageable) + { + if (TryComp(entity, out var thresholds)) + { + if (thresholds.Thresholds.Count > 0) + { + return (float)thresholds.Thresholds.Keys.Max(); + } + } + + if (damageable.HealthBarThreshold != null) + { + return (float)damageable.HealthBarThreshold.Value; + } + + return 100f; + } + + private async Task GenerateResponseAsync(EntityUid npcUid, LLMNPCComponent component, string message) + { + component.IsGenerating = true; + + try + { + var messages = new List(); + + var systemPrompt = component.SystemPrompt; + if (string.IsNullOrWhiteSpace(systemPrompt)) + { + systemPrompt = GetDefaultSystemPrompt(component); + } + + if (!string.IsNullOrWhiteSpace(systemPrompt)) + { + messages.Add(new { role = "system", content = systemPrompt }); + } + + foreach (var historyMessage in component.MessageHistory) + { + messages.Add(new { role = historyMessage.Role, content = historyMessage.Content }); + } + + var requestBody = new + { + model = component.Model, + messages = messages.ToArray() + }; + + var request = new HttpRequestMessage(HttpMethod.Post, component.ApiUrl) + { + Content = JsonContent.Create(requestBody) + }; + + request.Headers.Add("Authorization", $"Bearer {component.ApiKey}"); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var response = await _httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _sawmill.Error($"LLMNPC {npcUid}: API error [{response.StatusCode}]: {errorContent}"); + return; + } + + var responseContent = await response.Content.ReadFromJsonAsync(); + + if (responseContent?.Choices == null || responseContent.Choices.Length == 0) + { + _sawmill.Warning($"LLMNPC {npcUid}: Empty response from API"); + return; + } + + var generatedMessage = responseContent.Choices[0].Message?.Content; + if (string.IsNullOrWhiteSpace(generatedMessage)) + { + _sawmill.Warning($"LLMNPC {npcUid}: Empty message content in response"); + return; + } + + component.MessageHistory.Add(new LLMMessageHistoryItem { Role = "assistant", Content = generatedMessage }); + + while (component.MessageHistory.Count > component.MaxHistoryMessages) + { + component.MessageHistory.RemoveAt(0); + } + + _chatSystem.TrySendInGameICMessage(npcUid, generatedMessage, InGameICChatType.Speak, ChatTransmitRange.Normal); + } + catch (HttpRequestException ex) + { + _sawmill.Error($"LLMNPC {npcUid}: HTTP error: {ex.Message}"); + } + catch (TaskCanceledException) + { + _sawmill.Warning($"LLMNPC {npcUid}: Request timeout"); + } + catch (Exception ex) + { + _sawmill.Error($"LLMNPC {npcUid}: Error generating response: {ex}"); + } + finally + { + component.IsGenerating = false; + } + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/MessageOnMindSystem.cs b/Content.Server/Imperial/XxRaay/Systems/MessageOnMindSystem.cs new file mode 100644 index 00000000000..59d1e86f5b8 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/MessageOnMindSystem.cs @@ -0,0 +1,40 @@ +using Content.Server.Chat.Managers; +using Content.Server.Imperial.XxRaay.Components; +using Content.Shared.Mind.Components; +using Robust.Shared.Player; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Система для отправления сообщения при добавлении майнда существу +/// +public sealed class MessageOnMindSystem : EntitySystem +{ + + [Dependency] private readonly IChatManager _chatManager = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMindAdded); + } + + private void OnMindAdded(EntityUid uid, MessageOnMindComponent comp, MindAddedMessage args) + { + if (comp.SentMessage) + return; + + SendMessage(uid, comp); + comp.SentMessage = true; + } + + private void SendMessage(EntityUid uid, MessageOnMindComponent comp) + { + if (!TryComp(uid, out var actor)) + return; + + _chatManager.DispatchServerMessage(actor.PlayerSession, comp.Message); + + } +} + + diff --git a/Content.Server/Imperial/XxRaay/Systems/OrbitalStrikeSystem.cs b/Content.Server/Imperial/XxRaay/Systems/OrbitalStrikeSystem.cs new file mode 100644 index 00000000000..a9fbfc2f0ba --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/OrbitalStrikeSystem.cs @@ -0,0 +1,196 @@ +using System.Numerics; +using Content.Server.Explosion.EntitySystems; +using Content.Server.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Map; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// System for handling orbital strike item that spawns supply pods in a radius. +/// +public sealed class OrbitalStrikeSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly ExplosionSystem _explosionSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent>(OnGetVerbs); + } + + private void OnUseInHand(Entity entity, ref UseInHandEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + + var component = entity.Comp; + var user = args.User; + + var userXform = Transform(user); + var centerCoords = userXform.Coordinates; + var centerPos = _transformSystem.GetWorldPosition(userXform); + + var podCount = component.CurrentPodCount; + var radius = component.CurrentRadius; + var spawnInterval = component.SpawnInterval; + + for (var i = 0; i < podCount; i++) + { + var delay = spawnInterval * i; + + Timer.Spawn(delay, () => + { + if (!Exists(entity)) + return; + + var angle = _random.NextFloat(0, 2 * MathF.PI); + var distance = _random.NextFloat(0, radius); + var offset = new Vector2( + MathF.Cos(angle) * distance, + MathF.Sin(angle) * distance + ); + + var spawnPos = centerPos + offset; + var mapCoords = new MapCoordinates(spawnPos, userXform.MapID); + + EntityCoordinates spawnCoords; + if (_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid)) + { + var localPos = _mapSystem.WorldToLocal(gridUid, grid, mapCoords.Position); + spawnCoords = new EntityCoordinates(gridUid, localPos); + spawnCoords = _mapSystem.AlignToGrid(spawnCoords); + } + else + { + var mapUid = _mapSystem.GetMapOrInvalid(userXform.MapID); + spawnCoords = new EntityCoordinates(mapUid, mapCoords.Position); + } + + var podEntity = Spawn(component.PodPrototype, spawnCoords); + var podComp = EnsureComp(podEntity); + + var mode = component.AvailableExplosionModes.GetValueOrDefault(component.CurrentExplosionMode) + ?? new OrbitalExplosionMode(100f, 1f, 12f); + podComp.ExplosionIntensity = mode.Intensity; + podComp.ExplosionSlope = mode.Slope; + podComp.ExplosionMaxTileIntensity = mode.MaxTileIntensity; + + var explosionCoords = _transformSystem.ToMapCoordinates(spawnCoords); + var explosionDelay = component.ExplosionDelay; + Timer.Spawn(explosionDelay, () => + { + _explosionSystem.QueueExplosion( + explosionCoords, + ExplosionSystem.DefaultExplosionPrototypeId, + podComp.ExplosionIntensity, + podComp.ExplosionSlope, + podComp.ExplosionMaxTileIntensity, + null, + maxTileBreak: 0); + + if (Exists(podEntity)) + QueueDel(podEntity); + }); + }); + } + + _popup.PopupEntity(Loc.GetString(component.PopupLaunchLoc, ("count", podCount)), entity, user); + } + + private void OnGetVerbs(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + var component = entity.Comp; + var user = args.User; + + var priority = 0; + AddCountVerbs(entity, user, ref args, ref priority); + AddRadiusVerbs(entity, user, ref args, ref priority); + AddModeVerbs(entity, user, ref args, ref priority); + } + + private void AddCountVerbs(Entity entity, EntityUid user, ref GetVerbsEvent args, ref int priority) + { + foreach (var count in entity.Comp.AvailablePodCounts) + { + var isSelected = entity.Comp.CurrentPodCount == count; + var verb = new AlternativeVerb + { + Text = Loc.GetString(entity.Comp.VerbCountLoc, ("count", count), ("selected", isSelected)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + entity.Comp.CurrentPodCount = count; + Dirty(entity); + _popup.PopupEntity(Loc.GetString(entity.Comp.VerbCountLoc, ("count", count), ("selected", true)), entity, user); + }, + Priority = priority + }; + priority -= 1; + args.Verbs.Add(verb); + } + } + + private void AddRadiusVerbs(Entity entity, EntityUid user, ref GetVerbsEvent args, ref int priority) + { + foreach (var radiusValue in entity.Comp.AvailableRadii) + { + var isSelected = Math.Abs(entity.Comp.CurrentRadius - radiusValue) < 0.1f; + var verb = new AlternativeVerb + { + Text = Loc.GetString(entity.Comp.VerbRadiusLoc, ("radius", radiusValue), ("selected", isSelected)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + entity.Comp.CurrentRadius = radiusValue; + Dirty(entity); + _popup.PopupEntity(Loc.GetString(entity.Comp.VerbRadiusLoc, ("radius", radiusValue), ("selected", true)), entity, user); + }, + Priority = priority + }; + priority -= 1; + args.Verbs.Add(verb); + } + } + + private void AddModeVerbs(Entity entity, EntityUid user, ref GetVerbsEvent args, ref int priority) + { + foreach (var modeName in entity.Comp.AvailableExplosionModes.Keys) + { + var isSelected = entity.Comp.CurrentExplosionMode == modeName; + var verb = new AlternativeVerb + { + Text = Loc.GetString(entity.Comp.VerbModeLoc, ("mode", modeName), ("selected", isSelected)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + entity.Comp.CurrentExplosionMode = modeName; + Dirty(entity); + _popup.PopupEntity(Loc.GetString(entity.Comp.VerbModeLoc, ("mode", modeName), ("selected", true)), entity, user); + }, + Priority = priority + }; + priority -= 1; + args.Verbs.Add(verb); + } + } + +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/RechargeShoulderRocketLauncherSystem.cs b/Content.Server/Imperial/XxRaay/Systems/RechargeShoulderRocketLauncherSystem.cs new file mode 100644 index 00000000000..3b0344d4ab1 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/RechargeShoulderRocketLauncherSystem.cs @@ -0,0 +1,71 @@ +using Content.Shared.Imperial.XxRaay.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Система для автоперезарядки плечевой ракетной установки. +/// +public sealed class RechargeShoulderRocketLauncherSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var recharge, out var launcher)) + { + if (launcher.Charges >= launcher.MaxCharges || recharge.NextCharge == TimeSpan.Zero) + continue; + + if (recharge.NextCharge > _timing.CurTime) + continue; + + launcher.Charges++; + Dirty(uid, launcher); + + _audio.PlayPvs(recharge.RechargeSound, uid); + + if (launcher.Charges >= launcher.MaxCharges) + { + recharge.NextCharge = TimeSpan.Zero; + Dirty(uid, recharge); + continue; + } + + recharge.NextCharge = recharge.NextCharge + recharge.RechargeCooldown; + Dirty(uid, recharge); + } + } + + private void OnInit(EntityUid uid, RechargeShoulderRocketLauncherComponent component, MapInitEvent args) + { + component.NextCharge = _timing.CurTime; + Dirty(uid, component); + } + + public void Reset(EntityUid uid, RechargeShoulderRocketLauncherComponent? recharge = null) + { + if (!Resolve(uid, ref recharge, false)) + return; + + if (recharge.NextCharge == TimeSpan.Zero || recharge.NextCharge < _timing.CurTime) + { + recharge.NextCharge = _timing.CurTime + recharge.RechargeCooldown; + Dirty(uid, recharge); + } + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/SandevistanSystem.cs b/Content.Server/Imperial/XxRaay/Systems/SandevistanSystem.cs new file mode 100644 index 00000000000..6337f2b55c4 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/SandevistanSystem.cs @@ -0,0 +1,139 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Actions.Events; +using Content.Shared.Imperial.PhaseSpace; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Events; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Система для управления эффектом сандевистана. +/// +public sealed class SandevistanSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentShutdown); + SubscribeLocalEvent(OnSandevistanActivated); + SubscribeLocalEvent(OnRefreshMovementSpeed); + } + + private void OnComponentInit(Entity entity, ref ComponentInit args) + { + _actions.AddAction(entity.Owner, ref entity.Comp.ActionEntity, entity.Comp.ActionPrototype, entity.Owner); + } + + private void OnComponentShutdown(Entity entity, ref ComponentShutdown args) + { + if (entity.Comp.ActionEntity != null) + { + _actions.RemoveAction(entity.Comp.ActionEntity); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var currentTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var component)) + { + if (component.EffectEndTime != TimeSpan.Zero && currentTime >= component.EffectEndTime) + { + DeactivateEffect(uid, component); + } + + if (component.ActionEntity != null && TryComp(component.ActionEntity, out var action)) + { + if (currentTime < component.CooldownEndTime) + { + if (action.Cooldown == null || + action.Cooldown.Value.Start != component.CooldownStartTime || + action.Cooldown.Value.End != component.CooldownEndTime) + { + _actions.SetCooldown(component.ActionEntity, component.CooldownStartTime, component.CooldownEndTime); + } + } + else if (action.Cooldown != null) + { + _actions.RemoveCooldown(component.ActionEntity); + } + } + } + } + + private void OnSandevistanActivated(Entity entity, ref SandevistanActionEvent args) + { + if (args.Handled) + return; + + var component = entity.Comp; + var currentTime = _timing.CurTime; + + var effectEnded = component.EffectEndTime == TimeSpan.Zero || currentTime >= component.EffectEndTime; + if (effectEnded && currentTime < component.CooldownEndTime) + return; + + args.Handled = true; + ActivateEffect(entity.Owner, component); + } + + private void OnRefreshMovementSpeed(Entity entity, ref RefreshMovementSpeedModifiersEvent args) + { + var component = entity.Comp; + var currentTime = _timing.CurTime; + + if (component.EffectEndTime != TimeSpan.Zero && currentTime < component.EffectEndTime) + { + args.ModifySpeed(1f + component.SpeedModifierBonus, 1f + component.SpeedModifierBonus); + } + } + + private void ActivateEffect(EntityUid uid, SandevistanComponent component) + { + var currentTime = _timing.CurTime; + var effectEndTime = currentTime + component.EffectDuration; + var cooldownStartTime = currentTime; + var cooldownEndTime = effectEndTime + component.CooldownDuration; + + component.EffectEndTime = effectEndTime; + component.CooldownStartTime = cooldownStartTime; + component.CooldownEndTime = cooldownEndTime; + Dirty(uid, component); + + EnsureComp(uid); + + EnsureComp(uid); + + if (component.ActionEntity != null) + { + _actions.SetCooldown(component.ActionEntity, cooldownStartTime, cooldownEndTime); + } + + _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); + } + + private void DeactivateEffect(EntityUid uid, SandevistanComponent component) + { + component.EffectEndTime = TimeSpan.Zero; + Dirty(uid, component); + + RemComp(uid); + + _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/ShoulderRocketLauncherSystem.cs b/Content.Server/Imperial/XxRaay/Systems/ShoulderRocketLauncherSystem.cs new file mode 100644 index 00000000000..9bfdf2ae83c --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/ShoulderRocketLauncherSystem.cs @@ -0,0 +1,130 @@ +using System.Linq; +using System.Numerics; +using Content.Shared.Hands; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Imperial.TargetOverlay; +using Content.Shared.Imperial.TargetOverlay.Events; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Server.Imperial.Medieval.Magic.MedievalHomingProjectile; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Map; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Server.GameObjects; +using Content.Server.Weapons.Ranged.Systems; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Система для управления плечевой ракетной установкой с системой захвата целей. +/// +public sealed class ShoulderRocketLauncherSystem : EntitySystem +{ + [Dependency] private readonly SharedTargetOverlaySystem _targetOverlaySystem = default!; + [Dependency] private readonly MedievalHomingProjectileSystem _homingProjectileSystem = default!; + [Dependency] private readonly GunSystem _gunSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly PhysicsSystem _physicsSystem = default!; + [Dependency] private readonly RechargeShoulderRocketLauncherSystem _rechargeSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandSelected); + SubscribeLocalEvent(OnHandDeselected); + SubscribeNetworkEvent(OnTargetOverlayShoot); + } + + private void OnHandSelected(Entity entity, ref HandSelectedEvent args) + { + var component = entity.Comp; + var user = args.User; + + if (component.Charges > 0 && !component.IsTargeting) + { + component.IsTargeting = true; + Dirty(entity); + _targetOverlaySystem.StartTargeting(user, entity, maxTargetCount: component.MaxTargetCount, + whiteListComponents: component.WhiteListComponents); + } + } + + private void OnHandDeselected(Entity entity, ref HandDeselectedEvent args) + { + var component = entity.Comp; + var user = args.User; + + if (component.IsTargeting) + { + component.IsTargeting = false; + Dirty(entity); + _targetOverlaySystem.StopTargeting(user); + } + } + + private void OnTargetOverlayShoot(TargetOverlayShootEvent args) + { + if (args.Sender == null) + return; + + var sender = GetEntity(args.Sender.Value); + var performer = GetEntity(args.Performer); + + if (!TryComp(sender, out var component)) + return; + + if (!_handsSystem.IsHolding(performer, sender, out _)) + return; + + if (component.Charges <= 0) + return; + + var targetsToShoot = args.Targets.Take(component.Charges).ToList(); + + if (targetsToShoot.Count == 0) + return; + + var xform = Transform(performer); + var spawnMapCoords = _transformSystem.ToMapCoordinates(xform.Coordinates); + var userVelocity = _physicsSystem.GetMapLinearVelocity(performer); + + foreach (var (cursorPosition, netTarget) in targetsToShoot) + { + EntityUid? targetEntity = null; + if (netTarget.HasValue) + targetEntity = GetEntity(netTarget.Value); + + var targetMapCoords = targetEntity != null + ? _transformSystem.GetMapCoordinates(targetEntity.Value) + : cursorPosition; + + var rocket = Spawn(component.ProjectilePrototype, xform.Coordinates); + + if (targetEntity != null) + { + _homingProjectileSystem.SetTarget(rocket, targetEntity.Value, linearVelocityIntensy: 1.0f, Angle.Zero, rotateToTarget: true); + } + else + { + _homingProjectileSystem.SetTarget(rocket, targetMapCoords, linearVelocityIntensy: 1.0f, Angle.Zero, rotateToTarget: true); + } + + var direction = targetMapCoords.Position - spawnMapCoords.Position; + if (direction == Vector2.Zero) + direction = _transformSystem.GetWorldRotation(performer).ToVec(); + + _gunSystem.ShootProjectile(rocket, direction, userVelocity, performer, performer, speed: 15f); + } + + component.Charges -= targetsToShoot.Count; + Dirty(sender, component); + + if (TryComp(sender, out var recharge)) + { + _rechargeSystem.Reset(sender, recharge); + } + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/SmasherInnateToolsSystem.cs b/Content.Server/Imperial/XxRaay/Systems/SmasherInnateToolsSystem.cs new file mode 100644 index 00000000000..14bcc36b223 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/SmasherInnateToolsSystem.cs @@ -0,0 +1,70 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Interaction.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Система для автоматического добавления встроенных инструментов смешера в руки. +/// +public sealed class SmasherInnateToolsSystem : EntitySystem +{ + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, SmasherInnateToolsComponent component, MapInitEvent args) + { + if (!TryComp(uid, out var hands)) + return; + + var spawnCoord = Transform(uid).Coordinates; + var handIndex = 0; + + foreach (var toolProtoId in component.Tools) + { + if (handIndex >= component.Hands.Count) + break; + + if (!_prototypeManager.TryIndex(toolProtoId, out var prototype)) + continue; + + var handId = component.Hands[handIndex]; + + if (!_handsSystem.TryGetHand((uid, hands), handId, out _)) + { + handIndex++; + continue; + } + + if (!_handsSystem.HandIsEmpty((uid, hands), handId)) + { + handIndex++; + continue; + } + + var item = Spawn(toolProtoId, spawnCoord); + AddComp(item); + + if (_handsSystem.TryPickup(uid, item, handId, checkActionBlocker: false, handsComp: hands)) + { + component.ToolUids.Add(item); + } + else + { + QueueDel(item); + } + + handIndex++; + } + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/SpawnItemsOnDespawnSystem.cs b/Content.Server/Imperial/XxRaay/Systems/SpawnItemsOnDespawnSystem.cs new file mode 100644 index 00000000000..114408db1a1 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/SpawnItemsOnDespawnSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Imperial.XxRaay.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// System that handles spawning multiple items when an entity with SpawnItemsOnDespawnComponent despawns. +/// +public sealed class SpawnItemsOnDespawnSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDespawn); + } + + private void OnDespawn(EntityUid uid, SpawnItemsOnDespawnComponent comp, ref EntityTerminatingEvent args) + { + if (comp.Items.Count == 0) + return; + + var xform = Transform(uid); + var coords = xform.Coordinates; + + foreach (var itemProto in comp.Items) + { + Spawn(itemProto, coords); + } + } + + /// + /// Sets the list of items to spawn when the entity despawns. + /// + public void SetItems(Entity entity, List items) + { + entity.Comp.Items.Clear(); + entity.Comp.Items.AddRange(items); + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/SupplyCaseSystem.cs b/Content.Server/Imperial/XxRaay/Systems/SupplyCaseSystem.cs new file mode 100644 index 00000000000..fb0b5b60742 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/SupplyCaseSystem.cs @@ -0,0 +1,11 @@ +using Content.Server.Imperial.XxRaay.Components; + +namespace Content.Server.Imperial.XxRaay.Systems; + +/// +/// Server stub for supply case; shared logic in shared system. +/// +public sealed class SupplyCaseSystem : Content.Shared.Imperial.XxRaay.Systems.SupplyCaseSystem +{ +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/VentCrawler/ImperialVentCrawlerSystem.cs b/Content.Server/Imperial/XxRaay/Systems/VentCrawler/ImperialVentCrawlerSystem.cs new file mode 100644 index 00000000000..46d9e8fcc6c --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/VentCrawler/ImperialVentCrawlerSystem.cs @@ -0,0 +1,741 @@ +using System.Numerics; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.DataDefinitions; +using Content.Server.Body.Systems; +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Polymorph; +using Content.Server.NodeContainer.NodeGroups; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Stealth; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.CombatMode; +using Content.Shared.Actions.Components; +using Content.Shared.DoAfter; +using Content.Shared.Eye; +using Content.Shared.Imperial.XxRaay.Events; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.NodeContainer; +using Content.Shared.Physics; +using Content.Shared.Popups; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Content.Shared.Tools.Systems; +using Content.Shared.Verbs; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Content.Shared.Atmos; +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class ImperialVentCrawlerSystem : SharedImperialVentCrawlerSystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly StealthSystem _stealth = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly VisibilitySystem _visibility = default!; + [Dependency] private readonly WeldableSystem _weldable = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ExplosionSystem _explosion = default!; + + private EntityQuery _corpsePossessionQuery; + private EntityQuery _doorHidingQuery; + private EntityQuery _drinkingQuery; + private EntityQuery _evolutionQuery; + private EntityQuery _reproductionQuery; + + public override void Initialize() + { + base.Initialize(); + + _corpsePossessionQuery = GetEntityQuery(); + _doorHidingQuery = GetEntityQuery(); + _drinkingQuery = GetEntityQuery(); + _evolutionQuery = GetEntityQuery(); + _reproductionQuery = GetEntityQuery(); + + SubscribeLocalEvent>(OnVentGetVerbs); + SubscribeLocalEvent(OnEnterDoAfter); + SubscribeLocalEvent(OnExitDoAfter); + + SubscribeLocalEvent(OnGetVisMask); + SubscribeLocalEvent(OnRefreshMoveSpeed); + SubscribeLocalEvent(OnMoved); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnVentCrawlTileMoveAttempt); + + SubscribeLocalEvent(OnInhale); + SubscribeLocalEvent(OnExhale); + SubscribeLocalEvent(OnExposed); + SubscribeLocalEvent(OnPolymorphed); + } + + private void OnInhale(Entity ent, ref InhaleLocationEvent args) + { + if (TryGetPipeNode(ent.Comp.SourceVent, out var pipeNode)) + { + args.Gas = pipeNode.Air; + } + } + + private void OnExhale(Entity ent, ref ExhaleLocationEvent args) + { + if (TryGetPipeNode(ent.Comp.SourceVent, out var pipeNode)) + { + args.Gas = pipeNode.Air; + } + } + + private void OnExposed(Entity ent, ref AtmosExposedGetAirEvent args) + { + if (args.Handled) + return; + + if (TryGetPipeNode(ent.Comp.SourceVent, out var pipeNode)) + { + args.Gas = pipeNode.Air; + args.Handled = true; + } + } + + private void OnPolymorphed(Entity ent, ref PolymorphedEvent args) + { + if (!args.IsRevert) return; + + var vent = ent.Comp.SourceVent; + + if (TryGetGridTile(Transform(ent).Coordinates, out var gridUid, out var grid, out var tile)) + { + foreach (var anchored in _map.GetAnchoredEntities((gridUid, grid), tile)) + { + if (SharesPipeNetwork(anchored, vent)) + { + _explosion.QueueExplosion(anchored, ExplosionSystem.DefaultExplosionPrototypeId, 10, 2, 2); + QueueDel(anchored); + break; + } + } + } + } + + private void OnVentCrawlTileMoveAttempt(Entity ent, ref VentCrawlTileMoveAttemptEvent args) + { + if (!TryComp(args.GridUid, out MapGridComponent? grid)) + return; + + var targetCoords = _map.ToCenterCoordinates(args.GridUid, args.TargetTile, grid); + args.CanMove = IsOnPipeNetwork(ent.Comp, targetCoords); + } + + private void OnVentGetVerbs(Entity ent, ref GetVerbsEvent args) + { + AddVentVerbs(ent, ref args); + } + + private void OnEnterDoAfter(Entity ent, ref EnterImperialVentCrawlerDoAfterEvent args) + { + HandleEnterDoAfter(ent, ref args); + } + + private void OnExitDoAfter(Entity ent, ref ExitImperialVentCrawlerDoAfterEvent args) + { + HandleExitDoAfter(ent, ref args); + } + + private void AddVentVerbs(EntityUid vent, ref GetVerbsEvent args) + { + if (!_actionBlocker.CanConsciouslyPerformAction(args.User) || !_interaction.InRangeUnobstructed(args.User, vent)) + return; + + if (!TryComp(args.User, out ImperialVentCrawlerComponent? crawler)) + return; + + var user = args.User; + + if (CanEnterVent(user, vent)) + { + args.Verbs.Add(new InteractionVerb + { + Priority = crawler.EnterVerbPriority, + Text = Loc.GetString(crawler.EnterVerb), + Icon = crawler.EnterVerbIcon, + DoContactInteraction = false, + Act = () => StartEnterVent(user, vent, crawler), + }); + } + + if (TryComp(user, out VentCrawlingComponent? active) && CanExitVent(user, vent, active)) + { + args.Verbs.Add(new InteractionVerb + { + Priority = crawler.ExitVerbPriority, + Text = Loc.GetString(crawler.ExitVerb), + Icon = crawler.ExitVerbIcon, + DoContactInteraction = false, + Act = () => StartExitVent(user, vent, crawler), + }); + } + } + + private void StartEnterVent(EntityUid user, EntityUid vent, ImperialVentCrawlerComponent crawler) + { + if (!CanEnterVent(user, vent, showPopup: true) || !_actionBlocker.CanConsciouslyPerformAction(user)) + return; + + if (!_interaction.InRangeUnobstructed(user, vent)) + return; + + var args = new DoAfterArgs(EntityManager, user, crawler.EnterDelay, new EnterImperialVentCrawlerDoAfterEvent(), vent, target: vent) + { + BreakOnMove = true, + BreakOnWeightlessMove = true, + BreakOnDamage = true, + NeedHand = false, + }; + + _doAfter.TryStartDoAfter(args); + } + + private void HandleEnterDoAfter(EntityUid vent, ref EnterImperialVentCrawlerDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + if (!TryComp(args.User, out ImperialVentCrawlerComponent? _) || !CanEnterVent(args.User, vent)) + return; + + if (!_actionBlocker.CanConsciouslyPerformAction(args.User) || !_interaction.InRangeUnobstructed(args.User, vent)) + return; + + EnterVent(args.User, vent); + _combatMode.SetInCombatMode(args.User, false); + args.Handled = true; + } + + private void StartExitVent(EntityUid user, EntityUid vent, ImperialVentCrawlerComponent crawler) + { + if (!TryComp(user, out VentCrawlingComponent? active) || !CanExitVent(user, vent, active, showPopup: true)) + return; + + var args = new DoAfterArgs(EntityManager, user, crawler.ExitDelay, new ExitImperialVentCrawlerDoAfterEvent(), vent, target: vent) + { + BreakOnMove = true, + BreakOnWeightlessMove = true, + BreakOnDamage = true, + NeedHand = false, + RequireCanInteract = false, + }; + + _doAfter.TryStartDoAfter(args); + } + + private void HandleExitDoAfter(EntityUid vent, ref ExitImperialVentCrawlerDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + if (!TryComp(args.User, out VentCrawlingComponent? active) || !CanExitVent(args.User, vent, active)) + return; + + ExitVent(args.User, vent); + args.Handled = true; + } + + private bool CanEnterVent(EntityUid user, EntityUid vent, bool showPopup = false) + { + if (HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-in-vent")); + return false; + } + + if (_corpsePossessionQuery.HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-corpse")); + return false; + } + + if (_doorHidingQuery.HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-door")); + return false; + } + + if (_drinkingQuery.HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-drinking")); + return false; + } + + if (_evolutionQuery.HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-evolving")); + return false; + } + + if (_reproductionQuery.HasComp(user)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-reproducing")); + return false; + } + + if (!Transform(vent).Anchored) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-invalid")); + return false; + } + + if (_weldable.IsWelded(vent)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-welded")); + return false; + } + + if (!TryGetPipeNode(vent, out _)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-invalid")); + return false; + } + + return true; + } + + private void ShowFailPopup(EntityUid user, string message) + { + _popup.PopupEntity(message, user, user, PopupType.Small); + } + + private bool CanExitVent(EntityUid user, EntityUid vent, VentCrawlingComponent active, bool showPopup = false) + { + if (_weldable.IsWelded(vent)) + { + if (showPopup) + ShowFailPopup(user, Loc.GetString("vent-crawler-fail-welded")); + return false; + } + + return IsOnSameTile(user, vent) && SharesPipeNetwork(vent, active.SourceVent); + } + + public void EnterVent(EntityUid user, EntityUid vent) + { + if (!TryComp(user, out ImperialVentCrawlerComponent? crawler)) + return; + + var active = AddComp(user); + EnsureComp(user); + + active.SourceVent = vent; + active.RemovedComplexInteraction = false; + active.WasCollidable = true; + active.FixtureStates.Clear(); + active.AddedStealth = false; + active.AddedVisibility = false; + active.PreviousStealthEnabled = true; + active.PreviousStealthVisibility = 1f; + active.PreviousVisibilityLayer = crawler.HiddenVisibilityLayer; + active.SoundDistance = 0f; + active.DisabledActions.Clear(); + + if (TryComp(user, out PhysicsComponent? physics)) + { + active.WasCollidable = physics.CanCollide; + SaveAndClearFixtures(user, active, physics); + + if (physics.CanCollide) + _physics.SetCanCollide(user, false, body: physics); + + _physics.ResetDynamics(user, physics); + } + + if (crawler.UseStealth) + ApplyStealth(user, active, crawler); + + ApplyVentVisibility(user, active, crawler); + + if (crawler.RemoveComplexInteraction) + RemoveComplexInteraction(user, active); + + if (crawler.DisableActions) + SetActionsEnabled(user, false, active); + + _transform.SetCoordinates(user, Transform(user), Transform(vent).Coordinates); + _movement.RefreshMovementSpeedModifiers(user); + + EnsureComp(user); + + if (TryComp(user, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((user, eye)); + } + + private void ExitVent(EntityUid user, EntityUid vent) + { + if (!TryComp(user, out VentCrawlingComponent? active) || !CanExitVent(user, vent, active)) + return; + + _transform.SetCoordinates(user, Transform(user), Transform(vent).Coordinates); + RemComp(user); + } + + private void OnMoved(Entity ent, ref MoveEvent args) + { + TryPlayVentCrawlSound(ent, ref args); + } + + private void OnGetVisMask(Entity ent, ref GetVisMaskEvent args) + { + if (!TryComp(ent, out ImperialVentCrawlerComponent? crawler)) + return; + + if (crawler.EyeSeeSubfloor) + args.VisibilityMask |= (int) VisibilityFlags.Subfloor; + + if (crawler.EyeSeeCrawlVisibilityLayer) + args.VisibilityMask |= (int) crawler.CrawlVisibilityLayer; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + DisableTileMovement(ent); + + if (TryComp(ent, out PhysicsComponent? physics)) + { + RestoreFixtures(ent, ent.Comp, physics); + _physics.SetCanCollide(ent, ent.Comp.WasCollidable, body: physics); + _physics.ResetDynamics(ent, physics); + } + + if (TryComp(ent, out ImperialVentCrawlerComponent? crawler)) + { + if (crawler.RemoveComplexInteraction) + RestoreComplexInteraction(ent, ent.Comp); + + RestoreVisibility(ent, ent.Comp); + + if (crawler.UseStealth) + RestoreStealth(ent, ent.Comp); + + if (crawler.DisableActions) + SetActionsEnabled(ent, true, ent.Comp); + } + + RemComp(ent); + _movement.RefreshMovementSpeedModifiers(ent); + + if (TryComp(ent, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((ent, eye)); + } + + private void OnRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!TryComp(ent, out ImperialVentCrawlerComponent? crawler)) + return; + + args.ModifySpeed(crawler.VentSpeedMultiplier, crawler.VentSpeedMultiplier); + } + + private bool IsOnPipeNetwork(VentCrawlingComponent active, EntityCoordinates coordinates) + { + if (!TryGetPipeNode(active.SourceVent, out var sourcePipe)) + return false; + + if (!TryGetGridTile(coordinates, out var gridUid, out var grid, out var tile)) + return false; + + foreach (var anchored in _map.GetAnchoredEntities((gridUid, grid), tile)) + { + if (SharesPipeNetwork(anchored, active.SourceVent, sourcePipe)) + return true; + } + + return false; + } + + private bool SharesPipeNetwork(EntityUid entity, EntityUid sourceVent) + { + if (!TryGetPipeNode(sourceVent, out var sourcePipe)) + return false; + + return SharesPipeNetwork(entity, sourceVent, sourcePipe); + } + + private bool SharesPipeNetwork(EntityUid entity, EntityUid sourceVent, PipeNode sourcePipe) + { + if (!TryComp(entity, out NodeContainerComponent? nodeContainer)) + return false; + + foreach (var node in nodeContainer.Nodes.Values) + { + if (node is not PipeNode pipe) + continue; + + if (sourcePipe.NodeGroup == null) + return entity == sourceVent; + + if (ReferenceEquals(pipe.NodeGroup, sourcePipe.NodeGroup)) + return true; + } + + return false; + } + + private bool TryGetPipeNode(EntityUid uid, out PipeNode pipeNode) + { + pipeNode = default!; + + if (!TryComp(uid, out NodeContainerComponent? nodeContainer)) + return false; + + foreach (var node in nodeContainer.Nodes.Values) + { + if (node is not PipeNode pipe) + continue; + + pipeNode = pipe; + return true; + } + + return false; + } + + private bool TryGetGridTile(EntityCoordinates coordinates, out EntityUid gridUid, out MapGridComponent grid, out Vector2i tile) + { + gridUid = default; + grid = default!; + tile = default; + + if (!TryComp(coordinates.EntityId, out MapGridComponent? gridComp)) + return false; + + grid = gridComp; + gridUid = coordinates.EntityId; + tile = _map.TileIndicesFor(gridUid, grid, coordinates); + return true; + } + + private bool IsOnSameTile(EntityUid first, EntityUid second) + { + if (!TryGetGridTile(Transform(first).Coordinates, out var firstGrid, out _, out var firstTile)) + return false; + + if (!TryGetGridTile(Transform(second).Coordinates, out var secondGrid, out _, out var secondTile)) + return false; + + return firstGrid == secondGrid && firstTile == secondTile; + } + + private void ApplyStealth(EntityUid user, VentCrawlingComponent active, ImperialVentCrawlerComponent crawler) + { + var hadStealth = TryComp(user, out StealthComponent? stealth); + if (!hadStealth) + { + stealth = EnsureComp(user); + active.AddedStealth = true; + } + + active.PreviousStealthEnabled = stealth!.Enabled; + active.PreviousStealthVisibility = _stealth.GetVisibility(user, stealth); + + _stealth.SetEnabled(user, true, stealth); + _stealth.SetVisibility(user, crawler.StealthVisibility, stealth); + } + + private void RestoreStealth(EntityUid user, VentCrawlingComponent active) + { + if (!TryComp(user, out StealthComponent? stealth)) + return; + + if (active.AddedStealth) + { + RemComp(user); + return; + } + + _stealth.SetEnabled(user, active.PreviousStealthEnabled, stealth); + _stealth.SetVisibility(user, active.PreviousStealthVisibility, stealth); + } + + private void ApplyVentVisibility(EntityUid user, VentCrawlingComponent active, ImperialVentCrawlerComponent crawler) + { + var hadVisibility = TryComp(user, out VisibilityComponent? visibility); + if (!hadVisibility) + { + visibility = EnsureComp(user); + active.AddedVisibility = true; + } + + active.PreviousVisibilityLayer = visibility!.Layer; + _visibility.AddLayer((user, visibility), crawler.CrawlVisibilityLayer, false); + _visibility.RemoveLayer((user, visibility), crawler.HiddenVisibilityLayer, false); + _visibility.RefreshVisibility(user, visibilityComponent: visibility); + } + + private void RestoreVisibility(EntityUid user, VentCrawlingComponent active) + { + if (!TryComp(user, out VisibilityComponent? visibility)) + return; + + if (active.AddedVisibility) + { + RemComp(user); + return; + } + + _visibility.SetLayer((user, visibility), active.PreviousVisibilityLayer, false); + _visibility.RefreshVisibility(user, visibilityComponent: visibility); + } + + private void RemoveComplexInteraction(EntityUid user, VentCrawlingComponent active) + { + if (!HasComp(user)) + return; + + active.RemovedComplexInteraction = true; + RemComp(user); + } + + private void RestoreComplexInteraction(EntityUid user, VentCrawlingComponent active) + { + if (!active.RemovedComplexInteraction) + return; + + EnsureComp(user); + active.RemovedComplexInteraction = false; + } + + private void SaveAndClearFixtures(EntityUid user, VentCrawlingComponent active, PhysicsComponent physics) + { + if (!TryComp(user, out FixturesComponent? fixtures)) + return; + + active.FixtureStates.Clear(); + + foreach (var (id, fixture) in fixtures.Fixtures) + { + active.FixtureStates.Add(new ImperialVentCrawlerFixtureState + { + Id = id, + Hard = fixture.Hard, + CollisionLayer = (CollisionGroup) fixture.CollisionLayer, + CollisionMask = (CollisionGroup) fixture.CollisionMask, + }); + + _physics.SetHard(user, fixture, false, fixtures); + _physics.SetCollisionLayer(user, id, fixture, (int) CollisionGroup.None, fixtures, physics); + _physics.SetCollisionMask(user, id, fixture, (int) CollisionGroup.None, fixtures, physics); + } + } + + private void RestoreFixtures(EntityUid user, VentCrawlingComponent active, PhysicsComponent physics) + { + if (!TryComp(user, out FixturesComponent? fixtures)) + return; + + foreach (var state in active.FixtureStates) + { + if (!fixtures.Fixtures.TryGetValue(state.Id, out var fixture)) + continue; + + _physics.SetHard(user, fixture, state.Hard, fixtures); + _physics.SetCollisionLayer(user, state.Id, fixture, (int) state.CollisionLayer, fixtures, physics); + _physics.SetCollisionMask(user, state.Id, fixture, (int) state.CollisionMask, fixtures, physics); + } + + active.FixtureStates.Clear(); + } + + private void SetActionsEnabled(EntityUid user, bool enabled, VentCrawlingComponent active) + { + if (!enabled) + { + foreach (var action in _actions.GetActions(user)) + { + if (!action.Comp.Enabled) + continue; + + active.DisabledActions.Add(action.Owner); + _actions.SetEnabled(new Entity(action.Owner, action.Comp), false); + } + + return; + } + + foreach (var actionUid in active.DisabledActions) + { + if (!TryComp(actionUid, out ActionComponent? action)) + continue; + + _actions.SetEnabled(new Entity(actionUid, action), true); + } + + active.DisabledActions.Clear(); + } + + public void ForceExit(EntityUid user) + { + if (!TryComp(user, out VentCrawlingComponent? active)) + return; + + ExitVent(user, active.SourceVent); + } + + private void DisableTileMovement(EntityUid user) + { + RemComp(user); + + if (!TryComp(user, out PhysicsComponent? physics)) + return; + + _physics.SetLinearVelocity(user, Vector2.Zero, body: physics); + _physics.SetAngularVelocity(user, 0f, body: physics); + } + + private void TryPlayVentCrawlSound(Entity ent, ref MoveEvent args) + { + if (!TryComp(ent, out ImperialVentCrawlerComponent? crawler) || crawler.MoveSound == null || crawler.MoveSoundInterval <= 0f) + return; + + if (!args.OldPosition.TryDistance(EntityManager, args.NewPosition, out var distance) || distance <= 0f) + return; + + var interval = crawler.MoveSoundInterval; + + ent.Comp.SoundDistance = distance > interval + ? interval + : ent.Comp.SoundDistance + distance; + + if (ent.Comp.SoundDistance < interval) + return; + + ent.Comp.SoundDistance -= interval; + _audio.PlayPvs(crawler.MoveSound, ent); + } +} + diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/AllAtOnceWormVentSpawnRule.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/AllAtOnceWormVentSpawnRule.cs new file mode 100644 index 00000000000..87c2064bef4 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/AllAtOnceWormVentSpawnRule.cs @@ -0,0 +1,34 @@ +using Content.Server.Imperial.XxRaay.Components; +using Content.Server.StationEvents.Events; +using Content.Shared.GameTicking.Components; +using Robust.Shared.Player; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class AllAtOnceWormVentSpawnRule : StationEventSystem +{ + [Dependency] private readonly WormVentSpawnRule _wormVentSpawn = default!; + + protected override void Started(EntityUid uid, AllAtOnceWormVentSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (!_wormVentSpawn.TrySpawnWormsOnRandomStationVents(component.Prototype, component.Count, args.RuleId)) + { + ForceEndSelf(uid, gameRule); + return; + } + + Announce(component); + } + + private void Announce(AllAtOnceWormVentSpawnRuleComponent component) + { + var allPlayersInGame = Filter.Empty().AddWhere(GameTicker.UserHasJoinedGame); + + if (component.StartAnnouncement != null) + ChatSystem.DispatchFilteredAnnouncement(allPlayersInGame, Loc.GetString(component.StartAnnouncement), playSound: false, colorOverride: component.StartAnnouncementColor); + + Audio.PlayGlobal(component.StartAudio, allPlayersInGame, true); + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodDrinkSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodDrinkSystem.cs new file mode 100644 index 00000000000..2b3581a4edf --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodDrinkSystem.cs @@ -0,0 +1,38 @@ +using Content.Shared.Actions; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Systems; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormBloodDrinkSystem : SharedWormBloodDrinkSystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly WormBloodSystem _wormBlood = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.DrinkActionEntity, ent.Comp.DrinkAction); + } + + protected override void OnDrinkerShutdown(Entity ent) + { + if (ent.Comp.DrinkActionEntity != null) + _actions.RemoveAction(ent.Owner, ent.Comp.DrinkActionEntity); + } + + protected override void OnBloodDrained(EntityUid worm, EntityUid target, WormBloodDrinkerComponent drinker, int gained) + { + _wormBlood.TryAddBlood(worm, gained); + _audio.PlayPvs(drinker.DrinkSound, worm); + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodSystem.cs new file mode 100644 index 00000000000..7187d7dc98c --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormBloodSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Alert; +using Content.Shared.Eye; +using Content.Shared.Imperial.XxRaay; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Helpers; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormBloodSystem : EntitySystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly Robust.Shared.Timing.IGameTiming _timing = default!; + + private EntityQuery _bloodQuery; + private TimeSpan _accumulator = TimeSpan.Zero; + private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(0.5); + + public override void Initialize() + { + base.Initialize(); + _bloodQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetVisMask); + SubscribeLocalEvent(OnCorpseOccupiedStartup); + SubscribeLocalEvent(OnCorpseOccupiedShutdown); + SubscribeLocalEvent(OnCorpseOccupiedGetVisMask); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_timing.CurTime < _accumulator) + return; + + _accumulator = _timing.CurTime + UpdateInterval; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var blood)) + { + var severity = WormBloodHelper.GetSeverity(blood.Blood); + if (blood.LastAlertSeverity == severity) + continue; + + blood.LastAlertSeverity = severity; + UpdateBloodAlert(uid, blood, severity); + } + } + + private void OnStartup(EntityUid uid, WormBloodComponent component, ComponentStartup args) + { + var severity = WormBloodHelper.GetSeverity(component.Blood); + component.LastAlertSeverity = severity; + UpdateBloodAlert(uid, component, severity); + + if (TryComp(uid, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((uid, eye)); + } + + private void OnShutdown(EntityUid uid, WormBloodComponent component, ComponentShutdown args) + { + _alerts.ClearAlert(uid, component.BloodAlert); + + if (TryComp(uid, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((uid, eye)); + } + + private void OnGetVisMask(Entity ent, ref GetVisMaskEvent args) + { + AddWormVisibilityLayers(ref args); + } + + private void OnCorpseOccupiedStartup(Entity ent, ref ComponentStartup args) + { + if (TryComp(ent.Owner, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((ent.Owner, eye)); + } + + private void OnCorpseOccupiedShutdown(Entity ent, ref ComponentShutdown args) + { + if (TryComp(ent.Owner, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((ent.Owner, eye)); + } + + private void OnCorpseOccupiedGetVisMask(Entity ent, ref GetVisMaskEvent args) + { + AddWormVisibilityLayers(ref args); + } + + private static void AddWormVisibilityLayers(ref GetVisMaskEvent args) + { + args.VisibilityMask |= XxRaayVisibilityLayers.SpiderVentMask; + args.VisibilityMask |= XxRaayVisibilityLayers.DoorHideMask; + } + + public void SetBlood(EntityUid uid, int value, WormBloodComponent? component = null) + { + if (!_bloodQuery.Resolve(uid, ref component)) + return; + + var clamped = Math.Clamp(value, 0, component.MaxBlood); + if (component.Blood == clamped) + return; + + component.Blood = clamped; + Dirty(uid, component); + + var severity = WormBloodHelper.GetSeverity(component.Blood); + component.LastAlertSeverity = severity; + UpdateBloodAlert(uid, component, severity); + } + + public void ModifyBlood(EntityUid uid, int delta, WormBloodComponent? component = null) + { + if (!_bloodQuery.Resolve(uid, ref component)) + return; + + SetBlood(uid, component.Blood + delta, component); + } + + public int TryAddBlood(EntityUid uid, int amount, WormBloodComponent? component = null) + { + if (!_bloodQuery.Resolve(uid, ref component) || amount <= 0) + return 0; + + var space = component.MaxBlood - component.Blood; + var added = Math.Min(amount, space); + if (added <= 0) + return 0; + + SetBlood(uid, component.Blood + added, component); + return added; + } + + private void UpdateBloodAlert(EntityUid uid, WormBloodComponent component, short severity) + { + if (!HasComp(uid)) + return; + + _alerts.ShowAlert(uid, component.BloodAlert, severity); + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormCocoonObserveSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormCocoonObserveSystem.cs new file mode 100644 index 00000000000..bdbe9365413 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormCocoonObserveSystem.cs @@ -0,0 +1,141 @@ +using Content.Shared.Actions; +using Content.Shared.Alert; +using Content.Shared.Eye; +using Content.Shared.IdentityManagement; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormCocoonObserveSystem : SharedWormCocoonObserveSystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + private EntityQuery _corpsePossessionQuery; + private EntityQuery _evolutionQuery; + private EntityQuery _reproductionQuery; + + public override void Initialize() + { + base.Initialize(); + + _corpsePossessionQuery = GetEntityQuery(); + _evolutionQuery = GetEntityQuery(); + _reproductionQuery = GetEntityQuery(); + } + + public void SetupPlayerCocoon(EntityUid cocoon, EntityUid worm, TimeSpan startTime, TimeSpan endTime, ProtoId timerAlert) + { + var observer = EnsureComp(cocoon); + observer.SourceWorm = worm; + DirtyField(cocoon, observer, nameof(WormCocoonObserverComponent.SourceWorm)); + + _actions.AddAction(cocoon, ref observer.ObserveActionEntity, observer.ObserveAction); + DirtyField(cocoon, observer, nameof(WormCocoonObserverComponent.ObserveActionEntity)); + + if (HasComp(cocoon)) + _alerts.ShowAlert(cocoon, timerAlert, cooldown: (startTime, endTime), autoRemove: true, showCooldown: true); + } + + public void CleanupCocoon(EntityUid cocoon, ProtoId? timerAlert = null) + { + ClearObservation(cocoon); + + if (timerAlert is { } alert && HasComp(cocoon)) + _alerts.ClearAlert(cocoon, alert); + + if (TryComp(cocoon, out WormCocoonObserverComponent? observer)) + { + if (observer.ObserveActionEntity != null) + _actions.RemoveAction(cocoon, observer.ObserveActionEntity); + + RemComp(cocoon); + } + } + + protected override void ObserveRandomWorm(Entity cocoon, EntityUid? performer) + { + var candidates = new List(); + CollectObservableWorms(cocoon.Comp.SourceWorm, candidates); + + if (candidates.Count == 0) + { + ShowObservePopup(cocoon, performer, Loc.GetString("worm-cocoon-observe-fail-none")); + return; + } + + var target = candidates[_random.Next(candidates.Count)]; + SetObservationTarget(cocoon, target); + + var message = Loc.GetString("worm-cocoon-observe-start", + ("target", Identity.Entity(target, EntityManager))); + + ShowObservePopup(cocoon, performer, message); + } + + private void CollectObservableWorms(EntityUid excludeWorm, List results) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out _, out var xform)) + { + if (uid == excludeWorm) + continue; + + if (_mobState.IsDead(uid)) + continue; + + if (_corpsePossessionQuery.HasComp(uid) + || _evolutionQuery.HasComp(uid) + || _reproductionQuery.HasComp(uid)) + continue; + + if (xform.MapUid is not { } mapUid || _map.IsPaused(mapUid)) + continue; + + results.Add(uid); + } + } + + private void SetObservationTarget(Entity cocoon, EntityUid target) + { + cocoon.Comp.ObservedEntity = target; + + if (TryComp(cocoon, out EyeComponent? eye)) + _eye.SetTarget(cocoon, target, eye); + } + + private void ClearObservation(EntityUid cocoon) + { + if (!TryComp(cocoon, out WormCocoonObserverComponent? observer)) + return; + + observer.ObservedEntity = null; + + if (TryComp(cocoon, out EyeComponent? eye)) + _eye.SetTarget(cocoon, null, eye); + } + + public void ClearTargetFromAllObservers(EntityUid target) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var cocoon, out var observer)) + { + if (observer.ObservedEntity == target) + { + ClearObservation(cocoon); + ObserveRandomWorm((cocoon, observer), null); + } + } + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormCorpsePossessionSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormCorpsePossessionSystem.cs new file mode 100644 index 00000000000..d20e131deab --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormCorpsePossessionSystem.cs @@ -0,0 +1,493 @@ +using Content.Server.Ghost; +using Content.Server.Imperial.XxRaay.Components; +using Content.Server.Mind; +using Content.Server.NPC; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Systems; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Administration.Systems; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Maps; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.StatusIcon.Components; +using System.Numerics; +using Content.Shared.Ghost; +using Content.Shared.Follower; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormCorpsePossessionSystem : SharedWormCorpsePossessionSystem +{ + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedMindSystem _sharedMind = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly NPCSystem _npc = default!; + [Dependency] private readonly HTNSystem _htn = default!; + [Dependency] private readonly WormBloodDrinkSystem _wormBloodDrink = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly ISharedPlayerManager _players = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly GhostSystem _ghosts = default!; + [Dependency] private readonly RejuvenateSystem _rejuvenate = default!; + [Dependency] private readonly FollowerSystem _follower = default!; + [Dependency] private readonly WormCocoonObserveSystem _cocoonObserve = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHostMapInit); + SubscribeLocalEvent(OnCorpseMobStateChanged); + } + + private void OnHostMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.EnterActionEntity, ent.Comp.EnterAction); + } + + protected override void OnHostShutdown(Entity ent) + { + if (ent.Comp.EnterActionEntity != null) + _actions.RemoveAction(ent.Owner, ent.Comp.EnterActionEntity); + } + + private void OnCorpseMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState is not (MobState.Critical or MobState.Dead)) + return; + + if (TryComp(ent.Comp.Worm, out ActiveWormCorpsePossessionComponent? active) && active.Exiting) + return; + + ExitPossession(ent.Comp.Worm, ent.Owner, forced: true); + } + + protected override void TryPossess(EntityUid worm, EntityUid corpse, WormCorpseHostComponent host) + { + if (!CanPossess((worm, host), corpse) || HasComp(worm) || HasComp(worm) || HasComp(worm) || HasComp(worm)) + return; + + _wormBloodDrink.StopDrinking(worm); + + DetachPlayersFromCorpse(corpse, worm); + + if (!_mind.TryGetMind(worm, out var mindId, out var mind)) + (mindId, mind) = _mind.CreateMind(null, Name(worm)); + + _sharedMind.MakeSentient(corpse); + EnsureComp(corpse); + + _mobState.ChangeMobState(corpse, MobState.Alive); + ApplyPossessionHealth(corpse, host); + + HideWormBody(worm); + + var active = EnsureComp(worm); + active.Corpse = corpse; + active.CorpseCoordinates = Transform(corpse).Coordinates; + active.RelocatedHtn = false; + active.CorpseHadHtn = false; + active.PlayerControlled = mind.UserId != null; + Dirty(worm, active); + + if (active.PlayerControlled) + { + if (TryComp(worm, out var wormHtn)) + _npc.SleepNPC(worm, wormHtn); + } + else + { + RelocateHtn(worm, corpse, active); + } + + _mind.TransferTo(mindId, corpse, ghostCheckOverride: true, mind: mind); + + EntityUid? exitAction = null; + _actions.AddAction(corpse, ref exitAction, host.ExitAction); + + var hadStatusIcon = HasComp(corpse); + if (!hadStatusIcon) + EnsureComp(corpse); + + var occupied = EnsureComp(corpse); + occupied.Worm = worm; + occupied.ExitActionEntity = exitAction; + occupied.AddedStatusIcon = !hadStatusIcon; + Dirty(corpse, occupied); + } + + protected override void ExitPossession(EntityUid worm, EntityUid corpse, bool forced) + { + if (!TryComp(worm, out ActiveWormCorpsePossessionComponent? active)) + return; + + if (active.Exiting) + return; + + active.Exiting = true; + Dirty(worm, active); + + corpse = active.Corpse; + + if (TerminatingOrDeleted(corpse)) + { + FinishExitWithoutCorpse(worm, active, forced); + return; + } + + if (!TryComp(worm, out WormCorpseHostComponent? host)) + { + FinishExit(worm, corpse, active, host: null, forced); + return; + } + + RemoveExitAction(corpse); + + _damageable.TryChangeDamage(corpse, host.ExitBleedDamage); + EnsureCorpseDeadAfterWormExit(corpse); + + if (host.ExitSound != null) + _audio.PlayPvs(host.ExitSound, corpse); + + FinishExit(worm, corpse, active, host, forced); + } + + private void ApplyPossessionHealth(EntityUid corpse, WormCorpseHostComponent host) + { + _rejuvenate.PerformRejuvenate(corpse); + + if (!TryComp(corpse, out var thresholds)) + { + _mobState.ChangeMobState(corpse, MobState.Alive); + return; + } + + if (!_mobThreshold.TryGetThresholdForState(corpse, MobState.Critical, out var criticalThreshold, thresholds)) + { + if (!_mobThreshold.TryGetThresholdForState(corpse, MobState.Dead, out var deadThreshold, thresholds)) + { + _mobState.ChangeMobState(corpse, MobState.Alive); + return; + } + criticalThreshold = deadThreshold; + } + + var fraction = Math.Clamp(host.PossessMinHealthFraction, 0f, 1f); + var targetDamage = criticalThreshold.Value * FixedPoint2.New(1f - fraction); + + if (targetDamage > FixedPoint2.Zero) + { + var damage = new DamageSpecifier(); + damage.DamageDict[host.PossessDamageType] = targetDamage; + _damageable.TryChangeDamage(corpse, damage); + } + + _mobState.ChangeMobState(corpse, MobState.Alive); + } + + /// + /// Sends the worm body to nullspace and pauses it. + /// Nullspace itself is not paused, so the entity must be paused explicitly. + /// + public void HideWormBody(EntityUid worm) + { + _follower.StopAllFollowers(worm); + _cocoonObserve.ClearTargetFromAllObservers(worm); + _transform.DetachEntity(worm, Transform(worm)); + SetPaused(worm, true); + } + + private void RemoveExitAction(EntityUid corpse) + { + if (!TryComp(corpse, out WormCorpseOccupiedComponent? occupied) || occupied.ExitActionEntity is not {} actionUid) + return; + + if (TryComp(actionUid, out ActionComponent? action) && action.AttachedEntity == corpse) + _actions.RemoveAction(corpse, actionUid); + + occupied.ExitActionEntity = null; + Dirty(corpse, occupied); + } + + private void FinishExitWithoutCorpse( + EntityUid worm, + ActiveWormCorpsePossessionComponent active, + bool forced) + { + PlaceWormAtStoredCoordinates(worm, active.CorpseCoordinates); + + if (_mind.TryGetMind(active.Corpse, out var mindId, out var mind) + || _mind.TryGetMind(worm, out mindId, out mind)) + { + _mind.TransferTo(mindId, worm, ghostCheckOverride: true, mind: mind); + } + + ApplyPostExitHtn(worm, active.Corpse, active); + + TryComp(worm, out WormCorpseHostComponent? host); + CleanupPossession(worm, active.Corpse, active, host, forced); + } + + private void FinishExit( + EntityUid worm, + EntityUid corpse, + ActiveWormCorpsePossessionComponent active, + WormCorpseHostComponent? host, + bool forced) + { + PlaceWormAtCorpse(worm, corpse); + + if (_mind.TryGetMind(corpse, out var mindId, out var mind) + || _mind.TryGetMind(worm, out mindId, out mind)) + { + _mind.TransferTo(mindId, worm, ghostCheckOverride: true, mind: mind); + } + + ApplyPostExitHtn(worm, corpse, active); + + CleanupPossession(worm, corpse, active, host, forced); + } + + private void CleanupPossession( + EntityUid worm, + EntityUid corpse, + ActiveWormCorpsePossessionComponent active, + WormCorpseHostComponent? host, + bool forced) + { + if (TryComp(corpse, out WormCorpseOccupiedComponent? occupied) && occupied.AddedStatusIcon) + RemComp(corpse); + + RemComp(corpse); + RestoreVictimPlayers(corpse); + RemComp(worm); + + if (!forced && host?.EnterActionEntity != null) + _actions.SetCooldown(host.EnterActionEntity, host.EnterCooldown); + } + + private void RelocateHtn(EntityUid worm, EntityUid corpse, ActiveWormCorpsePossessionComponent active) + { + if (!TryComp(worm, out var wormHtn)) + return; + + _npc.SleepNPC(worm, wormHtn); + + active.CorpseHadHtn = HasComp(corpse); + active.RelocatedHtn = true; + + var corpseHtn = EnsureComp(corpse); + corpseHtn.RootTask = wormHtn.RootTask; + corpseHtn.Blackboard.SetValue(NPCBlackboard.Owner, corpse); + _npc.WakeNPC(corpse, corpseHtn); + _htn.Replan(corpseHtn); + + Dirty(worm, active); + } + + private void ApplyPostExitHtn(EntityUid worm, EntityUid corpse, ActiveWormCorpsePossessionComponent active) + { + if (active.PlayerControlled) + { + if (TryComp(worm, out var wormHtn)) + _npc.SleepNPC(worm, wormHtn); + + return; + } + + if (!active.RelocatedHtn) + return; + + if (TryComp(corpse, out var corpseHtn)) + { + _npc.SleepNPC(corpse, corpseHtn); + + if (!active.CorpseHadHtn) + RemComp(corpse); + } + + if (TryComp(worm, out var wormHtnNpc)) + { + _npc.WakeNPC(worm, wormHtnNpc); + _htn.Replan(wormHtnNpc); + } + } + + private void PlaceWormAtCorpse(EntityUid worm, EntityUid corpse) + { + PlaceWormAtStoredCoordinates(worm, Transform(corpse).Coordinates); + } + + private void PlaceWormAtStoredCoordinates(EntityUid worm, EntityCoordinates coordinates) + { + SetPaused(worm, false); + + var wormXform = Transform(worm); + + _transform.SetParent(worm, wormXform, coordinates.EntityId); + _transform.SetCoordinates(worm, wormXform, coordinates); + + if (TryComp(worm, out PhysicsComponent? physics)) + { + _physics.SetLinearVelocity(worm, Vector2.Zero, body: physics); + _physics.SetAngularVelocity(worm, 0f, body: physics); + } + } + + private void DetachPlayersFromCorpse(EntityUid corpse, EntityUid worm) + { + var netCorpse = GetNetEntity(corpse); + + if (_mind.TryGetMind(corpse, out var corpseMindId, out var corpseMind) + && corpseMind.UserId != null + && (!_mind.TryGetMind(worm, out var wormMindId, out _) || corpseMindId != wormMindId)) + { + if (corpseMind.OwnedEntity == corpse + && corpseMind.VisitingEntity is { } visiting + && HasComp(visiting)) + { + if (_players.TryGetSessionById(corpseMind.UserId.Value, out var session) + && session.AttachedEntity == corpse) + _players.SetAttachedEntity(session, visiting); + + if (TryComp(visiting, out GhostComponent? visitingGhost)) + _ghosts.SetCanReturnToBody((visiting, visitingGhost), false); + } + else + { + _ghosts.OnGhostAttempt(corpseMindId, canReturnGlobal: false, forced: true, mind: corpseMind); + } + } + + if (TryComp(corpse, out var actor) + && actor.PlayerSession.AttachedEntity == corpse) + { + var detachSession = true; + + if (TryComp(worm, out MindContainerComponent? wormContainer) + && wormContainer.Mind is { } possessedWormMindId + && _mind.TryGetMind(possessedWormMindId, out _, out var possessedWormMind) + && possessedWormMind.UserId == actor.PlayerSession.UserId) + { + detachSession = false; + } + + if (detachSession) + _players.SetAttachedEntity(actor.PlayerSession, null); + } + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var mindId, out var mind)) + { + if (mind.UserId == null || mind.OriginalOwnedEntity != netCorpse) + continue; + + if (_players.TryGetSessionById(mind.UserId.Value, out var session) && session.AttachedEntity == corpse) + { + if (TryGetPlayerGhost(mind, session, out var ghostUid)) + _players.SetAttachedEntity(session, ghostUid); + else + _players.SetAttachedEntity(session, null); + } + + if (TryGetPlayerGhost(mind, null, out var ghost) && TryComp(ghost, out GhostComponent? ghostComp)) + _ghosts.SetCanReturnToBody((ghost, ghostComp), false); + } + } + + private void RestoreVictimPlayers(EntityUid corpse) + { + var netCorpse = GetNetEntity(corpse); + var canReturn = _mobState.IsDead(corpse); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var mindId, out var mind)) + { + if (mind.UserId == null || mind.OriginalOwnedEntity != netCorpse) + continue; + + if (!TryGetPlayerGhost(mind, _players.TryGetSessionById(mind.UserId.Value, out var session) ? session : null, out var ghostUid) + || !TryComp(ghostUid, out GhostComponent? ghost)) + continue; + + ReestablishGhostVisit(mindId, mind, corpse); + + _ghosts.SetCanReturnToBody((ghostUid, ghost), canReturn); + } + } + + /// + /// Возврат в тело работает через Visit: OwnedEntity = труп, VisitingEntity = призрак, UnVisit переносит сессию на труп. + /// + private void ReestablishGhostVisit(EntityUid mindId, MindComponent mind, EntityUid corpse) + { + if (!Exists(corpse) || !TryGetPlayerGhost(mind, null, out var ghostUid)) + return; + + if (mind.OwnedEntity != corpse) + _mind.TransferTo(mindId, corpse, ghostCheckOverride: true, mind: mind); + + if (mind.VisitingEntity == ghostUid) + return; + + if (mind.VisitingEntity != null) + _mind.UnVisit(mindId, mind); + + if (mind.VisitingEntity != ghostUid) + _mind.Visit(mindId, ghostUid, mind); + } + + private bool TryGetPlayerGhost(MindComponent mind, ICommonSession? session, out EntityUid ghostUid) + { + ghostUid = default; + + if (mind.VisitingEntity != null && HasComp(mind.VisitingEntity)) + { + ghostUid = mind.VisitingEntity.Value; + return true; + } + + if (mind.OwnedEntity != null && HasComp(mind.OwnedEntity)) + { + ghostUid = mind.OwnedEntity.Value; + return true; + } + + if (session?.AttachedEntity is { } attached && HasComp(attached)) + { + ghostUid = attached; + return true; + } + + return false; + } + + private void EnsureCorpseDeadAfterWormExit(EntityUid corpse) + { + if (_mobState.IsDead(corpse)) + return; + + _mobState.ChangeMobState(corpse, MobState.Dead); + } + +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormDoorHideSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormDoorHideSystem.cs new file mode 100644 index 00000000000..6fd248a0f77 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormDoorHideSystem.cs @@ -0,0 +1,482 @@ +using Content.Server.Imperial.XxRaay.Components; +using Content.Server.Stealth; +using Content.Shared.ActionBlocker; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Doors; +using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; +using Content.Shared.DoAfter; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Events; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Interaction; +using Content.Shared.Movement.Events; +using Content.Shared.Popups; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Content.Shared.Stunnable; +using Content.Shared.Tools.Systems; +using Content.Shared.Verbs; +using Content.Shared.Eye; +using Content.Shared.Tag; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormDoorHideSystem : SharedWormDoorHideSystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedDoorSystem _doorSystem = default!; + [Dependency] private readonly SharedStaminaSystem _stamina = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly WeldableSystem _weldable = default!; + [Dependency] private readonly StealthSystem _stealth = default!; + [Dependency] private readonly VisibilitySystem _visibility = default!; + [Dependency] private readonly ImperialVentCrawlerSystem _ventCrawler = default!; + [Dependency] private readonly TagSystem _tags = default!; + + private EntityQuery _ventCrawlingQuery; + private EntityQuery _corpsePossessionQuery; + private EntityQuery _doorHidingQuery; + private EntityQuery _drinkingQuery; + private EntityQuery _occupiedQuery; + private EntityQuery _evolutionQuery; + private EntityQuery _reproductionQuery; + + public override void Initialize() + { + base.Initialize(); + + _ventCrawlingQuery = GetEntityQuery(); + _corpsePossessionQuery = GetEntityQuery(); + _doorHidingQuery = GetEntityQuery(); + _drinkingQuery = GetEntityQuery(); + _occupiedQuery = GetEntityQuery(); + _evolutionQuery = GetEntityQuery(); + _reproductionQuery = GetEntityQuery(); + + SubscribeLocalEvent>(OnAirlockGetVerbs); + SubscribeLocalEvent(OnHideDoAfter); + SubscribeLocalEvent(OnMoved); + SubscribeLocalEvent(OnHidingShutdown); + SubscribeLocalEvent(OnOccupiedShutdown); + SubscribeLocalEvent(OnBeforeDoorOpened); + SubscribeLocalEvent(OnDoorStateChanged); + } + + public override void ForceExit(EntityUid worm) + { + if (!_doorHidingQuery.HasComp(worm)) + return; + + ExitDoorHide(worm); + } + + private void OnAirlockGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!_actionBlocker.CanConsciouslyPerformAction(args.User) || !_interaction.InRangeUnobstructed(args.User, ent.Owner)) + return; + + if (!TryComp(args.User, out WormDoorHiderComponent? hider)) + return; + + if (!TryComp(ent, out DoorComponent? door)) + return; + + var user = args.User; + + if (CanHideInDoor(user, ent, door, hider)) + { + args.Verbs.Add(new InteractionVerb + { + Priority = hider.EnterVerbPriority, + Text = Loc.GetString(hider.EnterVerb), + Icon = hider.EnterVerbIcon, + DoContactInteraction = false, + Act = () => StartHideInDoor(user, ent, hider), + }); + } + + if (TryComp(user, out WormDoorHidingComponent? hiding) && CanExitDoor(user, ent, hiding)) + { + args.Verbs.Add(new InteractionVerb + { + Priority = hider.ExitVerbPriority, + Text = Loc.GetString(hider.ExitVerb), + Icon = hider.ExitVerbIcon, + DoContactInteraction = false, + Act = () => ExitDoorHide(user), + }); + } + } + + private void StartHideInDoor(EntityUid worm, EntityUid door, WormDoorHiderComponent hider) + { + if (!TryComp(door, out DoorComponent? doorComp) || !CanHideInDoor(worm, door, doorComp, hider, showPopup: true)) + return; + + if (!_actionBlocker.CanConsciouslyPerformAction(worm) || !_interaction.InRangeUnobstructed(worm, door)) + return; + + var args = new DoAfterArgs(EntityManager, worm, hider.EnterDelay, new WormDoorHideDoAfterEvent(), door, target: door) + { + BreakOnMove = true, + BreakOnWeightlessMove = true, + BreakOnDamage = true, + NeedHand = false, + }; + + _doAfter.TryStartDoAfter(args); + } + + private void OnHideDoAfter(Entity ent, ref WormDoorHideDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + args.Handled = true; + + if (!TryComp(args.User, out WormDoorHiderComponent? hider)) + return; + + if (!TryComp(ent, out DoorComponent? door)) + return; + + if (!CanHideInDoor(args.User, ent, door, hider)) + return; + + if (!_actionBlocker.CanConsciouslyPerformAction(args.User)) + return; + + EnterDoorHide(args.User, ent, hider); + } + + private void OnMoved(Entity ent, ref MoveEvent args) + { + if (ent.Comp.IgnoreNextMove) + { + ent.Comp.IgnoreNextMove = false; + return; + } + + ExitDoorHide(ent.Owner); + } + + private void OnHidingShutdown(Entity ent, ref ComponentShutdown args) + { + RestorePhysics(ent.Owner, ent.Comp); + RestoreStealth(ent.Owner, ent.Comp); + if (ent.Comp.AppliedVisibility) + RestoreVisibility(ent.Owner, ent.Comp); + + if (TryComp(ent.Owner, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((ent.Owner, eye)); + + RemComp(ent.Owner); + } + + private void OnOccupiedShutdown(Entity ent, ref ComponentShutdown args) + { + if (!Exists(ent.Comp.Worm) || !TryComp(ent.Comp.Worm, out WormDoorHidingComponent? hiding)) + return; + + if (hiding.Exiting) + return; + + RemComp(ent.Comp.Worm); + } + + private void OnBeforeDoorOpened(Entity ent, ref BeforeDoorOpenedEvent args) + { + if (args.User is not { } user || user == ent.Comp.Worm) + return; + + if (!TryComp(ent.Comp.Worm, out WormDoorHiderComponent? hider) || !hider.AmbushOnOpen) + return; + + if (!TryComp(ent, out DoorComponent? door) || !_doorSystem.HasAccess(ent, user, door)) + return; + + if (!_actionBlocker.CanConsciouslyPerformAction(user) || !_interaction.InRangeUnobstructed(user, ent.Owner)) + return; + + AmbushDoorOpener(ent.Comp.Worm, user, hider); + } + + private void OnDoorStateChanged(Entity ent, ref DoorStateChangedEvent args) + { + if (args.State is DoorState.Open or DoorState.Opening) + ExitDoorHide(ent.Comp.Worm); + } + + private void EnterDoorHide(EntityUid worm, EntityUid door, WormDoorHiderComponent hider) + { + if (_doorHidingQuery.HasComp(worm)) + return; + + _ventCrawler.ForceExit(worm); + + var hiding = AddComp(worm); + hiding.SourceDoor = door; + + if (TryComp(worm, out PhysicsComponent? physics)) + { + hiding.WasCollidable = physics.CanCollide; + + if (physics.CanCollide) + _physics.SetCanCollide(worm, false, body: physics); + + _physics.ResetDynamics(worm, physics); + } + + if (hider.UseStealth) + ApplyStealth(worm, hiding, hider); + + ApplyDoorVisibility(worm, hiding, hider); + + var occupied = EnsureComp(door); + occupied.Worm = worm; + Dirty(door, occupied); + + hiding.IgnoreNextMove = true; + EnsureComp(worm); + _transform.SetCoordinates(worm, Transform(worm), Transform(door).Coordinates); + + if (TryComp(worm, out EyeComponent? eye)) + _eye.RefreshVisibilityMask((worm, eye)); + } + + private void AmbushDoorOpener(EntityUid worm, EntityUid target, WormDoorHiderComponent hider) + { + ExitDoorHide(worm); + + if (!Exists(worm) || !Exists(target)) + return; + + _transform.SetCoordinates(worm, Transform(worm), Transform(target).Coordinates); + _stun.TryKnockdown(target, hider.AmbushKnockdownTime, force: true); + + if (TryComp(target, out StaminaComponent? stamina)) + { + var remainingStamina = MathF.Max(0f, stamina.CritThreshold - _stamina.GetStaminaDamage(target, stamina)); + var staminaDamage = MathF.Max(hider.AmbushStaminaDamage, remainingStamina); + _stamina.TakeStaminaDamage(target, staminaDamage, stamina, source: worm, visual: true, ignoreResist: true); + } + + _popup.PopupEntity(Loc.GetString("worm-door-hide-ambush"), target, PopupType.LargeCaution); + } + + private void ExitDoorHide(EntityUid worm) + { + if (!TryComp(worm, out WormDoorHidingComponent? hiding) || hiding.Exiting) + return; + + hiding.Exiting = true; + var door = hiding.SourceDoor; + + if (Exists(door) && TryComp(door, out WormDoorHideOccupiedComponent? occupied) && occupied.Worm == worm) + RemComp(door); + + RemComp(worm); + } + + private bool CanHideInDoor( + EntityUid worm, + EntityUid door, + DoorComponent doorComp, + WormDoorHiderComponent hider, + bool showPopup = false) + { + if (_doorHidingQuery.HasComp(worm)) + return false; + + if (_ventCrawlingQuery.HasComp(worm)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-vent")); + return false; + } + + if (_corpsePossessionQuery.HasComp(worm)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-possessing")); + return false; + } + + if (_drinkingQuery.HasComp(worm)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-drinking")); + return false; + } + + if (_evolutionQuery.HasComp(worm)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-evolving")); + return false; + } + + if (_reproductionQuery.HasComp(worm)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-reproducing")); + return false; + } + + if (_occupiedQuery.HasComp(door)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-occupied")); + return false; + } + + if (!Transform(door).Anchored) + return false; + + if (doorComp.State == DoorState.Welded || _weldable.IsWelded(door)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-welded")); + return false; + } + + if (doorComp.State != DoorState.Closed) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-open")); + return false; + } + + if (_tags.HasTag(door, hider.HighSecDoorTag)) + { + if (showPopup) + ShowFailPopup(worm, Loc.GetString("worm-door-hide-fail-armored")); + return false; + } + + return _interaction.InRangeUnobstructed(worm, door, popup: showPopup); + } + + private bool CanExitDoor(EntityUid worm, EntityUid door, WormDoorHidingComponent hiding) + { + return hiding.SourceDoor == door && IsOnSameTile(worm, door); + } + + private bool IsOnSameTile(EntityUid first, EntityUid second) + { + if (!TryGetGridTile(Transform(first).Coordinates, out var firstGrid, out _, out var firstTile)) + return false; + + if (!TryGetGridTile(Transform(second).Coordinates, out var secondGrid, out _, out var secondTile)) + return false; + + return firstGrid == secondGrid && firstTile == secondTile; + } + + private bool TryGetGridTile(EntityCoordinates coordinates, out EntityUid gridUid, out MapGridComponent grid, out Vector2i tile) + { + gridUid = default; + grid = default!; + tile = default; + + if (!TryComp(coordinates.EntityId, out MapGridComponent? gridComp)) + return false; + + grid = gridComp; + gridUid = coordinates.EntityId; + tile = _map.TileIndicesFor(gridUid, grid, coordinates); + return true; + } + + private void ApplyDoorVisibility(EntityUid worm, WormDoorHidingComponent hiding, WormDoorHiderComponent hider) + { + hiding.AppliedVisibility = true; + + var hadVisibility = TryComp(worm, out VisibilityComponent? visibility); + if (!hadVisibility) + { + visibility = EnsureComp(worm); + hiding.AddedVisibility = true; + } + + hiding.PreviousVisibilityLayer = visibility!.Layer; + _visibility.AddLayer((worm, visibility), hider.DoorHideVisibilityLayer, false); + _visibility.RemoveLayer((worm, visibility), hider.HiddenVisibilityLayer, false); + _visibility.RefreshVisibility(worm, visibilityComponent: visibility); + } + + private void RestoreVisibility(EntityUid worm, WormDoorHidingComponent hiding) + { + if (!TryComp(worm, out VisibilityComponent? visibility)) + return; + + if (hiding.AddedVisibility) + { + RemComp(worm); + return; + } + + _visibility.SetLayer((worm, visibility), hiding.PreviousVisibilityLayer, false); + _visibility.RefreshVisibility(worm, visibilityComponent: visibility); + } + + private void ApplyStealth(EntityUid worm, WormDoorHidingComponent hiding, WormDoorHiderComponent hider) + { + var hadStealth = TryComp(worm, out StealthComponent? stealth); + if (!hadStealth) + { + stealth = EnsureComp(worm); + hiding.AddedStealth = true; + } + + hiding.PreviousStealthEnabled = stealth!.Enabled; + hiding.PreviousStealthVisibility = _stealth.GetVisibility(worm, stealth); + + _stealth.SetEnabled(worm, true, stealth); + _stealth.SetVisibility(worm, hider.StealthVisibility, stealth); + } + + private void RestoreStealth(EntityUid worm, WormDoorHidingComponent hiding) + { + if (!TryComp(worm, out StealthComponent? stealth)) + return; + + if (hiding.AddedStealth) + { + RemComp(worm); + return; + } + + _stealth.SetEnabled(worm, hiding.PreviousStealthEnabled, stealth); + _stealth.SetVisibility(worm, hiding.PreviousStealthVisibility, stealth); + } + + private void RestorePhysics(EntityUid worm, WormDoorHidingComponent hiding) + { + if (!TryComp(worm, out PhysicsComponent? physics)) + return; + + _physics.SetCanCollide(worm, hiding.WasCollidable, body: physics); + _physics.ResetDynamics(worm, physics); + } + + private void ShowFailPopup(EntityUid worm, string message) + { + _popup.PopupEntity(message, worm, worm, PopupType.Small); + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormEvolutionSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormEvolutionSystem.cs new file mode 100644 index 00000000000..a4ed62a0dfd --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormEvolutionSystem.cs @@ -0,0 +1,238 @@ +using Content.Server.Mind; +using Content.Server.NPC; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Systems; +using Content.Shared.Actions; +using Content.Shared.Chat; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible; +using Content.Shared.GameTicking; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormEvolutionSystem : SharedWormEvolutionSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedMindSystem _sharedMind = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly WormBloodSystem _wormBlood = default!; + [Dependency] private readonly WormCorpsePossessionSystem _corpsePossession = default!; + [Dependency] private readonly ImperialVentCrawlerSystem _ventCrawler = default!; + [Dependency] private readonly NPCSystem _npc = default!; + [Dependency] private readonly HTNSystem _htn = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly WormCocoonObserveSystem _cocoonObserve = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEvolverMapInit); + SubscribeLocalEvent(OnEvolverShutdown); + SubscribeLocalEvent(OnCocoonDestroyed); + SubscribeLocalEvent(OnCocoonTransformSpeakerName); + } + + private void OnCocoonTransformSpeakerName(Entity ent, ref TransformSpeakerNameEvent args) + { + if (args.Sender != ent.Owner || !Exists(ent.Comp.Worm)) + return; + + args.VoiceName = Name(ent.Comp.Worm); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var cocoonUid, out var cocoon)) + { + if (_timing.CurTime < cocoon.EndTime) + continue; + + CompleteEvolution(cocoonUid, cocoon); + } + } + + private void OnEvolverMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.EvolutionActionEntity, ent.Comp.EvolutionAction); + } + + private void OnEvolverShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.EvolutionActionEntity != null) + _actions.RemoveAction(ent.Owner, ent.Comp.EvolutionActionEntity); + } + + private void OnCocoonDestroyed(Entity ent, ref DestructionEventArgs args) + { + if (ent.Comp.Completing) + return; + + FailEvolution(ent.Owner, ent.Comp); + } + + protected override void OnEvolutionStarted(EntityUid worm) + { + _ventCrawler.ForceExit(worm); + } + + protected override void BeginEvolution(EntityUid worm, WormEvolverComponent evolver) + { + if (!CanEvolve((worm, evolver))) + return; + + _wormBlood.ModifyBlood(worm, -evolver.BloodCost); + + var coords = Transform(worm).Coordinates; + var cocoon = Spawn(evolver.CocoonProto, coords); + + var cocoonComp = EnsureComp(cocoon); + cocoonComp.Worm = worm; + cocoonComp.ResultProto = evolver.ResultProto; + cocoonComp.SourceProto = evolver.SourceProto; + cocoonComp.FailDeathDamageType = evolver.FailDeathDamageType; + cocoonComp.StartTime = _timing.CurTime; + cocoonComp.EndTime = _timing.CurTime + evolver.EvolutionDuration; + Dirty(cocoon, cocoonComp); + + _corpsePossession.HideWormBody(worm); + + var active = EnsureComp(worm); + active.Cocoon = cocoon; + + if (_mind.TryGetMind(worm, out var mindId, out var mind) && mind.UserId != null) + { + active.PlayerControlled = true; + _metaData.SetEntityName(cocoon, Name(worm)); + + var cocoonXform = Transform(cocoon); + _transform.SetLocalRotation(cocoon, Angle.Zero, cocoonXform); + + _mind.Visit(mindId, cocoon, mind); + _cocoonObserve.SetupPlayerCocoon(cocoon, worm, cocoonComp.StartTime, cocoonComp.EndTime, cocoonComp.TimerAlert); + } + else + active.PlayerControlled = false; + + Dirty(worm, active); + } + + private void CompleteEvolution(EntityUid cocoonUid, WormCocoonComponent cocoon) + { + if (cocoon.Completing) + return; + + if (!Exists(cocoon.Worm) || !TryComp(cocoon.Worm, out ActiveWormEvolutionComponent? active)) + { + if (Exists(cocoonUid)) + QueueDel(cocoonUid); + return; + } + + if (active.Cocoon != cocoonUid) + return; + + cocoon.Completing = true; + Dirty(cocoonUid, cocoon); + + _cocoonObserve.CleanupCocoon(cocoonUid, cocoon.TimerAlert); + + var worm = cocoon.Worm; + var coords = Transform(cocoonUid).Coordinates; + var remainingBlood = TryComp(worm, out var blood) ? blood.Blood : 0; + + var newWorm = Spawn(cocoon.ResultProto, coords); + + if (remainingBlood > 0) + _wormBlood.TryAddBlood(newWorm, remainingBlood); + + if (TryTransferPlayerMind(worm, cocoonUid, newWorm)) + { + if (TryComp(newWorm, out var newHtn)) + _npc.SleepNPC(newWorm, newHtn); + } + else if (TryComp(newWorm, out var newHtn)) + { + _npc.WakeNPC(newWorm, newHtn); + _htn.Replan(newHtn); + } + + RemComp(worm); + QueueDel(worm); + QueueDel(cocoonUid); + } + + private void FailEvolution(EntityUid cocoonUid, WormCocoonComponent cocoon) + { + if (cocoon.Completing) + return; + + var worm = cocoon.Worm; + var coords = Transform(cocoonUid).Coordinates; + var playerControlled = Exists(worm) + && TryComp(worm, out ActiveWormEvolutionComponent? active) + && active.PlayerControlled; + + _cocoonObserve.CleanupCocoon(cocoonUid, cocoon.TimerAlert); + + var corpse = SpawnDeadWorm(cocoon.SourceProto, coords, cocoon.FailDeathDamageType); + + if (playerControlled) + TryTransferPlayerMind(worm, cocoonUid, corpse); + + if (Exists(worm)) + { + RemComp(worm); + QueueDel(worm); + } + + QueueDel(cocoonUid); + } + + private bool TryTransferPlayerMind(EntityUid worm, EntityUid _, EntityUid target) + { + if (!_mind.TryGetMind(worm, out var mindId, out var mind) || mind.UserId == null) + return false; + + _sharedMind.MakeSentient(target); + _mind.TransferTo(mindId, target, ghostCheckOverride: true, mind: mind); + return true; + } + + private EntityUid SpawnDeadWorm(EntProtoId proto, EntityCoordinates coords, ProtoId lethalDamageType) + { + var corpse = Spawn(proto, coords); + _mobState.ChangeMobState(corpse, MobState.Dead); + + if (TryComp(corpse, out var thresholds)) + { + var deadThreshold = _mobThreshold.GetThresholdForState(corpse, MobState.Dead, thresholds); + var damage = new DamageSpecifier(); + damage.DamageDict[lethalDamageType] = deadThreshold; + _damageable.TryChangeDamage(corpse, damage); + } + + return corpse; + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormReproductionSystem.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormReproductionSystem.cs new file mode 100644 index 00000000000..4dba83b5d68 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormReproductionSystem.cs @@ -0,0 +1,252 @@ +using System.Numerics; +using Content.Server.Mind; +using Content.Server.NPC; +using Content.Server.NPC.HTN; +using Content.Server.NPC.Systems; +using Content.Shared.Actions; +using Content.Shared.Chat; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible; +using Content.Shared.GameTicking; +using Content.Shared.Imperial.XxRaay.Components; +using Content.Shared.Imperial.XxRaay.Systems; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormReproductionSystem : SharedWormReproductionSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedMindSystem _sharedMind = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly WormBloodSystem _wormBlood = default!; + [Dependency] private readonly WormCorpsePossessionSystem _corpsePossession = default!; + [Dependency] private readonly ImperialVentCrawlerSystem _ventCrawler = default!; + [Dependency] private readonly NPCSystem _npc = default!; + [Dependency] private readonly HTNSystem _htn = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly WormCocoonObserveSystem _cocoonObserve = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnReproducerMapInit); + SubscribeLocalEvent(OnReproducerShutdown); + SubscribeLocalEvent(OnCocoonDestroyed); + SubscribeLocalEvent(OnCocoonTransformSpeakerName); + } + + private void OnCocoonTransformSpeakerName(Entity ent, ref TransformSpeakerNameEvent args) + { + if (args.Sender != ent.Owner || !Exists(ent.Comp.ParentWorm)) + return; + + args.VoiceName = Name(ent.Comp.ParentWorm); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var cocoonUid, out var cocoon)) + { + if (_timing.CurTime < cocoon.EndTime) + continue; + + CompleteReproduction(cocoonUid, cocoon); + } + } + + private void OnReproducerMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.ReproductionActionEntity, ent.Comp.ReproductionAction); + } + + private void OnReproducerShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.ReproductionActionEntity != null) + _actions.RemoveAction(ent.Owner, ent.Comp.ReproductionActionEntity); + } + + private void OnCocoonDestroyed(Entity ent, ref DestructionEventArgs args) + { + if (ent.Comp.Completing) + return; + + FailReproduction(ent.Owner, ent.Comp); + } + + protected override void OnReproductionStarted(EntityUid worm) + { + _ventCrawler.ForceExit(worm); + } + + protected override void BeginReproduction(EntityUid worm, WormReproducerComponent reproducer) + { + if (!CanReproduce((worm, reproducer))) + return; + + _wormBlood.ModifyBlood(worm, -reproducer.BloodCost); + + var coords = Transform(worm).Coordinates; + var cocoon = Spawn(reproducer.CocoonProto, coords); + + var cocoonComp = EnsureComp(cocoon); + cocoonComp.ParentWorm = worm; + cocoonComp.OffspringProto = reproducer.OffspringProto; + cocoonComp.OffspringCount = reproducer.OffspringCount; + cocoonComp.ParentResultProto = reproducer.ParentResultProto; + cocoonComp.SourceProto = reproducer.SourceProto; + cocoonComp.FailDeathDamageType = reproducer.FailDeathDamageType; + cocoonComp.StartTime = _timing.CurTime; + cocoonComp.EndTime = _timing.CurTime + reproducer.ReproductionDuration; + cocoonComp.OffspringOffsets = reproducer.OffspringOffsets; + Dirty(cocoon, cocoonComp); + + _corpsePossession.HideWormBody(worm); + + var active = EnsureComp(worm); + active.Cocoon = cocoon; + + if (_mind.TryGetMind(worm, out var mindId, out var mind) && mind.UserId != null) + { + active.PlayerControlled = true; + _metaData.SetEntityName(cocoon, Name(worm)); + + var cocoonXform = Transform(cocoon); + _transform.SetLocalRotation(cocoon, Angle.Zero, cocoonXform); + + _mind.Visit(mindId, cocoon, mind); + _cocoonObserve.SetupPlayerCocoon(cocoon, worm, cocoonComp.StartTime, cocoonComp.EndTime, cocoonComp.TimerAlert); + } + else + active.PlayerControlled = false; + + Dirty(worm, active); + } + + private void CompleteReproduction(EntityUid cocoonUid, WormReproductionCocoonComponent cocoon) + { + if (cocoon.Completing) + return; + + if (!Exists(cocoon.ParentWorm) || !TryComp(cocoon.ParentWorm, out ActiveWormReproductionComponent? active)) + { + if (Exists(cocoonUid)) + QueueDel(cocoonUid); + return; + } + + if (active.Cocoon != cocoonUid) + return; + + cocoon.Completing = true; + Dirty(cocoonUid, cocoon); + + _cocoonObserve.CleanupCocoon(cocoonUid, cocoon.TimerAlert); + + var worm = cocoon.ParentWorm; + var coords = Transform(cocoonUid).Coordinates; + var remainingBlood = TryComp(worm, out var blood) ? blood.Blood : 0; + + for (var i = 0; i < cocoon.OffspringCount; i++) + { + var offset = i < cocoon.OffspringOffsets.Count ? cocoon.OffspringOffsets[i] : Vector2.Zero; + var spawnCoords = coords.Offset(offset); + var offspring = Spawn(cocoon.OffspringProto, spawnCoords); + + if (TryComp(offspring, out var offspringHtn)) + { + _npc.WakeNPC(offspring, offspringHtn); + _htn.Replan(offspringHtn); + } + } + + var demotedWorm = Spawn(cocoon.ParentResultProto, coords); + + if (remainingBlood > 0) + _wormBlood.TryAddBlood(demotedWorm, remainingBlood); + + if (TryTransferPlayerMind(worm, cocoonUid, demotedWorm)) + { + if (TryComp(demotedWorm, out var demotedHtn)) + _npc.SleepNPC(demotedWorm, demotedHtn); + } + + RemComp(worm); + QueueDel(worm); + QueueDel(cocoonUid); + } + + private void FailReproduction(EntityUid cocoonUid, WormReproductionCocoonComponent cocoon) + { + if (cocoon.Completing) + return; + + var worm = cocoon.ParentWorm; + var coords = Transform(cocoonUid).Coordinates; + var playerControlled = Exists(worm) + && TryComp(worm, out ActiveWormReproductionComponent? active) + && active.PlayerControlled; + + _cocoonObserve.CleanupCocoon(cocoonUid, cocoon.TimerAlert); + + var corpse = SpawnDeadWorm(cocoon.SourceProto, coords, cocoon.FailDeathDamageType); + + if (playerControlled) + TryTransferPlayerMind(worm, cocoonUid, corpse); + + if (Exists(worm)) + { + if (HasComp(worm)) + RemComp(worm); + + QueueDel(worm); + } + + QueueDel(cocoonUid); + } + + private bool TryTransferPlayerMind(EntityUid worm, EntityUid _, EntityUid target) + { + if (!_mind.TryGetMind(worm, out var mindId, out var mind) || mind.UserId == null) + return false; + + _sharedMind.MakeSentient(target); + _mind.TransferTo(mindId, target, ghostCheckOverride: true, mind: mind); + return true; + } + + private EntityUid SpawnDeadWorm(EntProtoId proto, EntityCoordinates coords, ProtoId lethalDamageType) + { + var corpse = Spawn(proto, coords); + _mobState.ChangeMobState(corpse, MobState.Dead); + + if (TryComp(corpse, out var thresholds)) + { + var deadThreshold = _mobThreshold.GetThresholdForState(corpse, MobState.Dead, thresholds); + var damage = new DamageSpecifier(); + damage.DamageDict[lethalDamageType] = deadThreshold; + _damageable.TryChangeDamage(corpse, damage); + } + + return corpse; + } +} diff --git a/Content.Server/Imperial/XxRaay/Systems/Worm/WormVentSpawnRule.cs b/Content.Server/Imperial/XxRaay/Systems/Worm/WormVentSpawnRule.cs new file mode 100644 index 00000000000..e48a215864e --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Systems/Worm/WormVentSpawnRule.cs @@ -0,0 +1,66 @@ +using Content.Server.Imperial.XxRaay.Components; +using Content.Server.StationEvents.Components; +using Content.Server.StationEvents.Events; +using Content.Shared.GameTicking.Components; +using Content.Shared.Station.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.XxRaay.Systems; + +public sealed class WormVentSpawnRule : StationEventSystem +{ + [Dependency] private readonly ImperialVentCrawlerSystem _ventCrawler = default!; + protected override void Started(EntityUid uid, WormVentSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (!TrySpawnWormsOnRandomStationVents(component.Prototype, component.Count, args.RuleId)) + ForceEndSelf(uid, gameRule); + } + + public bool TrySpawnWormsOnRandomStationVents(EntProtoId prototype, int count, string ruleId) + { + if (!TryGetRandomStation(out var station)) + return false; + + var vents = GetStationVents(station.Value); + if (vents.Count == 0) + { + Sawmill.Warning($"Unable to find a valid vent for {ruleId}!"); + return false; + } + + RobustRandom.Shuffle(vents); + + var spawnCount = Math.Min(count, vents.Count); + for (var i = 0; i < spawnCount; i++) + { + var vent = vents[i]; + var coords = Transform(vent).Coordinates; + Sawmill.Info($"Spawning {prototype} at {coords}"); + var worm = Spawn(prototype, coords); + _ventCrawler.EnterVent(worm, vent); + } + + return true; + } + + private List GetStationVents(EntityUid station) + { + var vents = new List(); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out _, out var transform)) + { + if (!transform.Anchored) + continue; + + if (CompOrNull(transform.GridUid)?.Station != station) + continue; + + vents.Add(uid); + } + + return vents; + } +} diff --git a/Content.Server/Imperial/XxRaay/Zero/Overthrow/OverthrowSystem.cs b/Content.Server/Imperial/XxRaay/Zero/Overthrow/OverthrowSystem.cs new file mode 100644 index 00000000000..b9e303f9f65 --- /dev/null +++ b/Content.Server/Imperial/XxRaay/Zero/Overthrow/OverthrowSystem.cs @@ -0,0 +1,5 @@ +namespace Content.Server.Imperial.XxRaay.Zero.Overthrow; + +public sealed class OverthrowSystem : Shared.Imperial.XxRaay.Zero.Overthrow.SharedOverthrowSystem +{ +} diff --git a/Content.Server/Imperial/XxRaay/SyndieBattle/license.txt b/Content.Server/Imperial/XxRaay/license.txt similarity index 100% rename from Content.Server/Imperial/XxRaay/SyndieBattle/license.txt rename to Content.Server/Imperial/XxRaay/license.txt diff --git a/Content.Server/Imperial/_SystemsSupplementation/Zombies/Components/ZombieBarotraumaDamageComponent.cs b/Content.Server/Imperial/_SystemsSupplementation/Zombies/Components/ZombieBarotraumaDamageComponent.cs new file mode 100644 index 00000000000..02053d79dbb --- /dev/null +++ b/Content.Server/Imperial/_SystemsSupplementation/Zombies/Components/ZombieBarotraumaDamageComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Damage; +using Content.Shared.FixedPoint; + +namespace Content.Server.Zombies; + +/// +/// Компонент, хранящий информацию о том, +/// какой урон будет наносится сущности после зомбификации от компонента BarotraumaComponent. +/// +[RegisterComponent] +public sealed partial class ZombieBarotraumaDamageComponent : Component +{ + [DataField] + public DamageSpecifier Damage = new() + { + DamageDict = new() { { "Blunt", FixedPoint2.New(0.20) } } + }; +} diff --git a/Content.Server/Imperial/_SystemsSupplementation/Zombies/ZombifySystem.cs b/Content.Server/Imperial/_SystemsSupplementation/Zombies/ZombifySystem.cs new file mode 100644 index 00000000000..59ab1dcb9bb --- /dev/null +++ b/Content.Server/Imperial/_SystemsSupplementation/Zombies/ZombifySystem.cs @@ -0,0 +1,39 @@ +using Content.Shared.Zombies; +using Content.Server.Atmos.Components; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Components; + +namespace Content.Server.Zombies +{ + public sealed partial class ZombifySystem : EntitySystem + { + [Dependency] private readonly SharedHandsSystem _hands = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnZombified); + } + + private void OnZombified(Entity ent, ref EntityZombifiedEvent args) + { + if (!TryComp(ent, out var zombieBaro)) + return; + + var hands = EnsureComp(ent); + if (hands.Hands.Count == 0) + { + _hands.AddHand(ent.Owner, "left", HandLocation.Left); + _hands.AddHand(ent.Owner, "right", HandLocation.Right); + } + EnsureComp(ent); + EnsureComp(ent).Damage = zombieBaro.Damage; + + RemCompDeferred(ent); + RemCompDeferred(ent); + } + } +} diff --git a/Content.Server/Implants/ChameleonControllerSystem.cs b/Content.Server/Implants/ChameleonControllerSystem.cs index e884e181eeb..3de51a49499 100644 --- a/Content.Server/Implants/ChameleonControllerSystem.cs +++ b/Content.Server/Implants/ChameleonControllerSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Clothing.Systems; +using Content.Server.Clothing.Systems; using Content.Server.Preferences.Managers; using Content.Shared.Clothing; using Content.Shared.Clothing.Components; @@ -105,7 +105,7 @@ private void GetJobEquipmentInformation( private void ChameleonControllerOutfitItemSelected(Entity ent, ref InventoryRelayedEvent args) { - if (!_inventory.TryGetContainingSlot(ent.Owner, out var slot)) + if (!ent.Comp.CanBeSetByController || !_inventory.TryGetContainingSlot(ent.Owner, out var slot)) return; _chameleonClothingSystem.SetSelectedPrototype(ent, GetGearForSlot(args, slot.Name), component: ent.Comp); diff --git a/Content.Server/Implants/ImplantedSystem.cs b/Content.Server/Implants/ImplantedSystem.cs index 78517587302..be477119a03 100644 --- a/Content.Server/Implants/ImplantedSystem.cs +++ b/Content.Server/Implants/ImplantedSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Body.Events; +using Content.Shared.Gibbing; using Content.Shared.Implants.Components; using Content.Shared.Storage; using Robust.Shared.Containers; @@ -11,7 +12,7 @@ public void InitializeImplanted() { SubscribeLocalEvent(OnImplantedInit); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnGibbed); + SubscribeLocalEvent(OnGibbed); } private void OnImplantedInit(Entity ent, ref ComponentInit args) @@ -26,7 +27,7 @@ private void OnShutdown(Entity ent, ref ComponentShutdown ar _container.CleanContainer(ent.Comp.ImplantContainer); } - private void OnGibbed(Entity ent, ref BeingGibbedEvent args) + private void OnGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) { // Drop the storage implant contents before the implants are deleted by the body being gibbed foreach (var implant in ent.Comp.ImplantContainer.ContainedEntities) diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs index 48e83e4878c..08b744ee093 100644 --- a/Content.Server/Implants/ImplanterSystem.cs +++ b/Content.Server/Implants/ImplanterSystem.cs @@ -88,7 +88,7 @@ public void TryImplant(ImplanterComponent component, EntityUid user, EntityUid t if (!_doAfter.TryStartDoAfter(args)) return; - _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user); + _popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user); var userName = Identity.Entity(user, EntityManager); _popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, target, PopupType.LargeCaution); @@ -112,7 +112,7 @@ public void TryDraw(ImplanterComponent component, EntityUid user, EntityUid targ }; if (_doAfter.TryStartDoAfter(args)) - _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user); + _popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user); } diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index 582b9cb2ac4..08fe5b54cf7 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -1,44 +1,5 @@ -using Content.Server.Store.Components; -using Content.Server.Store.Systems; using Content.Shared.Implants; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Store.Components; namespace Content.Server.Implants; -public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem -{ - [Dependency] private readonly StoreSystem _store = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnStoreRelay); - } - - // TODO: This shouldn't be in the SubdermalImplantSystem - private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent implantRelay) - { - var args = implantRelay.Event; - - if (args.Handled) - return; - - // can only insert into yourself to prevent uplink checking with renault - if (args.Target != args.User) - return; - - if (!TryComp(args.Used, out var currency)) - return; - - // same as store code, but message is only shown to yourself - if (!_store.TryAddCurrency((args.Used, currency), (uid, store))) - return; - - args.Handled = true; - var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used)); - _popup.PopupEntity(msg, args.User, args.User); - } -} +public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem; diff --git a/Content.Server/Info/RulesManager.cs b/Content.Server/Info/RulesManager.cs index 224d7f7d9af..af66cdd7857 100644 --- a/Content.Server/Info/RulesManager.cs +++ b/Content.Server/Info/RulesManager.cs @@ -1,7 +1,10 @@ using System.Net; +using Content.Server.Administration.Logs; using Content.Server.Database; using Content.Shared.CCVar; +using Content.Shared.Database; using Content.Shared.Info; +using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; @@ -12,6 +15,8 @@ public sealed class RulesManager [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IAdminLogManager _adminLog = default!; + [Dependency] private readonly IPlayerManager _player = default!; private static DateTime LastValidReadTime => DateTime.UtcNow - TimeSpan.FromDays(60); @@ -24,8 +29,8 @@ public void Initialize() private async void OnConnected(object? sender, NetChannelArgs e) { - var isLocalhost = IPAddress.IsLoopback(e.Channel.RemoteEndPoint.Address) && - _cfg.GetCVar(CCVars.RulesExemptLocal); + var isLocalhost = IPAddress.IsLoopback(e.Channel.RemoteEndPoint.Address) && + _cfg.GetCVar(CCVars.RulesExemptLocal); var lastRead = await _dbManager.GetLastReadRules(e.Channel.UserId); var hasCooldown = lastRead > LastValidReadTime; @@ -43,5 +48,7 @@ private async void OnRulesAccepted(RulesAcceptedMessage message) { var date = DateTime.UtcNow; await _dbManager.SetLastReadRules(message.MsgChannel.UserId, date); + if (message.FuckRules && _player.TryGetSessionById(message.MsgChannel.UserId, out var session)) + _adminLog.Add(LogType.Connection, LogImpact.Extreme, $"Player {session} used the fuckrules command."); } } diff --git a/Content.Server/Instruments/InstrumentSystem.cs b/Content.Server/Instruments/InstrumentSystem.cs index 56b64576cad..c7b888114e5 100644 --- a/Content.Server/Instruments/InstrumentSystem.cs +++ b/Content.Server/Instruments/InstrumentSystem.cs @@ -438,20 +438,22 @@ public override void Update(float frameTime) if (Deleted(master)) { Clean(uid, instrument); + continue; } var masterActive = activeQuery.CompOrNull(master); if (masterActive == null) { Clean(uid, instrument); + continue; } var trans = transformQuery.GetComponent(uid); var masterTrans = transformQuery.GetComponent(master); - if (!_transform.InRange(masterTrans.Coordinates, trans.Coordinates, 10f) -) + if (!_transform.InRange(masterTrans.Coordinates, trans.Coordinates, 10f)) { Clean(uid, instrument); + continue; } } diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 9ac82b21858..c5488f458c5 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -1,12 +1,6 @@ using Content.Shared.Interaction; -using Content.Shared.Storage; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.Player; namespace Content.Server.Interaction { - // TODO Remove Shared prefix public sealed class InteractionSystem : SharedInteractionSystem; } diff --git a/Content.Server/Inventory/InventoryCommand.cs b/Content.Server/Inventory/InventoryCommand.cs index 81978892598..2a7bcab9095 100644 --- a/Content.Server/Inventory/InventoryCommand.cs +++ b/Content.Server/Inventory/InventoryCommand.cs @@ -12,6 +12,27 @@ public sealed class InventoryCommand : ToolshedCommand { private InventorySystem? _inventorySystem; + [CommandImplementation("contents")] + public IEnumerable InventoryQuery([PipedArgument] IEnumerable entities) => + entities.SelectMany(InventoryQuery); + + private IEnumerable InventoryQuery(EntityUid entity) + { + _inventorySystem ??= GetSys(); + + if (!EntityManager.TryGetComponent(entity, out var inventory)) + return []; + + List result = new(); + + foreach (var slot in inventory.Slots) + { + if (_inventorySystem.TryGetSlotEntity(entity, slot.Name, out var item, inventory)) + result.Add(item.Value); + } + return result; + } + [CommandImplementation("getflags")] public IEnumerable InventoryGetFlags([PipedArgument] IEnumerable ents, SlotFlags slotFlag) { @@ -44,7 +65,6 @@ public IEnumerable InventoryGetFlags(EntityUid ent, SlotFlags slotFla return items; } - [CommandImplementation("getnamed")] public IEnumerable InventoryGetNamed([PipedArgument] IEnumerable ents, string slotName) { @@ -81,6 +101,7 @@ public IEnumerable InventoryGetNamed(EntityUid ent, string slotName) public EntityUid? InventoryForcePut([PipedArgument] IEnumerable ents, EntityUid itemEnt, SlotFlags slotFlag) => InventoryPutEnumerableBase(ents, itemEnt, slotFlag, PutType.ForcePut); + [CommandImplementation("forcespawn")] public EntityUid? InventoryForceSpawn([PipedArgument] IEnumerable ents, EntProtoId itemEnt, @@ -90,6 +111,7 @@ public IEnumerable InventoryGetNamed(EntityUid ent, string slotName) public EntityUid? InventoryPut([PipedArgument] IEnumerable ents, EntityUid itemEnt, SlotFlags slotFlag) => InventoryPutEnumerableBase(ents, itemEnt, slotFlag, PutType.Put); + [CommandImplementation("spawn")] public EntityUid? InventorySpawn([PipedArgument] IEnumerable ents, EntProtoId itemEnt, @@ -99,6 +121,7 @@ public IEnumerable InventoryGetNamed(EntityUid ent, string slotName) public EntityUid? InventoryTryPut([PipedArgument] IEnumerable ents, EntityUid itemEnt, SlotFlags slotFlag) => InventoryPutEnumerableBase(ents, itemEnt, slotFlag, PutType.Put); + [CommandImplementation("tryspawn")] public EntityUid? InventoryTrySpawn([PipedArgument] IEnumerable ents, EntProtoId itemEnt, @@ -108,12 +131,12 @@ public IEnumerable InventoryGetNamed(EntityUid ent, string slotName) public EntityUid? InventoryEnsure([PipedArgument] IEnumerable ents, EntityUid itemEnt, SlotFlags slotFlag) => InventoryPutEnumerableBase(ents, itemEnt, slotFlag, PutType.Ensure); + [CommandImplementation("ensurespawn")] public EntityUid? InventoryEnsureSpawn([PipedArgument] IEnumerable ents, EntProtoId itemEnt, SlotFlags slotFlag) => InventorySpawnEnumerableBase(ents, itemEnt, slotFlag, PutType.Ensure); - private EntityUid? InventorySpawnEnumerableBase(IEnumerable targetEnts, EntProtoId itemToInsert, SlotFlags slotFlags, @@ -136,6 +159,7 @@ public IEnumerable InventoryGetNamed(EntityUid ent, string slotName) Del(spawnedItem); return null; } + private EntityUid? InventoryPutEnumerableBase(IEnumerable targetEnts, EntityUid itemToInsert, SlotFlags slotFlags, diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 2a278ac3a0d..2b4a507d604 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,4 +1,3 @@ -using Content.Server.Imperial.Medieval.Achievements.Jobs; using Content.Server.Administration; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -11,9 +10,8 @@ using Content.Server.Discord.DiscordLink; using Content.Server.Discord.WebhookMessages; using Content.Server.EUI; +using Content.Server.FeedbackSystem; using Content.Server.GhostKick; -using Content.Server.Imperial.Medieval.Afk; -using Content.Server.Imperial.Medieval.JoinQueue; using Content.Server.Info; using Content.Server.Mapping; using Content.Server.Maps; @@ -25,73 +23,67 @@ using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; -using Content.Server.Worldgen.Tools; using Content.Shared.Administration.Logs; using Content.Shared.Administration.Managers; using Content.Shared.Chat; +using Content.Shared.FeedbackSystem; +using Content.Shared.IoC; using Content.Shared.Kitchen; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.RateLimiting; using Content.Server.Imperial.Entry; -namespace Content.Server.IoC +namespace Content.Server.IoC; + +internal static class ServerContentIoC { - internal static class ServerContentIoC + public static void Register(IDependencyCollection deps) { - public static void Register() - { - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); // Imperial-Medieval-JoinQueue - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); // Imperial AlwaysPvs - IoCManager.Register(); // Imperial medieval anti afk - IoCManager.Register(); // Imperial Medieval Flavor Images - IoCManager.Register(); // Imperial Medieval Flavor Images + SharedContentIoC.Register(deps); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); + deps.Register(); - IoCManager.Register(); - IoCManager.Register(); - // Imperial ServerContentIoC Start - ImperialEntry.IoCRegister(); - IoCManager.Register(); - // Imperial ServerContentIoC End - } + ImperialEntry.IoCRegister(deps); // Imperial Space / Medieval } } diff --git a/Content.Server/Jobs/ApplyStatusEffectSpecial.cs b/Content.Server/Jobs/ApplyStatusEffectSpecial.cs new file mode 100644 index 00000000000..b06ce114ba7 --- /dev/null +++ b/Content.Server/Jobs/ApplyStatusEffectSpecial.cs @@ -0,0 +1,27 @@ +using Content.Shared.Roles; +using Content.Shared.StatusEffectNew; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Server.Jobs; + +/// +/// Adds permanent status effects to the entity. +/// TODO: Move this, and other JobSpecials, from Server to Shared. +/// +[UsedImplicitly] +public sealed partial class ApplyStatusEffectSpecial : JobSpecial +{ + [DataField(required: true)] + public HashSet StatusEffects { get; private set; } = new(); + + public override void AfterEquip(EntityUid mob) + { + var entMan = IoCManager.Resolve(); + var statusSystem = entMan.System(); + foreach (var effect in StatusEffects) + { + statusSystem.TrySetStatusEffectDuration(mob, effect); + } + } +} diff --git a/Content.Server/KillTracking/KillTrackingSystem.cs b/Content.Server/KillTracking/KillTrackingSystem.cs index ba27ea5d9e5..d40e0e13dd9 100644 --- a/Content.Server/KillTracking/KillTrackingSystem.cs +++ b/Content.Server/KillTracking/KillTrackingSystem.cs @@ -1,5 +1,6 @@ using Content.Server.NPC.HTN; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs index 5337d80fd17..85478b83cd7 100644 --- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs +++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs @@ -111,16 +111,4 @@ public sealed partial class MicrowaveComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public bool CanMicrowaveIdsSafely = true; } - - public sealed class BeingMicrowavedEvent : HandledEntityEventArgs - { - public EntityUid Microwave; - public EntityUid? User; - - public BeingMicrowavedEvent(EntityUid microwave, EntityUid? user) - { - Microwave = microwave; - User = user; - } - } } diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs deleted file mode 100644 index 5bbbe2dc8da..00000000000 --- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Content.Shared.Kitchen; -using Content.Server.Kitchen.EntitySystems; -using Robust.Shared.Audio; - -namespace Content.Server.Kitchen.Components -{ - /// - /// The combo reagent grinder/juicer. The reason why grinding and juicing are seperate is simple, - /// think of grinding as a utility to break an object down into its reagents. Think of juicing as - /// converting something into its single juice form. E.g, grind an apple and get the nutriment and sugar - /// it contained, juice an apple and get "apple juice". - /// - [Access(typeof(ReagentGrinderSystem)), RegisterComponent] - public sealed partial class ReagentGrinderComponent : Component - { - [DataField] - public int StorageMaxEntities = 6; - - [DataField] - public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds. - - [DataField] - public float WorkTimeMultiplier = 1; - - [DataField] - public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); - - [DataField] - public SoundSpecifier GrindSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/blender.ogg"); - - [DataField] - public SoundSpecifier JuiceSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/juicer.ogg"); - - [DataField] - public GrinderAutoMode AutoMode = GrinderAutoMode.Off; - - public EntityUid? AudioStream; - } - - [Access(typeof(ReagentGrinderSystem)), RegisterComponent] - public sealed partial class ActiveReagentGrinderComponent : Component - { - /// - /// Remaining time until the grinder finishes grinding/juicing. - /// - [ViewVariables] - public TimeSpan EndTime; - - [ViewVariables] - public GrinderProgram Program; - } -} diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 07c7f041a25..db0b0d5ffca 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Administration.Logs; -using Content.Server.Body.Systems; using Content.Server.Construction; using Content.Server.Explosion.EntitySystems; using Content.Server.DeviceLinking.Systems; @@ -7,10 +6,7 @@ using Content.Server.Kitchen.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; -using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; @@ -43,12 +39,14 @@ using Content.Shared.Damage; using Robust.Shared.Utility; using Content.Server.Imperial.Medieval.Farmer; +using Content.Shared.Damage.Components; +using Content.Shared.Power.EntitySystems; +using Content.Shared.Temperature.Components; namespace Content.Server.Kitchen.EntitySystems { public sealed class MicrowaveSystem : EntitySystem { - [Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly DeviceLinkSystem _deviceLink = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly PowerReceiverSystem _power = default!; @@ -70,6 +68,7 @@ public sealed class MicrowaveSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedSuicideSystem _suicide = default!; + [Dependency] private readonly SharedPowerStateSystem _powerState = default!; private static readonly EntProtoId MalfunctionSpark = "Spark"; @@ -118,6 +117,7 @@ private void OnCookStart(Entity ent, ref ComponentStar microwaveComponent.PlayingStream = _audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5))?.Entity; + _powerState.SetWorkingState(ent.Owner, true); } private void OnCookStop(Entity ent, ref ComponentShutdown args) @@ -127,6 +127,7 @@ private void OnCookStop(Entity ent, ref ComponentShutd SetAppearance(ent.Owner, MicrowaveVisualState.Idle, microwaveComponent); microwaveComponent.PlayingStream = _audio.Stop(microwaveComponent.PlayingStream); + _powerState.SetWorkingState(ent.Owner, false); } private void OnActiveMicrowaveInsert(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -244,7 +245,7 @@ private void SubtractContents(MicrowaveComponent component, FoodRecipePrototype // If an entity has a stack component, use the stacktype instead of prototype id if (TryComp(item, out var stackComp)) { - itemID = _prototype.Index(stackComp.StackTypeId).Spawn; + itemID = _prototype.Index(stackComp.StackTypeId).Spawn; } else { @@ -267,7 +268,7 @@ private void SubtractContents(MicrowaveComponent component, FoodRecipePrototype { _container.Remove(item, component.Storage); } - _stack.Use(item, 1, stackComp); + _stack.ReduceCount((item, stackComp), 1); break; } else @@ -309,26 +310,9 @@ private void OnSuicideByEnvironment(Entity ent, ref SuicideB _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat"); var victim = args.Victim; - var headCount = 0; - if (TryComp(victim, out var body)) - { - var headSlots = _bodySystem.GetBodyChildrenOfType(victim, BodyPartType.Head, body); - - foreach (var part in headSlots) - { - _container.Insert(part.Id, ent.Comp.Storage); - headCount++; - } - } - - var othersMessage = headCount > 1 - ? Loc.GetString("microwave-component-suicide-multi-head-others-message", ("victim", victim)) - : Loc.GetString("microwave-component-suicide-others-message", ("victim", victim)); - - var selfMessage = headCount > 1 - ? Loc.GetString("microwave-component-suicide-multi-head-message") - : Loc.GetString("microwave-component-suicide-message"); + var othersMessage = Loc.GetString("microwave-component-suicide-others-message", ("victim", victim)); + var selfMessage = Loc.GetString("microwave-component-suicide-message"); _popupSystem.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true); _popupSystem.PopupEntity(selfMessage, victim, victim); diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index cd0ce8f3a60..81bcc9d159f 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -1,345 +1,5 @@ -using Content.Server.Kitchen.Components; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; -using Content.Server.Stack; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Destructible; -using Content.Shared.FixedPoint; -using Content.Shared.Interaction; -using Content.Shared.Kitchen; -using Content.Shared.Kitchen.Components; -using Content.Shared.Popups; -using Content.Shared.Random; -using Content.Shared.Stacks; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Timing; -using System.Linq; -using Content.Server.Construction.Completions; -using Content.Server.Jittering; -using Content.Shared.Jittering; -using Content.Shared.Power; +using Content.Shared.Kitchen.EntitySystems; -namespace Content.Server.Kitchen.EntitySystems -{ - [UsedImplicitly] - internal sealed class ReagentGrinderSystem : EntitySystem - { - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainersSystem = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; - [Dependency] private readonly StackSystem _stackSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly SharedDestructibleSystem _destructible = default!; - [Dependency] private readonly RandomHelperSystem _randomHelper = default!; - [Dependency] private readonly JitteringSystem _jitter = default!; +namespace Content.Server.Kitchen.EntitySystems; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnActiveGrinderStart); - SubscribeLocalEvent(OnActiveGrinderRemove); - SubscribeLocalEvent((uid, _, _) => UpdateUiState(uid)); - SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUiState(uid)); - SubscribeLocalEvent(OnInteractUsing); - - SubscribeLocalEvent(OnContainerModified); - SubscribeLocalEvent(OnContainerModified); - SubscribeLocalEvent(OnEntRemoveAttempt); - - SubscribeLocalEvent(OnToggleAutoModeMessage); - SubscribeLocalEvent(OnStartMessage); - SubscribeLocalEvent(OnEjectChamberAllMessage); - SubscribeLocalEvent(OnEjectChamberContentMessage); - } - - private void OnToggleAutoModeMessage(Entity entity, ref ReagentGrinderToggleAutoModeMessage message) - { - entity.Comp.AutoMode = (GrinderAutoMode) (((byte) entity.Comp.AutoMode + 1) % Enum.GetValues(typeof(GrinderAutoMode)).Length); - - UpdateUiState(entity); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var active, out var reagentGrinder)) - { - if (active.EndTime > _timing.CurTime) - continue; - - reagentGrinder.AudioStream = _audioSystem.Stop(reagentGrinder.AudioStream); - RemCompDeferred(uid); - - var inputContainer = _containerSystem.EnsureContainer(uid, SharedReagentGrinder.InputContainerId); - var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId); - if (outputContainer is null || !_solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out var containerSoln, out var containerSolution)) - continue; - - foreach (var item in inputContainer.ContainedEntities.ToList()) - { - var solution = active.Program switch - { - GrinderProgram.Grind => GetGrindSolution(item), - GrinderProgram.Juice => CompOrNull(item)?.JuiceSolution, - _ => null, - }; - - if (solution is null) - continue; - - if (TryComp(item, out var stack)) - { - var totalVolume = solution.Volume * stack.Count; - if (totalVolume <= 0) - continue; - - // Maximum number of items we can process in the stack without going over AvailableVolume - // We add a small tolerance, because floats are inaccurate. - var fitsCount = (int) (stack.Count * FixedPoint2.Min(containerSolution.AvailableVolume / totalVolume + 0.01, 1)); - if (fitsCount <= 0) - continue; - - // Make a copy of the solution to scale - // Otherwise we'll actually change the volume of the remaining stack too - var scaledSolution = new Solution(solution); - scaledSolution.ScaleSolution(fitsCount); - solution = scaledSolution; - - _stackSystem.SetCount(item, stack.Count - fitsCount); // Setting to 0 will QueueDel - } - else - { - if (solution.Volume > containerSolution.AvailableVolume) - continue; - - _destructible.DestroyEntity(item); - } - - _solutionContainersSystem.TryAddSolution(containerSoln.Value, solution); - } - - _userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key, - new ReagentGrinderWorkCompleteMessage()); - - UpdateUiState(uid); - } - } - - private void OnActiveGrinderStart(Entity ent, ref ComponentStartup args) - { - _jitter.AddJitter(ent, -10, 100); - } - - private void OnActiveGrinderRemove(Entity ent, ref ComponentRemove args) - { - RemComp(ent); - } - - private void OnEntRemoveAttempt(Entity entity, ref ContainerIsRemovingAttemptEvent args) - { - if (HasComp(entity)) - args.Cancel(); - } - - private void OnContainerModified(EntityUid uid, ReagentGrinderComponent reagentGrinder, ContainerModifiedMessage args) - { - UpdateUiState(uid); - - var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId); - _appearanceSystem.SetData(uid, ReagentGrinderVisualState.BeakerAttached, outputContainer.HasValue); - - if (reagentGrinder.AutoMode != GrinderAutoMode.Off && !HasComp(uid) && this.IsPowered(uid, EntityManager)) - { - var program = reagentGrinder.AutoMode == GrinderAutoMode.Grind ? GrinderProgram.Grind : GrinderProgram.Juice; - DoWork(uid, reagentGrinder, program); - } - } - - private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) - { - var heldEnt = args.Used; - var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); - - if (!HasComp(heldEnt)) - { - if (!HasComp(heldEnt)) - { - // This is ugly but we can't use whitelistFailPopup because there are 2 containers with different whitelists. - _popupSystem.PopupEntity(Loc.GetString("reagent-grinder-component-cannot-put-entity-message"), entity.Owner, args.User); - } - - // Entity did NOT pass the whitelist for grind/juice. - // Wouldn't want the clown grinding up the Captain's ID card now would you? - // Why am I asking you? You're biased. - return; - } - - if (args.Handled) - return; - - // Cap the chamber. Don't want someone putting in 500 entities and ejecting them all at once. - // Maybe I should have done that for the microwave too? - if (inputContainer.ContainedEntities.Count >= entity.Comp.StorageMaxEntities) - return; - - if (!_containerSystem.Insert(heldEnt, inputContainer)) - return; - - args.Handled = true; - } - - private void UpdateUiState(EntityUid uid) - { - ReagentGrinderComponent? grinderComp = null; - if (!Resolve(uid, ref grinderComp)) - return; - - var inputContainer = _containerSystem.EnsureContainer(uid, SharedReagentGrinder.InputContainerId); - var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId); - Solution? containerSolution = null; - var isBusy = HasComp(uid); - var canJuice = false; - var canGrind = false; - - if (outputContainer is not null - && _solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out _, out containerSolution) - && inputContainer.ContainedEntities.Count > 0) - { - canGrind = inputContainer.ContainedEntities.All(CanGrind); - canJuice = inputContainer.ContainedEntities.All(CanJuice); - } - - var state = new ReagentGrinderInterfaceState( - isBusy, - outputContainer.HasValue, - this.IsPowered(uid, EntityManager), - canJuice, - canGrind, - grinderComp.AutoMode, - GetNetEntityArray(inputContainer.ContainedEntities.ToArray()), - containerSolution?.Contents.ToArray() - ); - _userInterfaceSystem.SetUiState(uid, ReagentGrinderUiKey.Key, state); - } - - private void OnStartMessage(Entity entity, ref ReagentGrinderStartMessage message) - { - if (!this.IsPowered(entity.Owner, EntityManager) || HasComp(entity)) - return; - - DoWork(entity.Owner, entity.Comp, message.Program); - } - - private void OnEjectChamberAllMessage(Entity entity, ref ReagentGrinderEjectChamberAllMessage message) - { - var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); - - if (HasComp(entity) || inputContainer.ContainedEntities.Count <= 0) - return; - - ClickSound(entity); - foreach (var toEject in inputContainer.ContainedEntities.ToList()) - { - _containerSystem.Remove(toEject, inputContainer); - _randomHelper.RandomOffset(toEject, 0.4f); - } - UpdateUiState(entity); - } - - private void OnEjectChamberContentMessage(Entity entity, ref ReagentGrinderEjectChamberContentMessage message) - { - if (HasComp(entity)) - return; - - var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); - var ent = GetEntity(message.EntityId); - - if (_containerSystem.Remove(ent, inputContainer)) - { - _randomHelper.RandomOffset(ent, 0.4f); - ClickSound(entity); - UpdateUiState(entity); - } - } - - /// - /// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker. - /// - /// The grinder itself - /// - /// Which program, such as grind or juice - private void DoWork(EntityUid uid, ReagentGrinderComponent reagentGrinder, GrinderProgram program) - { - var inputContainer = _containerSystem.EnsureContainer(uid, SharedReagentGrinder.InputContainerId); - var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId); - - // Do we have anything to grind/juice and a container to put the reagents in? - if (inputContainer.ContainedEntities.Count <= 0 || !HasComp(outputContainer)) - return; - - SoundSpecifier? sound; - switch (program) - { - case GrinderProgram.Grind when inputContainer.ContainedEntities.All(CanGrind): - sound = reagentGrinder.GrindSound; - break; - case GrinderProgram.Juice when inputContainer.ContainedEntities.All(CanJuice): - sound = reagentGrinder.JuiceSound; - break; - default: - return; - } - - var active = AddComp(uid); - active.EndTime = _timing.CurTime + reagentGrinder.WorkTime * reagentGrinder.WorkTimeMultiplier; - active.Program = program; - - reagentGrinder.AudioStream = _audioSystem.PlayPvs(sound, uid, - AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier))?.Entity; //slightly higher pitched - _userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key, - new ReagentGrinderWorkStartedMessage(program)); - } - - private void ClickSound(Entity reagentGrinder) - { - _audioSystem.PlayPvs(reagentGrinder.Comp.ClickSound, reagentGrinder.Owner, AudioParams.Default.WithVolume(-2f)); - } - - private Solution? GetGrindSolution(EntityUid uid) - { - if (TryComp(uid, out var extractable) - && extractable.GrindableSolution is not null - && _solutionContainersSystem.TryGetSolution(uid, extractable.GrindableSolution, out _, out var solution)) - { - return solution; - } - else - return null; - } - - private bool CanGrind(EntityUid uid) - { - var solutionName = CompOrNull(uid)?.GrindableSolution; - - return solutionName is not null && _solutionContainersSystem.TryGetSolution(uid, solutionName, out _, out _); - } - - private bool CanJuice(EntityUid uid) - { - return CompOrNull(uid)?.JuiceSolution is not null; - } - } -} +public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem; diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index 39a3ecb7bb5..29c4d65d5b2 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -1,9 +1,8 @@ -using Content.Server.Body.Systems; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; +using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.DoAfter; +using Content.Shared.Gibbing; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Kitchen; @@ -24,7 +23,7 @@ namespace Content.Server.Kitchen.EntitySystems; public sealed class SharpSystem : EntitySystem { - [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly SharedDestructibleSystem _destructibleSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; @@ -134,7 +133,7 @@ private void OnDoAfter(EntityUid uid, SharpComponent component, DoAfterEvent arg args.Args.User, popupType); - _bodySystem.GibBody(args.Args.Target.Value); // does nothing if ent can't be gibbed + _gibbing.Gib(args.Args.Target.Value); // does nothing if ent can't be gibbed _destructibleSystem.DestroyEntity(args.Args.Target.Value); args.Handled = true; diff --git a/Content.Server/Lathe/LatheProducingSystem.cs b/Content.Server/Lathe/LatheProducingSystem.cs new file mode 100644 index 00000000000..ce81ed66e04 --- /dev/null +++ b/Content.Server/Lathe/LatheProducingSystem.cs @@ -0,0 +1,34 @@ +using Content.Server.Lathe.Components; +using Content.Shared.Power.EntitySystems; + +namespace Content.Server.Lathe; + +/// +/// System for handling lathes that are actively producing items. +/// The component is used more so as a marker for EntityQueryEnumerator, +/// however it's also used to set the power state of the lathe when producing. +/// +public sealed class LatheProducingSystem : EntitySystem +{ + [Dependency] private readonly SharedPowerStateSystem _powerState = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnComponentShutdown); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + // use the Try variant of this here + // or else you get trolled by AllComponentsOneToOneDeleteTest + _powerState.TrySetWorkingState(ent.Owner, false); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + _powerState.TrySetWorkingState(ent.Owner, true); + } +} diff --git a/Content.Server/Lathe/LatheSystem.cs b/Content.Server/Lathe/LatheSystem.cs index 3b3a7bce732..32e1c971fb6 100644 --- a/Content.Server/Lathe/LatheSystem.cs +++ b/Content.Server/Lathe/LatheSystem.cs @@ -189,15 +189,8 @@ public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, int quanti if (!CanProduce(uid, recipe, quantity, component)) return false; - foreach (var (mat, amount) in recipe.Materials) - { - var adjustedAmount = recipe.ApplyMaterialDiscount - ? (int)(-amount * component.MaterialUseMultiplier) - : -amount; - adjustedAmount *= quantity; - - _materialStorage.TryChangeMaterialAmount(uid, mat, adjustedAmount); - } + foreach (var (mat, amount) in GetAdjustedAmount(component, recipe)) + _materialStorage.TryChangeMaterialAmount(uid, mat, -amount * quantity); if (component.Queue.Last is { } node && node.ValueRef.Recipe == recipe.ID) node.ValueRef.ItemsRequested += quantity; @@ -299,7 +292,7 @@ public void UpdateUserInterfaceState(EntityUid uid, LatheComponent? component = if (producing == null && component.Queue.First is { } node) producing = node.Value.Recipe; - var state = new LatheUpdateState(GetAvailableRecipes(uid, component), component.Queue.ToArray(), producing, component.UseCardId); + var state = new LatheUpdateState(GetAvailableRecipes(uid, component), component.Queue.ToArray(), producing, component.UseCardId); // Imperial _uiSys.SetUiState(uid, LatheUiKey.Key, state); } @@ -449,6 +442,48 @@ protected override bool HasRecipe(EntityUid uid, LatheRecipePrototype recipe, La return GetAvailableRecipes(uid, component).Contains(recipe.ID); } + /// + /// Iterator returning adjusted amount of material needed to + /// produce a given recipe + /// + private static IEnumerable<(ProtoId mat, int amount)> GetAdjustedAmount(LatheComponent lathe, LatheRecipePrototype recipe) + { + foreach (var (mat, amount) in recipe.Materials) + { + var adjustedAmount = recipe.ApplyMaterialDiscount + ? (int)(amount * lathe.MaterialUseMultiplier) + : amount; + + yield return (mat, adjustedAmount); + } + } + + /// + /// Refunds the material cost of the currently running recipe, + /// without cancelling production + /// + private void RefundCurrentRecipe(EntityUid uid, LatheComponent lathe) + { + _proto.Resolve(lathe.CurrentRecipe, out var recipe); + + foreach (var (mat, amount) in GetAdjustedAmount(lathe, recipe!)) + _materialStorage.TryChangeMaterialAmount(uid, mat, amount); + } + + /// + /// Refunds the material cost of a given batch, + /// without deleting it + /// + private void RefundBatch(EntityUid uid, LatheComponent lathe, LatheRecipeBatch batch) + { + var delta = batch.ItemsRequested - batch.ItemsPrinted; + + _proto.Resolve(batch.Recipe, out var recipe); + + foreach (var (mat, amount) in GetAdjustedAmount(lathe, recipe!)) + _materialStorage.TryChangeMaterialAmount(uid, mat, amount * delta); + } + public void AbortProduction(EntityUid uid, LatheComponent? component = null) { if (!Resolve(uid, ref component)) @@ -471,6 +506,7 @@ public void AbortProduction(EntityUid uid, LatheComponent? component = null) } } + RefundCurrentRecipe(uid, component); component.CurrentRecipe = null; } RemCompDeferred(uid); @@ -525,6 +561,7 @@ public void OnLatheDeleteRequestMessage(EntityUid uid, LatheComponent component, LogImpact.Low, $"{ToPrettyString(args.Actor):player} deleted a lathe job for ({batch.ItemsPrinted}/{batch.ItemsRequested}) {GetRecipeName(batch.Recipe)} at {ToPrettyString(uid):lathe}"); + RefundBatch(uid, component, batch); component.Queue.Remove(node); UpdateUserInterfaceState(uid, component); } @@ -583,6 +620,7 @@ public void OnLatheAbortFabricationMessage(EntityUid uid, LatheComponent compone LogImpact.Low, $"{ToPrettyString(args.Actor):player} aborted printing {GetRecipeName(component.CurrentRecipe.Value)} at {ToPrettyString(uid):lathe}"); + RefundCurrentRecipe(uid, component); component.CurrentRecipe = null; FinishProducing(uid, component); } diff --git a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs index 1b7b6146659..824afbc7890 100644 --- a/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs +++ b/Content.Server/Light/EntitySystems/EmergencyLightSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Light; using Content.Shared.Light.Components; using Content.Shared.Power; +using Content.Shared.Power.Components; using Content.Shared.Station.Components; using Robust.Server.GameObjects; using Color = Robust.Shared.Maths.Color; @@ -144,7 +145,7 @@ private void Update(Entity entity, BatteryComponent bat { if (entity.Comp.State == EmergencyLightState.On) { - if (!_battery.TryUseCharge(entity.Owner, entity.Comp.Wattage * frameTime, battery)) + if (!_battery.TryUseCharge((entity.Owner, battery), entity.Comp.Wattage * frameTime)) { SetState(entity.Owner, entity.Comp, EmergencyLightState.Empty); TurnOff(entity); @@ -152,8 +153,8 @@ private void Update(Entity entity, BatteryComponent bat } else { - _battery.SetCharge(entity.Owner, battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency, battery); - if (_battery.IsFull(entity, battery)) + _battery.ChangeCharge((entity.Owner, battery), entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency); + if (_battery.IsFull((entity.Owner, battery))) { if (TryComp(entity.Owner, out var receiver)) { diff --git a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs index e9a0f8ac8f6..b7fffee18ab 100644 --- a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs @@ -141,13 +141,13 @@ private void OnInteractUsing(EntityUid uid, ExpendableLightComponent component, component.StateExpiryTime = (float)component.RefuelMaterialTime.TotalSeconds; _nameModifier.RefreshNameModifiers(uid); - _stackSystem.SetCount(args.Used, stack.Count - 1, stack); + _stackSystem.ReduceCount((args.Used, stack), 1); UpdateVisualizer((uid, component)); return; } component.StateExpiryTime += (float)component.RefuelMaterialTime.TotalSeconds; - _stackSystem.SetCount(args.Used, stack.Count - 1, stack); + _stackSystem.ReduceCount((args.Used, stack), 1); args.Handled = true; } diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index 2c8d18539ff..e4e947b5487 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -1,12 +1,11 @@ using Content.Server.Actions; using Content.Server.Popups; -using Content.Server.Power.EntitySystems; -using Content.Server.PowerCell; using Content.Shared.Actions; -using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Light; using Content.Shared.Light.Components; +using Content.Shared.Power.EntitySystems; +using Content.Shared.PowerCell; using Content.Shared.Rounding; using Content.Shared.Toggleable; using JetBrains.Annotations; @@ -25,7 +24,7 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem [Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPointLightSystem _lights = default!; @@ -44,7 +43,6 @@ public override void Initialize() SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnActivate); @@ -108,13 +106,15 @@ private void OnShutdown(EntityUid uid, HandheldLightComponent component, Compone // Curently every single flashlight has the same number of levels for status and that's all it uses the charge for // Thus we'll just check if the level changes. - if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery)) + if (!_powerCell.TryGetBatteryFromSlotOrEntity(ent.Owner, out var battery)) return null; - if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge) + var currentCharge = _battery.GetCharge(battery.Value.AsNullable()); + + if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge) return 0; - return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels); + return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels); } private void OnRemove(Entity ent, ref ComponentRemove args) @@ -140,19 +140,14 @@ public bool ToggleStatus(EntityUid user, Entity ent) return ent.Comp.Activated ? TurnOff(ent) : TurnOn(user, ent); } - private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args) - { - args.PushMarkup(component.Activated - ? Loc.GetString("handheld-light-component-on-examine-is-on-message") - : Loc.GetString("handheld-light-component-on-examine-is-off-message")); - } - public override void Shutdown() { base.Shutdown(); _activeLights.Clear(); } + // TODO: Very important: Make this charge rate based instead of instantly removing charge each update step. + // See BatteryComponent public override void Update(float frameTime) { var toRemove = new RemQueue>(); @@ -199,8 +194,7 @@ public override bool TurnOn(EntityUid user, Entity uid) return false; } - if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) && - !TryComp(uid, out battery)) + if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery)) { _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid); _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user); @@ -210,7 +204,7 @@ public override bool TurnOn(EntityUid user, Entity uid) // To prevent having to worry about frame time in here. // Let's just say you need a whole second of charge before you can turn it on. // Simple enough. - if (component.Wattage > battery.CurrentCharge) + if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable())) { _audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid); _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user); @@ -227,24 +221,20 @@ public override bool TurnOn(EntityUid user, Entity uid) public void TryUpdate(Entity uid, float frameTime) { var component = uid.Comp; - if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) && - !TryComp(uid, out battery)) + if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery)) { TurnOff(uid, false); return; } - if (batteryUid == null) - return; - var appearanceComponent = EntityManager.GetComponentOrNull(uid); - var fraction = battery.CurrentCharge / battery.MaxCharge; - if (fraction >= 0.30) + var chargeFraction = _battery.GetChargeLevel(battery.Value.AsNullable()); + if (chargeFraction >= 0.30) { _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent); } - else if (fraction >= 0.10) + else if (chargeFraction >= 0.10) { _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower, appearanceComponent); } @@ -253,7 +243,7 @@ public void TryUpdate(Entity uid, float frameTime) _appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent); } - if (component.Activated && !_battery.TryUseCharge(batteryUid.Value, component.Wattage * frameTime, battery)) + if (component.Activated && !_battery.TryUseCharge(battery.Value.AsNullable(), component.Wattage * frameTime)) TurnOff(uid, false); UpdateLevel(uid); diff --git a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs index 948c44cd757..13ff6ee360d 100644 --- a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs +++ b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Emp; using Content.Server.Ghost; using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems; @@ -16,30 +15,23 @@ public override void Initialize() SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnGhostBoo); - - SubscribeLocalEvent(OnEmpPulse); } private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args) { - if (light.IgnoreGhostsBoo) - return; + if (light.IgnoreGhostsBoo || HasComp(uid)) + return; // The light is immune or already blinking. // check cooldown first to prevent abuse - var time = GameTiming.CurTime; - if (light.LastGhostBlink != null) - { - if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown) - return; - } + var curTime = GameTiming.CurTime; + if (light.LastGhostBlink != null && curTime <= light.LastGhostBlink + light.GhostBlinkingCooldown) + return; - light.LastGhostBlink = time; + light.LastGhostBlink = curTime; - ToggleBlinkingLight(uid, light, true); - uid.SpawnTimer(light.GhostBlinkingTime, () => - { - ToggleBlinkingLight(uid, light, false); - }); + var blinkingComp = EnsureComp(uid); + blinkingComp.StopBlinkingTime = curTime + light.GhostBlinkingTime; + Dirty(uid, blinkingComp); args.Handled = true; } @@ -49,16 +41,10 @@ private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent // TODO: Use ContainerFill dog if (light.HasLampOnSpawn != null) { - var entity = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent(uid).Coordinates); + var entity = Spawn(light.HasLampOnSpawn, Comp(uid).Coordinates); ContainerSystem.Insert(entity, light.LightBulbContainer); } // need this to update visualizers UpdateLight(uid, light); } - - private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args) - { - if (TryDestroyBulb(uid, component)) - args.Affected = true; - } } diff --git a/Content.Server/Lightning/LightningTargetSystem.cs b/Content.Server/Lightning/LightningTargetSystem.cs index 4a0ee23c5b7..eac23c30163 100644 --- a/Content.Server/Lightning/LightningTargetSystem.cs +++ b/Content.Server/Lightning/LightningTargetSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Lightning; using Content.Server.Lightning.Components; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Robust.Server.GameObjects; namespace Content.Server.Tesla.EntitySystems; @@ -26,7 +27,7 @@ private void OnHitByLightning(Entity uid, ref HitByLig { DamageSpecifier damage = new(); damage.DamageDict.Add("Structural", uid.Comp.DamageFromLightning); - _damageable.TryChangeDamage(uid, damage, true); + _damageable.ChangeDamage(uid.Owner, damage, true); if (uid.Comp.LightningExplode) { diff --git a/Content.Server/Lube/LubedSystem.cs b/Content.Server/Lube/LubedSystem.cs index 3c536dcceb2..01a2fa8dde5 100644 --- a/Content.Server/Lube/LubedSystem.cs +++ b/Content.Server/Lube/LubedSystem.cs @@ -1,9 +1,11 @@ +using Content.Shared.Hands; +using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; +using Content.Shared.Item; using Content.Shared.Lube; using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Popups; using Content.Shared.Throwing; -using Robust.Shared.Containers; using Robust.Shared.Random; namespace Content.Server.Lube; @@ -21,7 +23,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnHandPickUp); + SubscribeLocalEvent(OnHandPickUp); SubscribeLocalEvent(OnRefreshNameModifiers); } @@ -30,21 +32,38 @@ private void OnInit(EntityUid uid, LubedComponent component, ComponentInit args) _nameMod.RefreshNameModifiers(uid); } - private void OnHandPickUp(EntityUid uid, LubedComponent component, ContainerGettingInsertedAttemptEvent args) + /// + /// Note to whoever makes this predicted—there is a mispredict here that + /// would be nice to keep! If this is in shared, the client will predict + /// this and not run the pickup animation in + /// which would (probably) make this effect look less funny. You will + /// probably want to either tweak + /// to be able to cancel but still run the animation or something—we do want + /// the event to run before the animation for stuff like + /// . + /// + private void OnHandPickUp(Entity ent, ref BeforeGettingEquippedHandEvent args) { - if (component.SlipsLeft <= 0) + if (args.Cancelled) + return; + + if (ent.Comp.SlipsLeft <= 0) { - RemComp(uid); - _nameMod.RefreshNameModifiers(uid); + RemComp(ent); + _nameMod.RefreshNameModifiers(ent.Owner); return; } - component.SlipsLeft--; - args.Cancel(); - var user = args.Container.Owner; - _transform.SetCoordinates(uid, Transform(user).Coordinates); - _transform.AttachToGridOrMap(uid); - _throwing.TryThrow(uid, _random.NextVector2(), baseThrowSpeed: component.SlipStrength); - _popup.PopupEntity(Loc.GetString("lube-slip", ("target", Identity.Entity(uid, EntityManager))), user, user, PopupType.MediumCaution); + + ent.Comp.SlipsLeft--; + args.Cancelled = true; + + _transform.SetCoordinates(ent, Transform(args.User).Coordinates); + _transform.AttachToGridOrMap(ent); + _throwing.TryThrow(ent, _random.NextVector2(), ent.Comp.SlipStrength); + _popup.PopupEntity(Loc.GetString("lube-slip", ("target", Identity.Entity(ent, EntityManager))), + args.User, + args.User, + PopupType.MediumCaution); } private void OnRefreshNameModifiers(Entity entity, ref RefreshNameModifiersEvent args) diff --git a/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs index 04903241e36..7435a7706eb 100644 --- a/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs +++ b/Content.Server/Machines/EntitySystems/MultipartMachineSystem.cs @@ -75,7 +75,7 @@ public void ClearPartEntity(Entity ent, Enum part) public bool Rescan(Entity ent, EntityUid? user = null) { // Get all required transform information to start looking for the other parts based on their offset - if (!XformQuery.TryGetComponent(ent.Owner, out var xform) || !xform.Anchored) + if (!TryComp(ent.Owner, out TransformComponent? xform) || !xform.Anchored) return false; var gridUid = xform.GridUid; @@ -210,7 +210,7 @@ private void OnComponentStartup(Entity ent, ref Compo { // If anchored, perform a rescan of this machine when the component starts so we can immediately // jump to an assembled state if needed. - if (XformQuery.TryGetComponent(ent.Owner, out var xform) && xform.Anchored) + if (TryComp(ent.Owner, out TransformComponent? xform) && xform.Anchored) Rescan(ent); } @@ -240,7 +240,7 @@ private void OnMachineAnchorChanged(Entity ent, private void OnPartConstructionNodeChanged(Entity ent, ref AfterConstructionChangeEntityEvent args) { - if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform)) + if (!TryComp(ent.Owner, out TransformComponent? constructXform)) return; _lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange); @@ -283,7 +283,7 @@ private void OnPartAnchorChanged(Entity ent, ref // We're anchoring some construction, we have no idea which machine this might be for // so we have to just check everyone in range and perform a rescan. - if (!XformQuery.TryGetComponent(ent.Owner, out var constructXform)) + if (!TryComp(ent.Owner, out TransformComponent? constructXform)) return; _lookupSystem.GetEntitiesInRange(constructXform.Coordinates, MaximumRange, _entitiesInRange); diff --git a/Content.Server/MagicMirror/MagicMirrorSystem.cs b/Content.Server/MagicMirror/MagicMirrorSystem.cs deleted file mode 100644 index 0fd00fca249..00000000000 --- a/Content.Server/MagicMirror/MagicMirrorSystem.cs +++ /dev/null @@ -1,406 +0,0 @@ -using System.Linq; -using Content.Server.DoAfter; -using Content.Server.Humanoid; -using Content.Shared.DoAfter; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Markings; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Inventory; -using Content.Shared.MagicMirror; -using Content.Shared.Popups; -using Content.Shared.Tag; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Server.MagicMirror; - -/// -/// Allows humanoids to change their appearance mid-round. -/// -public sealed class MagicMirrorSystem : SharedMagicMirrorSystem -{ - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly MarkingManager _markings = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - - private static readonly ProtoId HidesHairTag = "HidesHair"; - - public override void Initialize() - { - base.Initialize(); - - Subs.BuiEvents(MagicMirrorUiKey.Key, - subs => - { - subs.Event(OnUiClosed); - subs.Event(OnMagicMirrorSelect); - subs.Event(OnTryMagicMirrorChangeColor); - subs.Event(OnTryMagicMirrorAddSlot); - subs.Event(OnTryMagicMirrorRemoveSlot); - }); - - - SubscribeLocalEvent(OnSelectSlotDoAfter); - SubscribeLocalEvent(OnChangeColorDoAfter); - SubscribeLocalEvent(OnRemoveSlotDoAfter); - SubscribeLocalEvent(OnAddSlotDoAfter); - } - - private void OnMagicMirrorSelect(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectMessage message) - { - if (component.Target is not { } target) - return; - - // Check if the target getting their hair altered has any clothes that hides their hair - if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value)) - { - _popup.PopupEntity( - component.Target == message.Actor - ? Loc.GetString("magic-mirror-blocked-by-hat-self") - : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))), - message.Actor, - message.Actor, - PopupType.Medium); - return; - } - - _doAfterSystem.Cancel(component.DoAfter); - component.DoAfter = null; - - var doafterTime = component.SelectSlotTime; - if (component.Target == message.Actor) - doafterTime /= 3; - - var doAfter = new MagicMirrorSelectDoAfterEvent() - { - Category = message.Category, - Slot = message.Slot, - Marking = message.Marking, - }; - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid) - { - DistanceThreshold = SharedInteractionSystem.InteractionRange, - BreakOnDamage = true, - BreakOnMove = true, - NeedHand = true, - }, - out var doAfterId); - - if (component.Target == message.Actor) - { - _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium); - } - else - { - _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium); - } - - component.DoAfter = doAfterId; - _audio.PlayPvs(component.ChangeHairSound, uid); - } - - private void OnSelectSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectDoAfterEvent args) - { - if (args.Handled || args.Target == null || args.Cancelled) - return; - - if (component.Target != args.Target) - return; - - MarkingCategories category; - - switch (args.Category) - { - case MagicMirrorCategory.Hair: - category = MarkingCategories.Hair; - break; - case MagicMirrorCategory.FacialHair: - category = MarkingCategories.FacialHair; - break; - default: - return; - } - - _humanoid.SetMarkingId(component.Target.Value, category, args.Slot, args.Marking); - - UpdateInterface(uid, component.Target.Value, component); - } - - private void OnTryMagicMirrorChangeColor(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorMessage message) - { - if (component.Target is not { } target) - return; - - // Check if the target getting their hair altered has any clothes that hides their hair - if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value)) - { - _popup.PopupEntity( - component.Target == message.Actor - ? Loc.GetString("magic-mirror-blocked-by-hat-self") - : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))), - message.Actor, - message.Actor, - PopupType.Medium); - return; - } - - _doAfterSystem.Cancel(component.DoAfter); - component.DoAfter = null; - - var doafterTime = component.ChangeSlotTime; - if (component.Target == message.Actor) - doafterTime /= 3; - - var doAfter = new MagicMirrorChangeColorDoAfterEvent() - { - Category = message.Category, - Slot = message.Slot, - Colors = message.Colors, - }; - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid) - { - BreakOnDamage = true, - BreakOnMove = true, - NeedHand = true - }, - out var doAfterId); - - if (component.Target == message.Actor) - { - _popup.PopupEntity(Loc.GetString("magic-mirror-change-color-self"), component.Target.Value, component.Target.Value, PopupType.Medium); - } - else - { - _popup.PopupEntity(Loc.GetString("magic-mirror-change-color-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium); - } - - component.DoAfter = doAfterId; - } - private void OnChangeColorDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorDoAfterEvent args) - { - if (args.Handled || args.Target == null || args.Cancelled) - return; - - if (component.Target != args.Target) - return; - - MarkingCategories category; - switch (args.Category) - { - case MagicMirrorCategory.Hair: - category = MarkingCategories.Hair; - break; - case MagicMirrorCategory.FacialHair: - category = MarkingCategories.FacialHair; - break; - default: - return; - } - - _humanoid.SetMarkingColor(component.Target.Value, category, args.Slot, args.Colors); - - // using this makes the UI feel like total ass - // que - // UpdateInterface(uid, component.Target, message.Session); - } - - private void OnTryMagicMirrorRemoveSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotMessage message) - { - if (component.Target is not { } target) - return; - - // Check if the target getting their hair altered has any clothes that hides their hair - if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value)) - { - _popup.PopupEntity( - component.Target == message.Actor - ? Loc.GetString("magic-mirror-blocked-by-hat-self") - : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))), - message.Actor, - message.Actor, - PopupType.Medium); - return; - } - - _doAfterSystem.Cancel(component.DoAfter); - component.DoAfter = null; - - var doafterTime = component.RemoveSlotTime; - if (component.Target == message.Actor) - doafterTime /= 3; - - var doAfter = new MagicMirrorRemoveSlotDoAfterEvent() - { - Category = message.Category, - Slot = message.Slot, - }; - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid) - { - DistanceThreshold = SharedInteractionSystem.InteractionRange, - BreakOnDamage = true, - NeedHand = true - }, - out var doAfterId); - - if (component.Target == message.Actor) - { - _popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium); - } - else - { - _popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium); - } - - component.DoAfter = doAfterId; - _audio.PlayPvs(component.ChangeHairSound, uid); - } - - private void OnRemoveSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotDoAfterEvent args) - { - if (args.Handled || args.Target == null || args.Cancelled) - return; - - if (component.Target != args.Target) - return; - - MarkingCategories category; - - switch (args.Category) - { - case MagicMirrorCategory.Hair: - category = MarkingCategories.Hair; - break; - case MagicMirrorCategory.FacialHair: - category = MarkingCategories.FacialHair; - break; - default: - return; - } - - _humanoid.RemoveMarking(component.Target.Value, category, args.Slot); - - UpdateInterface(uid, component.Target.Value, component); - } - - private void OnTryMagicMirrorAddSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotMessage message) - { - if (component.Target == null) - return; - - // Check if the target getting their hair altered has any clothes that hides their hair - if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value)) - { - _popup.PopupEntity( - component.Target == message.Actor - ? Loc.GetString("magic-mirror-blocked-by-hat-self") - : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))), - message.Actor, - message.Actor, - PopupType.Medium); - return; - } - - _doAfterSystem.Cancel(component.DoAfter); - component.DoAfter = null; - - var doafterTime = component.AddSlotTime; - if (component.Target == message.Actor) - doafterTime /= 3; - - var doAfter = new MagicMirrorAddSlotDoAfterEvent() - { - Category = message.Category, - }; - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: component.Target.Value, used: uid) - { - BreakOnDamage = true, - BreakOnMove = true, - NeedHand = true, - }, - out var doAfterId); - - if (component.Target == message.Actor) - { - _popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium); - } - else - { - _popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium); - } - - component.DoAfter = doAfterId; - _audio.PlayPvs(component.ChangeHairSound, uid); - } - private void OnAddSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotDoAfterEvent args) - { - if (args.Handled || args.Target == null || args.Cancelled || !TryComp(component.Target, out HumanoidAppearanceComponent? humanoid)) - return; - - MarkingCategories category; - - switch (args.Category) - { - case MagicMirrorCategory.Hair: - category = MarkingCategories.Hair; - break; - case MagicMirrorCategory.FacialHair: - category = MarkingCategories.FacialHair; - break; - default: - return; - } - - var marking = _markings.MarkingsByCategoryAndSpecies(category, humanoid.Species).Keys.FirstOrDefault(); - - if (string.IsNullOrEmpty(marking)) - return; - - _humanoid.AddMarking(component.Target.Value, marking, Color.Black); - - UpdateInterface(uid, component.Target.Value, component); - - } - - private void OnUiClosed(Entity ent, ref BoundUIClosedEvent args) - { - ent.Comp.Target = null; - Dirty(ent); - } - - /// - /// Helper function that checks if the wearer has anything on their head - /// Or if they have any clothes that hides their hair - /// - private bool CheckHeadSlotOrClothes(EntityUid user, EntityUid target) - { - if (TryComp(target, out var inventoryComp)) - { - // any hat whatsoever will block haircutting - if (_inventory.TryGetSlotEntity(target, "head", out var hat, inventoryComp)) - { - return true; - } - - // maybe there's some kind of armor that has the HidesHair tag as well, so check every slot for it - var slots = _inventory.GetSlotEnumerator((target, inventoryComp), SlotFlags.WITHOUT_POCKET); - while (slots.MoveNext(out var slot)) - { - if (slot.ContainedEntity != null && _tagSystem.HasTag(slot.ContainedEntity.Value, HidesHairTag)) - { - return true; - } - } - } - - return false; - } -} diff --git a/Content.Server/Mapping/MappingManager.cs b/Content.Server/Mapping/MappingManager.cs index 3a46b301e83..dc0dcf4d42c 100644 --- a/Content.Server/Mapping/MappingManager.cs +++ b/Content.Server/Mapping/MappingManager.cs @@ -14,6 +14,7 @@ namespace Content.Server.Mapping; public sealed class MappingManager : IPostInjectInit { +#if !FULL_RELEASE [Dependency] private readonly IAdminManager _admin = default!; [Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly IServerNetManager _net = default!; @@ -23,6 +24,7 @@ public sealed class MappingManager : IPostInjectInit private ISawmill _sawmill = default!; private ZStdCompressionContext _zstd = default!; +#endif public void PostInject() { diff --git a/Content.Server/Maps/Conditions/HolidayMapCondition.cs b/Content.Server/Maps/Conditions/HolidayMapCondition.cs index da971c78f32..b7edc3f6c28 100644 --- a/Content.Server/Maps/Conditions/HolidayMapCondition.cs +++ b/Content.Server/Maps/Conditions/HolidayMapCondition.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Holiday; +using Content.Shared.Maps; namespace Content.Server.Maps.Conditions; diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index 9638dffaa7c..9914b7bde2b 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Server.GameTicking; using Content.Shared.CCVar; +using Content.Shared.Maps; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; diff --git a/Content.Server/Maps/GameMapPoolPrototype.cs b/Content.Server/Maps/GameMapPoolPrototype.cs index b2ba64a50c6..831dea556d8 100644 --- a/Content.Server/Maps/GameMapPoolPrototype.cs +++ b/Content.Server/Maps/GameMapPoolPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; diff --git a/Content.Server/Maps/IGameMapManager.cs b/Content.Server/Maps/IGameMapManager.cs index 02433c7b792..d05c42a78c9 100644 --- a/Content.Server/Maps/IGameMapManager.cs +++ b/Content.Server/Maps/IGameMapManager.cs @@ -1,3 +1,5 @@ +using Content.Shared.Maps; + namespace Content.Server.Maps; /// diff --git a/Content.Server/Maps/MapMigrationSystem.cs b/Content.Server/Maps/MapMigrationSystem.cs index 389652a0c3e..7cd1104c49e 100644 --- a/Content.Server/Maps/MapMigrationSystem.cs +++ b/Content.Server/Maps/MapMigrationSystem.cs @@ -17,9 +17,9 @@ namespace Content.Server.Maps; /// public sealed class MapMigrationSystem : EntitySystem { -#pragma warning disable CS0414 +#if DEBUG [Dependency] private readonly IPrototypeManager _protoMan = default!; -#pragma warning restore CS0414 +#endif [Dependency] private readonly IResourceManager _resMan = default!; private const string MigrationFile = "/migration.yml"; diff --git a/Content.Server/Maps/TileGridSplitSystem.cs b/Content.Server/Maps/TileGridSplitSystem.cs new file mode 100644 index 00000000000..fef0efed2b2 --- /dev/null +++ b/Content.Server/Maps/TileGridSplitSystem.cs @@ -0,0 +1,74 @@ +using System.Numerics; +using Content.Shared.Maps; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Maps; + +/// +/// This system handles transferring data when a grid is split. +/// +public sealed class TileGridSplitSystem : EntitySystem +{ + [Dependency] private readonly SharedMapSystem _maps = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGridSplit); + } + + /// + /// Transfer tile history from the old grid to the new grids. + /// + private void OnGridSplit(ref GridSplitEvent ev) + { + if (!TryComp(ev.Grid, out var oldHistory)) + return; + + var oldGrid = Comp(ev.Grid); + + foreach (var gridUid in ev.NewGrids) + { + // ensure the new grid has a history component and get its grid component + var newHistory = EnsureComp(gridUid); + var newGrid = Comp(gridUid); + + foreach (var tile in _maps.GetAllTiles(gridUid, newGrid)) + { + // calculate where this tile was on the old grid + var oldIndices = _maps.LocalToTile(ev.Grid, oldGrid, new EntityCoordinates(gridUid, new Vector2(tile.GridIndices.X + 0.5f, tile.GridIndices.Y + 0.5f))); + + var chunkIndices = SharedMapSystem.GetChunkIndices(oldIndices, TileSystem.ChunkSize); + if (oldHistory.ChunkHistory.TryGetValue(chunkIndices, out var oldChunk) && + oldChunk.History.TryGetValue(oldIndices, out var history)) + { + // now we move the history from the old grid to the new grid + var newChunkIndices = SharedMapSystem.GetChunkIndices(tile.GridIndices, TileSystem.ChunkSize); + if (!newHistory.ChunkHistory.TryGetValue(newChunkIndices, out var newChunk)) + { + newChunk = new TileHistoryChunk(); + newHistory.ChunkHistory[newChunkIndices] = newChunk; + } + + newChunk.History[tile.GridIndices] = new List>(history); + newChunk.LastModified = _timing.CurTick; + + // clean up the old history + oldChunk.History.Remove(oldIndices); + if (oldChunk.History.Count == 0) + oldHistory.ChunkHistory.Remove(chunkIndices); + else + oldChunk.LastModified = _timing.CurTick; + } + } + + Dirty(gridUid, newHistory); + } + + Dirty(ev.Grid, oldHistory); + } +} diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs index 15ff93833de..c00c938865f 100644 --- a/Content.Server/Materials/MaterialReclaimerSystem.cs +++ b/Content.Server/Materials/MaterialReclaimerSystem.cs @@ -4,7 +4,6 @@ using Content.Server.Popups; using Content.Server.Stack; using Content.Server.Wires; -using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; @@ -25,6 +24,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Linq; +using Content.Shared.Gibbing; using Content.Shared.Humanoid; namespace Content.Server.Materials; @@ -39,7 +39,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem [Dependency] private readonly OpenableSystem _openable = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly SharedBodySystem _body = default!; //bobby + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly SharedMindSystem _mind = default!; @@ -112,7 +112,7 @@ private void OnSuicideByEnvironment(Entity entity, r Filter.PvsExcept(victim, entityManager: EntityManager), true); - _body.GibBody(victim, true); + _gibbing.Gib(victim); _appearance.SetData(entity.Owner, RecyclerVisuals.Bloody, true); args.Handled = true; } @@ -189,11 +189,11 @@ public override void Reclaim(EntityUid uid, if (CanGib(uid, item, component)) { - var logImpact = HasComp(item) ? LogImpact.Extreme : LogImpact.Medium; + var logImpact = HasComp(item) ? LogImpact.Extreme : LogImpact.Medium; _adminLogger.Add(LogType.Gib, logImpact, $"{ToPrettyString(item):victim} was gibbed by {ToPrettyString(uid):entity} "); if (component.ReclaimSolutions) SpawnChemicalsFromComposition(uid, item, completion, false, component, xform); - _body.GibBody(item, true); + _gibbing.Gib(item); _appearance.SetData(uid, RecyclerVisuals.Bloody, true); } else diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs index 936bd06e607..a090aa3864f 100644 --- a/Content.Server/Materials/MaterialStorageSystem.cs +++ b/Content.Server/Materials/MaterialStorageSystem.cs @@ -76,7 +76,7 @@ private void OnEjectMessage(EjectMaterialMessage msg, EntitySessionEventArgs arg return; var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value; - var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity)); + var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity.Value)); volume = sheetsToExtract * volumePerSheet; } @@ -192,7 +192,7 @@ public List SpawnMultipleFromMaterial(int amount, MaterialPrototype m if (amountToSpawn == 0) return new List(); - return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates); + return _stackSystem.SpawnMultipleAtPosition(materialProto.StackEntity.Value, amountToSpawn, coordinates); } /// diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs index 7b0c2f64723..7b4c7f32f49 100644 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -1,19 +1,19 @@ -using System.Linq; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; using Content.Server.Mech.Components; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Shared.ActionBlocker; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Mech; using Content.Shared.Mech.Components; using Content.Shared.Mech.EntitySystems; using Content.Shared.Movement.Events; using Content.Shared.Popups; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Tools; using Content.Shared.Tools.Components; using Content.Shared.Tools.Systems; @@ -25,6 +25,8 @@ using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using System.Linq; +using Content.Shared.Atmos; namespace Content.Server.Mech.Systems; @@ -33,7 +35,7 @@ public sealed partial class MechSystem : SharedMechSystem { [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; @@ -112,7 +114,7 @@ private void OnInsertBattery(EntityUid uid, MechComponent component, EntInserted if (args.Container != component.BatterySlot || !TryComp(args.Entity, out var battery)) return; - component.Energy = battery.CurrentCharge; + component.Energy = _battery.GetCharge((args.Entity, battery)); component.MaxEnergy = battery.MaxCharge; Dirty(uid, component); @@ -219,7 +221,7 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE { BreakOnMove = true, }; - _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large); + _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", Identity.Entity(args.User, EntityManager))), uid, PopupType.Large); _doAfter.TryStartDoAfter(doAfterEventArgs); } @@ -235,7 +237,7 @@ private void OnMechEntry(EntityUid uid, MechComponent component, MechEntryEvent if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User)) { - _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User); + _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), Identity.Entity(args.User, EntityManager)); return; } @@ -257,7 +259,7 @@ private void OnMechExit(EntityUid uid, MechComponent component, MechExitEvent ar private void OnDamageChanged(EntityUid uid, MechComponent component, DamageChangedEvent args) { - var integrity = component.MaxIntegrity - args.Damageable.TotalDamage; + var integrity = component.MaxIntegrity - _damageable.GetTotalDamage((uid, args.Damageable)); SetIntegrity(uid, integrity, component); if (args.DamageIncreased && @@ -265,7 +267,7 @@ private void OnDamageChanged(EntityUid uid, MechComponent component, DamageChang component.PilotSlot.ContainedEntity != null) { var damage = args.DamageDelta * component.MechToPilotDamageMultiplier; - _damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage); + _damageable.ChangeDamage(component.PilotSlot.ContainedEntity.Value, damage); } } @@ -340,11 +342,13 @@ public override bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechCompo if (!TryComp(battery, out var batteryComp)) return false; - _battery.SetCharge(battery!.Value, batteryComp.CurrentCharge + delta.Float(), batteryComp); - if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them + _battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float()); + // TODO: Power cells are predicted now, so no need to duplicate the charge level + var charge = _battery.GetCharge((battery.Value, batteryComp)); + if (charge != component.Energy) //if there's a discrepency, we have to resync them { - Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}"); - component.Energy = batteryComp.CurrentCharge; + Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}"); + component.Energy = charge; Dirty(uid, component); } _actionBlocker.UpdateCanMove(uid); @@ -360,7 +364,7 @@ public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? comp return; _container.Insert(toInsert, component.BatterySlot); - component.Energy = battery.CurrentCharge; + component.Energy = _battery.GetCharge((toInsert, battery)); component.MaxEnergy = battery.MaxCharge; _actionBlocker.UpdateCanMove(uid); diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs index 61d36f98b96..bc295545b0b 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Chemistry.Components; using Content.Shared.Storage; namespace Content.Server.Medical.BiomassReclaimer @@ -34,10 +35,10 @@ public sealed partial class BiomassReclaimerComponent : Component public float CurrentExpectedYield = 0f; /// - /// The reagent that will be spilled while processing a mob. + /// The reagents that will be spilled while processing a mob. /// [ViewVariables] - public string? BloodReagent; + public Solution? BloodReagents = null; /// /// Entities that can be randomly spawned while processing a mob. diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index e029071574b..f4b7ec838af 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -7,7 +7,7 @@ using Content.Shared.Audio; using Content.Shared.Body.Components; using Content.Shared.CCVar; -using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Climbing.Events; using Content.Shared.Construction.Components; using Content.Shared.Database; @@ -45,6 +45,7 @@ public sealed class BiomassReclaimerSystem : EntitySystem [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly PuddleSystem _puddleSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; @@ -68,10 +69,8 @@ public override void Update(float frameTime) if (reclaimer.RandomMessTimer <= 0) { - if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagent is not null) + if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagents is { } blood) { - Solution blood = new(); - blood.AddReagent(reclaimer.BloodReagent, 50); _puddleSystem.TrySpillAt(uid, blood, out _); } if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0) @@ -92,7 +91,7 @@ public override void Update(float frameTime) reclaimer.CurrentExpectedYield = reclaimer.CurrentExpectedYield - actualYield; // store non-integer leftovers _material.SpawnMultipleFromMaterial(actualYield, BiomassPrototype, Transform(uid).Coordinates); - reclaimer.BloodReagent = null; + reclaimer.BloodReagents = null; reclaimer.SpawnedEntities.Clear(); RemCompDeferred(uid); } @@ -208,9 +207,11 @@ private void StartProcessing(EntityUid toProcess, Entity(ent); - if (TryComp(toProcess, out var stream)) + if (TryComp(toProcess, out var stream) && + _solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution)) { - component.BloodReagent = stream.BloodReagent; + component.BloodReagents = solution.Clone(); + component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume); } if (TryComp(toProcess, out var butcherableComponent)) { @@ -253,7 +254,7 @@ private bool CanGib(Entity reclaimer, EntityUid dragg // Reject souled bodies in easy mode. if (_configManager.GetCVar(CCVars.BiomassEasyMode) && - HasComp(dragged) && + HasComp(dragged) && _minds.TryGetMind(dragged, out _, out var mind)) { if (mind.UserId != null && _playerManager.TryGetSessionById(mind.UserId.Value, out _)) diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs index 9c7ccd53304..d5d882a178d 100644 --- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs +++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs @@ -26,6 +26,12 @@ public sealed partial class HealthAnalyzerComponent : Component [DataField] public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); + /// + /// If the last state of the health analyzer was active (e.g. they are in range of the patient). + /// + [DataField] + public bool IsAnalyzerActive = false; + /// /// How long it takes to scan someone. /// diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs index 0e1d27a3c55..92c5f59b157 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs @@ -1,7 +1,7 @@ using System.Linq; using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork.Systems; -using Content.Server.PowerCell; +using Content.Shared.PowerCell; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Medical.CrewMonitoring; diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index 20dc1149180..9668b0ab58b 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -1,18 +1,13 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Unary.EntitySystems; using Content.Server.Medical.Components; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.Nodes; -using Content.Server.Temperature.Components; using Content.Shared.Atmos; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Medical.Cryogenics; -using Content.Shared.MedicalScanner; -using Content.Shared.UserInterface; -using Robust.Shared.Containers; namespace Content.Server.Medical; @@ -20,47 +15,56 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem { [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly GasCanisterSystem _gasCanisterSystem = default!; + [Dependency] private readonly GasAnalyzerSystem _gasAnalyzerSystem = default!; + [Dependency] private readonly HealthAnalyzerSystem _healthAnalyzerSystem = default!; [Dependency] private readonly NodeContainerSystem _nodeContainer = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnActivateUI); SubscribeLocalEvent(OnCryoPodUpdateAtmosphere); SubscribeLocalEvent(OnGasAnalyzed); - SubscribeLocalEvent(OnEjected); } - private void OnActivateUI(Entity entity, ref AfterActivatableUIOpenEvent args) + public override void Update(float frameTime) { - if (!entity.Comp.BodyContainer.ContainedEntity.HasValue) - return; + base.Update(frameTime); - TryComp(entity.Comp.BodyContainer.ContainedEntity, out var temp); - TryComp(entity.Comp.BodyContainer.ContainedEntity, out var bloodstream); + var query = EntityQueryEnumerator(); - if (TryComp(entity, out var healthAnalyzer)) + while (query.MoveNext(out var uid, out _, out var cryoPod)) { - healthAnalyzer.ScannedEntity = entity.Comp.BodyContainer.ContainedEntity; + if (Timing.CurTime < cryoPod.NextUiUpdateTime) + continue; + + cryoPod.NextUiUpdateTime += cryoPod.UiUpdateInterval; + Dirty(uid, cryoPod); + UpdateUi((uid, cryoPod)); } + } + + protected override void UpdateUi(Entity entity) + { + if (!UI.IsUiOpen(entity.Owner, CryoPodUiKey.Key) + || !TryComp(entity, out CryoPodAirComponent? air)) + return; - // TODO: This should be a state my dude - _uiSystem.ServerSendUiMessage( + var patient = entity.Comp.BodyContainer.ContainedEntity; + var gasMix = _gasAnalyzerSystem.GenerateGasMixEntry("Cryo pod", air.Air); + var (beakerCapacity, beaker) = GetBeakerInfo(entity); + var injecting = GetInjectingReagents(entity); + var health = _healthAnalyzerSystem.GetHealthAnalyzerUiState(patient); + health.ScanMode = true; + var hasDamage = patient is null ? false : _damageable.GetTotalDamage(patient.Value) > 0; + + UI.ServerSendUiMessage( entity.Owner, - HealthAnalyzerUiKey.Key, - new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity), - temp?.CurrentTemperature ?? 0, - (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value, - bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) - ? bloodSolution.FillFraction - : 0, - null, - null, - null - )); + CryoPodUiKey.Key, + new CryoPodUserMessage(gasMix, health, beakerCapacity, beaker, injecting, hasDamage) + ); } private void OnCryoPodUpdateAtmosphere(Entity entity, ref AtmosDeviceUpdateEvent args) @@ -96,15 +100,4 @@ private void OnGasAnalyzed(Entity entity, ref GasAnalyzerScanE args.GasMixtures.Add((entity.Comp.PortName, portAirLocal)); } } - - private void OnEjected(Entity cryoPod, ref EntRemovedFromContainerMessage args) - { - if (TryComp(cryoPod.Owner, out var healthAnalyzer)) - { - healthAnalyzer.ScannedEntity = null; - } - - // if body is ejected - no need to display health-analyzer - _uiSystem.CloseUi(cryoPod.Owner, HealthAnalyzerUiKey.Key); - } } diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index 94d62641cec..34260fac431 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -1,245 +1,19 @@ -using Content.Server.Atmos.Rotting; -using Content.Server.Chat.Systems; -using Content.Server.DoAfter; -using Content.Server.Electrocution; using Content.Server.EUI; using Content.Server.Ghost; -using Content.Server.Popups; -using Content.Server.PowerCell; -using Content.Shared.Traits.Assorted; -using Content.Shared.Damage; -using Content.Shared.DoAfter; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Interaction.Events; -using Content.Shared.Item.ItemToggle; using Content.Shared.Medical; using Content.Shared.Mind; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.PowerCell; -using Content.Shared.Timing; -using Content.Shared.Toggleable; -using Robust.Shared.Audio.Systems; using Robust.Shared.Player; namespace Content.Server.Medical; -/// -/// This handles interactions and logic relating to -/// -public sealed class DefibrillatorSystem : EntitySystem +public sealed class DefibrillatorSystem : SharedDefibrillatorSystem { - [Dependency] private readonly ChatSystem _chatManager = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly DoAfterSystem _doAfter = default!; - [Dependency] private readonly ElectrocutionSystem _electrocution = default!; - [Dependency] private readonly EuiManager _euiManager = default!; + [Dependency] private readonly EuiManager _eui = default!; [Dependency] private readonly ISharedPlayerManager _player = default!; - [Dependency] private readonly ItemToggleSystem _toggle = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly RottingSystem _rotting = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - /// - public override void Initialize() + protected override void OpenReturnToBodyEui(Entity mind, ICommonSession session) { - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnAfterInteract(EntityUid uid, DefibrillatorComponent component, AfterInteractEvent args) - { - if (args.Handled || args.Target is not { } target) - return; - - args.Handled = TryStartZap(uid, target, args.User, component); - } - - private void OnDoAfter(EntityUid uid, DefibrillatorComponent component, DefibrillatorZapDoAfterEvent args) - { - if (args.Handled || args.Cancelled) - return; - - if (args.Target is not { } target) - return; - - if (!CanZap(uid, target, args.User, component)) - return; - - args.Handled = true; - Zap(uid, target, args.User, component); - } - - /// - /// Checks if you can actually defib a target. - /// - /// Uid of the defib - /// Uid of the target getting defibbed - /// Uid of the entity using the defibrillator - /// Defib component - /// - /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are. - /// - /// - /// Returns true if the target is valid to be defibed, false otherwise. - /// - public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false) - { - if (!Resolve(uid, ref component)) - return false; - - if (!_toggle.IsActivated(uid)) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("defibrillator-not-on"), uid, user.Value); - return false; - } - - if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay), component.DelayId)) - return false; - - if (!TryComp(target, out var mobState)) - return false; - - if (!_powerCell.HasActivatableCharge(uid, user: user)) - return false; - - if (!targetCanBeAlive && _mobState.IsAlive(target, mobState)) - return false; - - if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState)) - return false; - - return true; - } - - /// - /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after. - /// - /// Uid of the defib - /// Uid of the target getting defibbed - /// Uid of the entity using the defibrillator - /// Defib component - /// - /// Returns true if the defibrillation do-after started, otherwise false. - /// - public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (!CanZap(uid, target, user, component)) - return false; - - _audio.PlayPvs(component.ChargeSound, uid); - return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(), - uid, target, uid) - { - NeedHand = true, - BreakOnMove = !component.AllowDoAfterMovement - }); - } - - /// - /// Tries to defibrillate the target with the given defibrillator. - /// - public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (!_powerCell.TryUseActivatableCharge(uid, user: user)) - return; - - var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target); - RaiseLocalEvent(user, selfEvent); - - target = selfEvent.DefibTarget; - - // Ensure thet new target is still valid. - if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true)) - return; - - var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target); - RaiseLocalEvent(target, targetEvent); - - target = targetEvent.DefibTarget; - - if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true)) - return; - - if (!TryComp(target, out var mob) || - !TryComp(target, out var thresholds)) - return; - - _audio.PlayPvs(component.ZapSound, uid); - _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); - if (!TryComp(uid, out var useDelay)) - return; - _useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId); - _useDelay.TryResetDelay((uid, useDelay), id: component.DelayId); - - ICommonSession? session = null; - - var dead = true; - if (_rotting.IsRotten(target)) - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-rotten"), - InGameICChatType.Speak, true); - } - else if (TryComp(target, out var unrevivable)) - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString(unrevivable.ReasonMessage), - InGameICChatType.Speak, true); - } - else - { - if (_mobState.IsDead(target, mob)) - _damageable.TryChangeDamage(target, component.ZapHeal, true, origin: uid); - - if (_mobThreshold.TryGetThresholdForState(target, MobState.Dead, out var threshold) && - TryComp(target, out var damageableComponent) && - damageableComponent.TotalDamage < threshold) - { - _mobState.ChangeMobState(target, MobState.Critical, mob, uid); - dead = false; - } - - if (_mind.TryGetMind(target, out _, out var mind) && - _player.TryGetSessionById(mind.UserId, out var playerSession)) - { - session = playerSession; - // notify them they're being revived. - if (mind.CurrentEntity != target) - { - _euiManager.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session); - } - } - else - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-no-mind"), - InGameICChatType.Speak, true); - } - } - - var sound = dead || session == null - ? component.FailureSound - : component.SuccessSound; - _audio.PlayPvs(sound, uid); - - // if we don't have enough power left for another shot, turn it off - if (!_powerCell.HasActivatableCharge(uid)) - _toggle.TryDeactivate(uid); - - // TODO clean up this clown show above - var ev = new TargetDefibrillatedEvent(user, (uid, component)); - RaiseLocalEvent(target, ref ev); + _eui.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session); } } diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 11e4ed4fcf7..9986bf15a08 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -1,9 +1,7 @@ using Content.Server.Medical.Components; -using Content.Server.PowerCell; -using Content.Server.Temperature.Components; using Content.Shared.Body.Components; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -13,11 +11,14 @@ using Content.Shared.MedicalScanner; using Content.Shared.Mobs.Components; using Content.Shared.Popups; +using Content.Shared.PowerCell; +using Content.Shared.Temperature.Components; using Content.Shared.Traits.Assorted; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Timing; +using Content.Server.Body.Systems; namespace Content.Server.Medical; @@ -32,6 +33,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; public override void Initialize() { @@ -67,11 +69,12 @@ public override void Update(float frameTime) var patientCoordinates = Transform(patient).Coordinates; if (component.MaxScanRange != null && !_transformSystem.InRange(patientCoordinates, transform.Coordinates, component.MaxScanRange.Value)) { - //Range too far, disable updates - StopAnalyzingEntity((uid, component), patient); + //Range too far, disable updates until they are back in range + PauseAnalyzingEntity((uid, component), patient); continue; } + component.IsAnalyzerActive = true; UpdateScannedUser(uid, patient, true); } } @@ -81,7 +84,7 @@ public override void Update(float frameTime) /// private void OnAfterInteract(Entity uid, ref AfterInteractEvent args) { - if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid, user: args.User)) + if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User)) return; _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid); @@ -101,7 +104,7 @@ private void OnAfterInteract(Entity uid, ref AfterInter private void OnDoAfter(Entity uid, ref HealthAnalyzerDoAfterEvent args) { - if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User)) + if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User)) return; if (!uid.Comp.Silent) @@ -177,6 +180,21 @@ private void StopAnalyzingEntity(Entity healthAnalyzer, UpdateScannedUser(healthAnalyzer, target, false); } + + /// + /// If the scanner is active, sends one last update and sets it to inactive. + /// + /// The health analyzer that's receiving the updates + /// The entity to analyze + private void PauseAnalyzingEntity(Entity healthAnalyzer, EntityUid target) + { + if (!healthAnalyzer.Comp.IsAnalyzerActive) + return; + + UpdateScannedUser(healthAnalyzer, target, false); + healthAnalyzer.Comp.IsAnalyzerActive = false; + } + /// /// Send an update for the target to the healthAnalyzer /// @@ -185,39 +203,58 @@ private void StopAnalyzingEntity(Entity healthAnalyzer, /// True makes the UI show ACTIVE, False makes the UI show INACTIVE public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode) { - if (!_uiSystem.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key)) + if (!_uiSystem.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key) + || !HasComp(target)) return; - if (!HasComp(target)) - return; + var uiState = GetHealthAnalyzerUiState(target); + uiState.ScanMode = scanMode; + + _uiSystem.ServerSendUiMessage( + healthAnalyzer, + HealthAnalyzerUiKey.Key, + new HealthAnalyzerScannedUserMessage(uiState) + ); + } + + /// + /// Creates a HealthAnalyzerState based on the current state of an entity. + /// + /// The entity being scanned + /// + public HealthAnalyzerUiState GetHealthAnalyzerUiState(EntityUid? target) + { + if (!target.HasValue || !HasComp(target)) + return new HealthAnalyzerUiState(); + var entity = target.Value; var bodyTemperature = float.NaN; - if (TryComp(target, out var temp)) + if (TryComp(entity, out var temp)) bodyTemperature = temp.CurrentTemperature; var bloodAmount = float.NaN; var bleeding = false; var unrevivable = false; - if (TryComp(target, out var bloodstream) && - _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, + if (TryComp(entity, out var bloodstream) && + _solutionContainerSystem.ResolveSolution(entity, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) { - bloodAmount = bloodSolution.FillFraction; + bloodAmount = _bloodstreamSystem.GetBloodLevel(entity); bleeding = bloodstream.BleedAmount > 0; } - if (TryComp(target, out var unrevivableComp) && unrevivableComp.Analyzable) + if (TryComp(entity, out var unrevivableComp) && unrevivableComp.Analyzable) unrevivable = true; - _uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage( - GetNetEntity(target), + return new HealthAnalyzerUiState( + GetNetEntity(entity), bodyTemperature, bloodAmount, - scanMode, + null, bleeding, unrevivable - )); + ); } } diff --git a/Content.Server/Medical/InsideCryoPodSystem.cs b/Content.Server/Medical/InsideCryoPodSystem.cs index 21827c105f9..f04bb3cc40e 100644 --- a/Content.Server/Medical/InsideCryoPodSystem.cs +++ b/Content.Server/Medical/InsideCryoPodSystem.cs @@ -1,6 +1,6 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Systems; +using Content.Server.Body.Systems; using Content.Server.Medical.Components; +using Content.Shared.Atmos; using Content.Shared.Medical.Cryogenics; namespace Content.Server.Medical diff --git a/Content.Server/Medical/MedicalScannerSystem.cs b/Content.Server/Medical/MedicalScannerSystem.cs index b24690e204a..82bbfd9c6d9 100644 --- a/Content.Server/Medical/MedicalScannerSystem.cs +++ b/Content.Server/Medical/MedicalScannerSystem.cs @@ -10,7 +10,7 @@ using Content.Server.DeviceLinking.Systems; using Content.Shared.DeviceLinking.Events; using Content.Server.Power.EntitySystems; -using Content.Shared.Body.Components; +using Content.Shared.Body; using Content.Shared.Climbing.Systems; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index 7af093b1783..5674b149e3e 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -1,8 +1,6 @@ using Content.Server.DeviceNetwork.Systems; -using Content.Server.Emp; using Content.Server.Medical.CrewMonitoring; using Content.Shared.DeviceNetwork.Components; -using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensors; using Robust.Shared.Timing; @@ -14,14 +12,6 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnEmpPulse); - SubscribeLocalEvent(OnEmpFinished); - } - public override void Update(float frameTime) { base.Update(frameTime); @@ -37,12 +27,11 @@ public override void Update(float frameTime) // check if sensor is ready to update if (curTime < sensor.NextUpdate) continue; + sensor.NextUpdate += sensor.UpdateRate; if (!CheckSensorAssignedStation((uid, sensor))) continue; - sensor.NextUpdate += sensor.UpdateRate; - // get sensor status var status = GetSensorState((uid, sensor)); if (status == null) @@ -70,22 +59,4 @@ public override void Update(float frameTime) _deviceNetworkSystem.QueuePacket(uid, sensor.ConnectedServer, payload, device: device); } } - - private void OnEmpPulse(Entity ent, ref EmpPulseEvent args) - { - args.Affected = true; - args.Disabled = true; - - ent.Comp.PreviousMode = ent.Comp.Mode; - SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null); - - ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked; - ent.Comp.ControlsLocked = true; - } - - private void OnEmpFinished(Entity ent, ref EmpDisabledRemoved args) - { - SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null); - ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked; - } } diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs deleted file mode 100644 index 235cc173318..00000000000 --- a/Content.Server/Medical/VomitSystem.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Server.Fluids.EntitySystems; -using Content.Server.Forensics; -using Content.Server.Popups; -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.IdentityManagement; -using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Server.Audio; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Server.Medical -{ - public sealed class VomitSystem : EntitySystem - { - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly BloodstreamSystem _bloodstream = default!; - [Dependency] private readonly BodySystem _body = default!; - [Dependency] private readonly ForensicsSystem _forensics = default!; - [Dependency] private readonly HungerSystem _hunger = default!; - [Dependency] private readonly MobStateSystem _mobstate = default!; - [Dependency] private readonly MovementModStatusSystem _movementMod = default!; - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PuddleSystem _puddle = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly ThirstSystem _thirst = default!; - - private static readonly ProtoId VomitCollection = "Vomit"; - - private readonly SoundSpecifier _vomitSound = new SoundCollectionSpecifier(VomitCollection, - AudioParams.Default.WithVariation(0.2f).WithVolume(-4f)); - - /// - /// Make an entity vomit, if they have a stomach. - /// - public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = -40f, bool force = false) - { - // Main requirement: You have a stomach - var stomachList = _body.GetBodyOrganEntityComps(uid); - if (stomachList.Count == 0) - return; - - // Vomit only if entity is alive - // Ignore condition if force was set to true - if (!force && _mobstate.IsDead(uid)) - return; - - // Vomiting makes you hungrier and thirstier - if (TryComp(uid, out var hunger)) - _hunger.ModifyHunger(uid, hungerAdded, hunger); - - if (TryComp(uid, out var thirst)) - _thirst.ModifyThirst(uid, thirst, thirstAdded); - - // It fully empties the stomach, this amount from the chem stream is relatively small - var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6; - // Apply a bit of slowdown - _movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f); - - // TODO: Need decals - var solution = new Solution(); - - // Empty the stomach out into it - foreach (var stomach in stomachList) - { - if (_solutionContainer.ResolveSolution(stomach.Owner, StomachSystem.DefaultSolutionName, ref stomach.Comp1.Solution, out var sol)) - { - solution.AddSolution(sol, _proto); - sol.RemoveAllSolution(); - _solutionContainer.UpdateChemicals(stomach.Comp1.Solution.Value); - } - } - // Adds a tiny amount of the chem stream from earlier along with vomit - if (TryComp(uid, out var bloodStream)) - { - const float chemMultiplier = 0.1f; - - var vomitAmount = solutionSize; - - // Takes 10% of the chemicals removed from the chem stream - if (_solutionContainer.ResolveSolution(uid, bloodStream.ChemicalSolutionName, ref bloodStream.ChemicalSolution)) - { - var vomitChemstreamAmount = _solutionContainer.SplitSolution(bloodStream.ChemicalSolution.Value, vomitAmount); - vomitChemstreamAmount.ScaleSolution(chemMultiplier); - solution.AddSolution(vomitChemstreamAmount, _proto); - - vomitAmount -= (float)vomitChemstreamAmount.Volume; - } - - // Makes a vomit solution the size of 90% of the chemicals removed from the chemstream - solution.AddReagent(new ReagentId("Vomit", _bloodstream.GetEntityBloodData(uid)), vomitAmount); // TODO: Dehardcode vomit prototype - } - - if (_puddle.TrySpillAt(uid, solution, out var puddle, false)) - { - _forensics.TransferDna(puddle, uid, false); - } - - // Force sound to play as spill doesn't work if solution is empty. - _audio.PlayPvs(_vomitSound, uid); - _popup.PopupEntity(Loc.GetString("disease-vomit", ("person", Identity.Entity(uid, EntityManager))), uid); - } - } -} diff --git a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs index dae8079ac13..975f4c19bdf 100644 --- a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs +++ b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs @@ -20,7 +20,7 @@ public sealed partial class TargetObjectiveMindFilter : MindFilter [DataField] public EntityWhitelist? Blacklist; - protected override bool ShouldRemove(Entity mind, EntityUid? excluded, IEntityManager entMan, SharedMindSystem mindSys) + protected override bool ShouldRemove(Entity mind, EntityUid? excluded, IEntityManager entMan) { // ignore this filter if there is no user to check if (!entMan.TryGetComponent(excluded, out var excludedMind)) @@ -33,7 +33,7 @@ protected override bool ShouldRemove(Entity mind, EntityUid? excl if (entMan.TryGetComponent(objective, out var kill) && kill.Target == mind.Owner) { // remove the mind if this objective is blacklisted - if (whitelistSys.IsBlacklistPassOrNull(Blacklist, objective)) + if (whitelistSys.IsWhitelistPassOrNull(Blacklist, objective)) return true; } } diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index a7547e87e4c..36a3e3aa95e 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -184,8 +184,8 @@ public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostC { component = EnsureComp(entity.Value); - if (component.HasMind) - _ghosts.OnGhostAttempt(component.Mind.Value, false); + if (TryGetMind(entity.Value, out var entityMindId, out _)) + _ghosts.OnGhostAttempt(entityMindId, false); if (TryComp(entity.Value, out var actor)) { @@ -219,12 +219,18 @@ public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostC var oldEntity = mind.OwnedEntity; if (TryComp(oldEntity, out MindContainerComponent? oldContainer)) { - oldContainer.Mind = null; - mind.OwnedEntity = null; Entity mindEnt = (mindId, mind); Entity containerEnt = (oldEntity.Value, oldContainer); - RaiseLocalEvent(oldEntity.Value, new MindRemovedMessage(mindEnt, containerEnt)); - RaiseLocalEvent(mindId, new MindGotRemovedEvent(mindEnt, containerEnt)); + + RaiseLocalEvent(oldEntity.Value, new BeforeMindRemovedMessage(mindEnt, containerEnt, entity)); + RaiseLocalEvent(mindId, new BeforeMindGotRemovedEvent(mindEnt, containerEnt, entity)); + + oldContainer.Mind = null; + oldContainer.HasMind = false; + mind.OwnedEntity = null; + + RaiseLocalEvent(oldEntity.Value, new MindRemovedMessage(mindEnt, containerEnt, entity)); + RaiseLocalEvent(mindId, new MindGotRemovedEvent(mindEnt, containerEnt, entity)); Dirty(oldEntity.Value, oldContainer); } @@ -256,12 +262,13 @@ public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostC if (entity != null) { component!.Mind = mindId; + component.HasMind = true; mind.OwnedEntity = entity; mind.OriginalOwnedEntity ??= GetNetEntity(mind.OwnedEntity); Entity mindEnt = (mindId, mind); Entity containerEnt = (entity.Value, component); - RaiseLocalEvent(entity.Value, new MindAddedMessage(mindEnt, containerEnt)); - RaiseLocalEvent(mindId, new MindGotAddedEvent(mindEnt, containerEnt)); + RaiseLocalEvent(entity.Value, new MindAddedMessage(mindEnt, containerEnt, oldEntity)); + RaiseLocalEvent(mindId, new MindGotAddedEvent(mindEnt, containerEnt, oldEntity)); Dirty(entity.Value, component); } } diff --git a/Content.Server/Mind/TransferMindOnGibSystem.cs b/Content.Server/Mind/TransferMindOnGibSystem.cs index ff71fa05609..81ebf96a87e 100644 --- a/Content.Server/Mind/TransferMindOnGibSystem.cs +++ b/Content.Server/Mind/TransferMindOnGibSystem.cs @@ -1,5 +1,5 @@ using System.Linq; -using Content.Shared.Body.Events; +using Content.Shared.Gibbing; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Tag; @@ -21,19 +21,19 @@ public sealed class TransferMindOnGibSystem : EntitySystem /// public override void Initialize() { - SubscribeLocalEvent(OnGib); + SubscribeLocalEvent(OnGib); } - private void OnGib(EntityUid uid, TransferMindOnGibComponent component, BeingGibbedEvent args) + private void OnGib(Entity ent, ref GibbedBeforeDeletionEvent args) { - if (!_mindSystem.TryGetMind(uid, out var mindId, out var mind)) + if (!_mindSystem.TryGetMind(ent, out var mindId, out var mind)) return; - var validParts = args.GibbedParts.Where(p => _tag.HasTag(p, component.TargetTag)).ToHashSet(); + var validParts = args.Giblets.Where(p => _tag.HasTag(p, ent.Comp.TargetTag)).ToHashSet(); if (!validParts.Any()) return; - var ent = _random.Pick(validParts); - _mindSystem.TransferTo(mindId, ent, mind: mind); + var transfer = _random.Pick(validParts); + _mindSystem.TransferTo(mindId, transfer, mind: mind); } } diff --git a/Content.Server/Mindshield/MindShieldSystem.cs b/Content.Server/Mindshield/MindShieldSystem.cs index bc5b65159b8..f69e7b45fba 100644 --- a/Content.Server/Mindshield/MindShieldSystem.cs +++ b/Content.Server/Mindshield/MindShieldSystem.cs @@ -32,9 +32,6 @@ public override void Initialize() private void OnImplantImplanted(Entity ent, ref ImplantImplantedEvent ev) { - if (ev.Implanted == null) - return; - EnsureComp(ev.Implanted); MindShieldRemovalCheck(ev.Implanted, ev.Implant); } diff --git a/Content.Server/Mining/MeteorSystem.cs b/Content.Server/Mining/MeteorSystem.cs index 3b0c6920b3b..e5f190167d9 100644 --- a/Content.Server/Mining/MeteorSystem.cs +++ b/Content.Server/Mining/MeteorSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.Destructible; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Systems; @@ -45,12 +46,13 @@ private void OnCollide(EntityUid uid, MeteorComponent component, ref StartCollid { threshold = FixedPoint2.MaxValue; } - var otherEntDamage = CompOrNull(args.OtherEntity)?.TotalDamage ?? FixedPoint2.Zero; + + var otherEntDamage = _damageable.GetTotalDamage(args.OtherEntity); // account for the damage that the other entity has already taken: don't overkill threshold -= otherEntDamage; // The max amount of damage our meteor can take before breaking. - var maxMeteorDamage = _destructible.DestroyedAt(uid) - CompOrNull(uid)?.TotalDamage ?? FixedPoint2.Zero; + var maxMeteorDamage = _destructible.DestroyedAt(uid) - _damageable.GetTotalDamage(uid); // Cap damage so we don't overkill the meteor var trueDamage = FixedPoint2.Min(maxMeteorDamage, threshold); diff --git a/Content.Server/Mobs/CritMobActionsSystem.cs b/Content.Server/Mobs/CritMobActionsSystem.cs index bcc4aced1b8..a626490934f 100644 --- a/Content.Server/Mobs/CritMobActionsSystem.cs +++ b/Content.Server/Mobs/CritMobActionsSystem.cs @@ -1,15 +1,15 @@ -using Content.Server.Administration; +using Content.Server.Administration; using Content.Server.Chat.Systems; using Content.Server.Popups; -using Content.Server.Speech.Muting; +using Content.Shared.Chat; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Speech.Muting; using Robust.Server.Console; using Robust.Shared.Player; -using Content.Shared.Speech.Muting; -using Content.Shared.Chat; -using Content.Shared.Damage; namespace Content.Server.Mobs; diff --git a/Content.Server/Movement/Components/StressTestMovementComponent.cs b/Content.Server/Movement/Components/StressTestMovementComponent.cs deleted file mode 100644 index 051747a8490..00000000000 --- a/Content.Server/Movement/Components/StressTestMovementComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Numerics; - -namespace Content.Server.Movement.Components; - -[RegisterComponent] -public sealed partial class StressTestMovementComponent : Component -{ - public float Progress { get; set; } - public Vector2 Origin { get; set; } -} diff --git a/Content.Server/Movement/StressTestMovementSystem.cs b/Content.Server/Movement/StressTestMovementSystem.cs deleted file mode 100644 index 51916204da4..00000000000 --- a/Content.Server/Movement/StressTestMovementSystem.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Numerics; -using Content.Server.Movement.Components; - -namespace Content.Server.Movement; - -public sealed class StressTestMovementSystem : EntitySystem -{ - [Dependency] private readonly SharedTransformSystem _transform = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStressStartup); - } - - private void OnStressStartup(EntityUid uid, StressTestMovementComponent component, ComponentStartup args) - { - component.Origin = _transform.GetWorldPosition(uid); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var stressTest, out var transform)) - { - if (!transform.ParentUid.IsValid()) - continue; - - stressTest.Progress += frameTime; - - if (stressTest.Progress > 1) - { - stressTest.Progress -= 1; - } - - var x = MathF.Sin(stressTest.Progress * MathHelper.TwoPi); - var y = MathF.Cos(stressTest.Progress * MathHelper.TwoPi); - - _transform.SetWorldPosition((uid, transform), stressTest.Origin + new Vector2(x, y) * 5); - } - } -} diff --git a/Content.Server/NPC/Components/NPCComponent.cs b/Content.Server/NPC/Components/NPCComponent.cs index b1d5bfcf5f2..3b396f034e3 100644 --- a/Content.Server/NPC/Components/NPCComponent.cs +++ b/Content.Server/NPC/Components/NPCComponent.cs @@ -9,4 +9,5 @@ public abstract partial class NPCComponent : SharedNPCComponent /// [DataField("blackboard", customTypeSerializer: typeof(NPCBlackboardSerializer))] public NPCBlackboard Blackboard = new(); + // TODO FULL GAME SAVE Serialize this } diff --git a/Content.Server/NPC/HTN/HTNComponent.cs b/Content.Server/NPC/HTN/HTNComponent.cs index d739d6dcd04..f710f3eefbb 100644 --- a/Content.Server/NPC/HTN/HTNComponent.cs +++ b/Content.Server/NPC/HTN/HTNComponent.cs @@ -24,6 +24,7 @@ public sealed partial class HTNComponent : NPCComponent /// [ViewVariables] public HTNPlan? Plan; + // TODO FULL GAME SAVE serialize this? /// /// How long to wait after having planned to try planning again. diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs index 4d9e321dd91..7bfe4329986 100644 --- a/Content.Server/NPC/HTN/HTNSystem.cs +++ b/Content.Server/NPC/HTN/HTNSystem.cs @@ -33,6 +33,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(_npc.OnMobStateChange); SubscribeLocalEvent(_npc.OnNPCMapInit); + SubscribeLocalEvent(_npc.OnNPCStartup); SubscribeLocalEvent(_npc.OnPlayerNPCAttach); SubscribeLocalEvent(_npc.OnPlayerNPCDetach); SubscribeLocalEvent(OnHTNShutdown); diff --git a/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs index 58647d88749..961d3c39ab7 100644 --- a/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs +++ b/Content.Server/NPC/HTN/Preconditions/GunAmmoPrecondition.cs @@ -10,10 +10,10 @@ public sealed partial class GunAmmoPrecondition : HTNPrecondition { [Dependency] private readonly IEntityManager _entManager = default!; - [DataField("minPercent")] + [DataField] public float MinPercent = 0f; - [DataField("maxPercent")] + [DataField] public float MaxPercent = 1f; public override bool IsMet(NPCBlackboard blackboard) @@ -21,19 +21,19 @@ public override bool IsMet(NPCBlackboard blackboard) var owner = blackboard.GetValue(NPCBlackboard.Owner); var gunSystem = _entManager.System(); - if (!gunSystem.TryGetGun(owner, out var gunUid, out _)) + if (!gunSystem.TryGetGun(owner, out var gun)) { return false; } var ammoEv = new GetAmmoCountEvent(); - _entManager.EventBus.RaiseLocalEvent(gunUid, ref ammoEv); + _entManager.EventBus.RaiseLocalEvent(gun, ref ammoEv); float percent; if (ammoEv.Capacity == 0) percent = 0f; else - percent = ammoEv.Count / (float) ammoEv.Capacity; + percent = ammoEv.Count / (float)ammoEv.Capacity; percent = System.Math.Clamp(percent, 0f, 1f); diff --git a/Content.Server/NPC/HTN/Preconditions/HasStatusEffectPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/HasStatusEffectPrecondition.cs new file mode 100644 index 00000000000..d11a99e2b5a --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/HasStatusEffectPrecondition.cs @@ -0,0 +1,28 @@ +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Returns true if entity have specified status effect +/// +public sealed partial class HasStatusEffectPrecondition : HTNPrecondition +{ + private StatusEffectsSystem _statusEffects = default!; + + [DataField(required: true)] + public EntProtoId StatusEffect; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _statusEffects = sysManager.GetEntitySystem(); + } + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + return _statusEffects.HasStatusEffect(owner, StatusEffect); + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs new file mode 100644 index 00000000000..ffee9adaf32 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/InFriendlyContainerPrecondition.cs @@ -0,0 +1,50 @@ +using Content.Shared.NPC.Systems; +using Robust.Shared.Containers; +using Robust.Server.Containers; +using Robust.Shared.GameObjects; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Checks if the owner is in a friendly container. +/// Recursively checks if container's container is friendly. +/// +public sealed partial class InFriendlyContainerPrecondition : HTNPrecondition +{ + private ContainerSystem _container = default!; + private NpcFactionSystem _npcFaction = default!; + + [DataField] public bool IsInFriendlyContainer = true; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _container = sysManager.GetEntitySystem(); + _npcFaction = sysManager.GetEntitySystem(); + } + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!_container.TryGetContainingContainer(owner, out var container)) + return !IsInFriendlyContainer; + + return IsInFriendlyContainer == IsContainerOrParentFriendly(owner, container.Owner); + } + + /// + /// Recursively check if a container or any parent container is friendly. + /// + /// True if any container is friendly. + private bool IsContainerOrParentFriendly(EntityUid owner, EntityUid containerOwner) + { + if (_npcFaction.IsEntityFriendly(owner, containerOwner)) + return true; + + if (!_container.TryGetContainingContainer(containerOwner, out var nextContainer)) + return false; + + return IsContainerOrParentFriendly(owner, nextContainer.Owner); + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs new file mode 100644 index 00000000000..fe76968f34b --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs @@ -0,0 +1,31 @@ +using Content.Shared.Emag.Systems; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// A precondition which is met if the NPC is emagged with , as computed by +/// . This is useful for changing NPC behavior in the case that the NPC is emagged, +/// eg. like a helper NPC bot turning evil. +/// +public sealed partial class IsEmaggedPrecondition : HTNPrecondition +{ + private EmagSystem _emag; + + /// + /// The type of emagging to check for. + /// + [DataField] + public EmagType EmagType = EmagType.Interaction; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _emag = sysManager.GetEntitySystem(); + } + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + return _emag.CheckFlag(owner, EmagType); + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/ItemTogglePrecondition.cs b/Content.Server/NPC/HTN/Preconditions/ItemTogglePrecondition.cs new file mode 100644 index 00000000000..431e68a55ba --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/ItemTogglePrecondition.cs @@ -0,0 +1,39 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Item.ItemToggle.Components; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Checks if the item in the active hand has and whether its +/// state matches the expected value. +/// +public sealed partial class ItemTogglePrecondition : HTNPrecondition +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + /// + /// The activation state to check for. + /// + [DataField] + public bool Activated = true; + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return false; + + var handsSystem = _entManager.System(); + if (!handsSystem.TryGetHeldItem(owner, activeHand, out var heldEntity)) + return false; + + if (!_entManager.TryGetComponent(heldEntity, out var itemToggle)) + return false; + + if (!itemToggle.OnUse) + return false; + + return itemToggle.Activated == Activated; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/NeedToRackBoltPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/NeedToRackBoltPrecondition.cs new file mode 100644 index 00000000000..7872a66f021 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/NeedToRackBoltPrecondition.cs @@ -0,0 +1,38 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Checks if the NPC's gun needs bolt racking - either bolt is open OR bolt is closed but chamber is empty. +/// Returns true if gun needs racking to prepare for firing. +/// +public sealed partial class NeedToRackBoltPrecondition : HTNPrecondition +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return false; + + var handsSystem = _entManager.System(); + if (!handsSystem.TryGetHeldItem(owner, activeHand, out var heldEntity)) + return false; + + if (!_entManager.TryGetComponent(heldEntity, out var chamberMagazine)) + return false; + + if (!chamberMagazine.CanRack) + return false; + + var gunSystem = _entManager.System(); + var chamberEntity = gunSystem.GetChamberEntity(heldEntity.Value); + bool hasRoundInChamber = chamberEntity is not null; + + return chamberMagazine.BoltClosed == false || !hasRoundInChamber; + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs b/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs index 921b5ffa226..b88d17b13d9 100644 --- a/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs +++ b/Content.Server/NPC/HTN/Preconditions/TargetInRangePrecondition.cs @@ -20,6 +20,9 @@ public override void Initialize(IEntitySystemManager sysManager) _transformSystem = sysManager.GetEntitySystem(); } + [DataField] + public bool Invert; + public override bool IsMet(NPCBlackboard blackboard) { if (!blackboard.TryGetValue(NPCBlackboard.OwnerCoordinates, out var coordinates, _entManager)) @@ -29,7 +32,6 @@ public override bool IsMet(NPCBlackboard blackboard) !_entManager.TryGetComponent(target, out var targetXform)) return false; - var transformSystem = _entManager.System; - return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault(RangeKey, _entManager)); + return _transformSystem.InRange(coordinates, targetXform.Coordinates, blackboard.GetValueOrDefault(RangeKey, _entManager)) ^ Invert; } } diff --git a/Content.Server/NPC/HTN/Preconditions/WieldedPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/WieldedPrecondition.cs new file mode 100644 index 00000000000..dc9a9cbb7dd --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/WieldedPrecondition.cs @@ -0,0 +1,45 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Wieldable; +using Content.Shared.Wieldable.Components; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// Checks if the item in the active hand has and whether its +/// state matches the expected value. +/// When is false, also checks that the item can actually be wielded. +/// +public sealed partial class WieldedPrecondition : HTNPrecondition +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + /// + /// The wield state to check for. + /// + [DataField] + public bool Wielded = true; + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return false; + + var handsSystem = _entManager.System(); + if (!handsSystem.TryGetHeldItem(owner, activeHand, out var heldEntity)) + return false; + + if (!_entManager.TryGetComponent(heldEntity, out var wieldable)) + return false; + + if (Wielded) + return wieldable.Wielded; + + if (wieldable.Wielded) + return false; + + var wieldableSystem = _entManager.System(); + return wieldableSystem.CanWield(heldEntity.Value, wieldable, owner, quiet: true); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs new file mode 100644 index 00000000000..a1440acb2e1 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs @@ -0,0 +1,70 @@ +using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions; +using Content.Shared.CombatMode; +using Content.Shared.Weapons.Melee; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee; + +/// +/// Something between and , this operator causes the NPC +/// to attempt a SINGLE melee attack on the specified +/// target. +/// +public sealed partial class MeleeAttackOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + private SharedMeleeWeaponSystem _melee; + + /// + /// Key that contains the target entity. + /// + [DataField(required: true)] + public string TargetKey = default!; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _melee = sysManager.GetEntitySystem(); + } + + public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status) + { + base.TaskShutdown(blackboard, status); + + ExitCombatMode(blackboard); + } + + public override void PlanShutdown(NPCBlackboard blackboard) + { + base.PlanShutdown(blackboard); + + ExitCombatMode(blackboard); + } + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!_entManager.TryGetComponent(owner, out var combatMode) || + !_melee.TryGetWeapon(owner, out var weaponUid, out var weapon)) + { + return HTNOperatorStatus.Failed; + } + + _entManager.System().SetInCombatMode(owner, true, combatMode); + + + if (!blackboard.TryGetValue(TargetKey, out var target, _entManager) || + !_melee.AttemptLightAttack(owner, weaponUid, weapon, target)) + { + return HTNOperatorStatus.Continuing; + } + + return HTNOperatorStatus.Finished; + } + + private void ExitCombatMode(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + _entManager.System().SetInCombatMode(owner, false); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/RackBoltOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/RackBoltOperator.cs new file mode 100644 index 00000000000..6dc8e494011 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/RackBoltOperator.cs @@ -0,0 +1,33 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; + +/// +/// Operator that racks the bolt of a gun with . +/// +public sealed partial class RackBoltOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return HTNOperatorStatus.Failed; + + var handsSystem = _entManager.System(); + if (!handsSystem.TryGetHeldItem(owner, activeHand, out var gunUid)) + return HTNOperatorStatus.Failed; + + if (!_entManager.TryGetComponent(gunUid, out var chamberMagazine)) + return HTNOperatorStatus.Failed; + + var gunSystem = _entManager.System(); + gunSystem.UseChambered(gunUid.Value, chamberMagazine, owner); + + return HTNOperatorStatus.Finished; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/WieldOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/WieldOperator.cs new file mode 100644 index 00000000000..98cd2afd4de --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/WieldOperator.cs @@ -0,0 +1,53 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Wieldable; +using Content.Shared.Wieldable.Components; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; + +/// +/// Operator that wields or unwields a weapon. +/// +public sealed partial class WieldOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + /// + /// If true, tries to wield the item. If false, tries to unwield it. + /// + [DataField] + public bool Wield = true; + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return HTNOperatorStatus.Failed; + + var handsSystem = _entManager.System(); + if (!handsSystem.TryGetHeldItem(owner, activeHand, out var weaponUid)) + return HTNOperatorStatus.Failed; + + if (!_entManager.TryGetComponent(weaponUid, out var wieldable)) + return HTNOperatorStatus.Failed; + + var wieldableSystem = _entManager.System(); + + if (Wield) + { + if (wieldable.Wielded) + return HTNOperatorStatus.Failed; + + return wieldableSystem.TryWield(weaponUid.Value, wieldable, owner) + ? HTNOperatorStatus.Finished + : HTNOperatorStatus.Failed; + } + + if (!wieldable.Wielded) + return HTNOperatorStatus.Failed; + + return wieldableSystem.TryUnwield(weaponUid.Value, wieldable, owner) + ? HTNOperatorStatus.Finished + : HTNOperatorStatus.Failed; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/UseItemInHandOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/UseItemInHandOperator.cs new file mode 100644 index 00000000000..3048e043b55 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/UseItemInHandOperator.cs @@ -0,0 +1,24 @@ +using Content.Shared.Hands.EntitySystems; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions; + +/// +/// Uses the item in the NPC's active hand. +/// +public sealed partial class UseItemInHandOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out var activeHand, _entManager)) + return HTNOperatorStatus.Failed; + + var handsSystem = _entManager.System(); + var success = handsSystem.TryUseItemInHand(owner, handName: activeHand); + + return success ? HTNOperatorStatus.Finished : HTNOperatorStatus.Failed; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs index 558b1fc04dc..99227492af4 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SayKeyOperator.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs index 8a4c655a39b..98c63dd912c 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs @@ -1,13 +1,25 @@ using Content.Server.Chat.Systems; +using Robust.Shared.Timing; +using Content.Shared.Chat; +using Content.Shared.Dataset; +using Content.Shared.Random.Helpers; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using static Content.Server.NPC.HTN.PrimitiveTasks.Operators.SpeakOperator.SpeakOperatorSpeech; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators; public sealed partial class SpeakOperator : HTNOperator { + [Dependency] private readonly IEntityManager _entMan = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; private ChatSystem _chat = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; [DataField(required: true)] - public string Speech = string.Empty; + public SpeakOperatorSpeech Speech; /// /// Whether to hide message from chat window and logs. @@ -15,18 +27,74 @@ public sealed partial class SpeakOperator : HTNOperator [DataField] public bool Hidden; + /// + /// Skip speaking for `cooldown` seconds, intended to stop spam + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.Zero; + + /// + /// Define what key is used for storing the cooldown + /// + [DataField] + public string CooldownID = string.Empty; + public override void Initialize(IEntitySystemManager sysManager) { base.Initialize(sysManager); - _chat = sysManager.GetEntitySystem(); } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) { + if (Cooldown != TimeSpan.Zero && CooldownID != string.Empty) + { + if (blackboard.TryGetValue(CooldownID, out var nextSpeechTime, _entMan) && _gameTiming.CurTime < nextSpeechTime) + return base.Update(blackboard, frameTime); + + blackboard.SetValue(CooldownID, _gameTiming.CurTime + Cooldown); + } + + LocId speechLocId; + switch (Speech) + { + case LocalizedSetSpeakOperatorSpeech localizedDataSet: + if (!_proto.TryIndex(localizedDataSet.LineSet, out var speechSet)) + return HTNOperatorStatus.Failed; + speechLocId = _random.Pick(speechSet); + break; + case SingleSpeakOperatorSpeech single: + speechLocId = single.Line; + break; + default: + throw new ArgumentOutOfRangeException(nameof(Speech)); + } + var speaker = blackboard.GetValue(NPCBlackboard.Owner); - _chat.TrySendInGameICMessage(speaker, Loc.GetString(Speech), InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden); + _chat.TrySendInGameICMessage( + speaker, + Loc.GetString(speechLocId), + InGameICChatType.Speak, + hideChat: Hidden, + hideLog: Hidden + ); return base.Update(blackboard, frameTime); } + + [ImplicitDataDefinitionForInheritors, MeansImplicitUse] + public abstract partial class SpeakOperatorSpeech + { + public sealed partial class SingleSpeakOperatorSpeech : SpeakOperatorSpeech + { + [DataField(required: true)] + public string Line; + } + + public sealed partial class LocalizedSetSpeakOperatorSpeech : SpeakOperatorSpeech + { + [DataField(required: true)] + public ProtoId LineSet; + } + } } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/DisposalInsertOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/DisposalInsertOperator.cs new file mode 100644 index 00000000000..6693c3c63c2 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/DisposalInsertOperator.cs @@ -0,0 +1,55 @@ +using Content.Server.Disposal.Unit; +using Content.Shared.Disposal.Components; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; + +public sealed partial class DisposalInsertOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + private DisposalUnitSystem _disposalSystem = default!; + + /// + /// Target entity to flush + /// + [DataField(required: true)] + public string TargetKey = string.Empty; + + /// + /// Target disposal bin entity + /// + [DataField(required: true)] + public string DisposalTargetKey = string.Empty; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _disposalSystem = sysManager.GetEntitySystem(); + } + + public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status) + { + base.TaskShutdown(blackboard, status); + blackboard.Remove(TargetKey); + blackboard.Remove(DisposalTargetKey); + } + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(TargetKey, out var target, _entManager) || _entManager.Deleted(target)) + return HTNOperatorStatus.Failed; + + if (!blackboard.TryGetValue(DisposalTargetKey, out var disposalUnitTarget, _entManager) || _entManager.Deleted(target)) + return HTNOperatorStatus.Failed; + + if (!_entManager.TryGetComponent(disposalUnitTarget, out var disposalComp)) + return HTNOperatorStatus.Failed; + + if (!_disposalSystem.TryInsert(disposalUnitTarget, target, owner, disposalComp)) + return HTNOperatorStatus.Failed; + + return HTNOperatorStatus.Finished; + + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs new file mode 100644 index 00000000000..317b7aacf2e --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/EnsureComponentOperator.cs @@ -0,0 +1,29 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; + +public sealed partial class EnsureComponentOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + /// + /// Target entity to inject. + /// + [DataField(required: true)] + public string TargetKey = string.Empty; + + /// + /// Components to be added + /// + [DataField] + public ComponentRegistry Components = new(); + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + if (!blackboard.TryGetValue(TargetKey, out var target, _entMan)) + return HTNOperatorStatus.Failed; + + _entMan.AddComponents(target, Components); + return HTNOperatorStatus.Finished; + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs index bac9cfcf40b..20e3daec538 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs @@ -1,10 +1,7 @@ using Content.Server.Chat.Systems; -using Content.Shared.NPC.Components; +using Content.Shared.Chat; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Damage; -using Content.Shared.Emag.Components; using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; using Content.Shared.Popups; using Content.Shared.Silicons.Bots; using Robust.Shared.Audio.Systems; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickEntityNearMobOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickEntityNearMobOperator.cs new file mode 100644 index 00000000000..d07a9c29170 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickEntityNearMobOperator.cs @@ -0,0 +1,131 @@ +using System.Threading; +using System.Threading.Tasks; +using Content.Server.NPC.Pathfinding; +using Content.Shared.Interaction; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Whitelist; +using Robust.Server.Containers; + +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; + +/// +/// Queries for nearby entities matching a whitelist/blacklist, and then searches for a mob near to that entity. +/// +public sealed partial class PickEntityNearMobOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + private EntityLookupSystem _lookup = default!; + private PathfindingSystem _pathfinding = default!; + private ContainerSystem _container = default!; + private EntityWhitelistSystem _entityWhitelist = default!; + + /// + /// Range to search for entities + /// + [DataField(required: true)] + public string RangeKey = default!; + + /// + /// Target mob that was found near NearbyEntityTargetKey + /// + [DataField(required: true)] + public string TargetKey = string.Empty; + + /// + /// Target entity that was found + /// + [DataField(required: true)] + public string NearbyEntityTargetKey = string.Empty; + + /// + /// Target entitycoordinates to move to. + /// + [DataField(required: true)] + public string TargetMoveKey = string.Empty; + + /// + /// Whitelist for what entities will get picked + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist for what entities will NOT get picked + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// Range to search for mobs near the target entity + /// + [DataField(required: true)] + public string MobRangeKey = default!; + + /// + /// MobState to check for + /// + [DataField] + public MobState? MobState; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _lookup = sysManager.GetEntitySystem(); + _pathfinding = sysManager.GetEntitySystem(); + _container = sysManager.GetEntitySystem(); + _entityWhitelist = sysManager.GetEntitySystem(); + } + + public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, + CancellationToken cancelToken) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(RangeKey, out var range, _entManager)) + return (false, null); + + if (!blackboard.TryGetValue(RangeKey, out var mobRange, _entManager)) + return (false, null); + + var mobState = _entManager.GetEntityQuery(); + + foreach (var entity in _lookup.GetEntitiesInRange(owner, range)) + { + if (!_entityWhitelist.CheckBoth(entity, Blacklist, Whitelist)) + continue; + + //checking if there is anyone NEAR the entity we found + foreach (var mob in _lookup.GetEntitiesInRange(entity, mobRange)) + { + if (mob == owner) + continue; + + if (_container.IsEntityInContainer(mob)) + continue; + + if (mobState.TryGetComponent(mob, out var state)) + { + if (MobState != null && state.CurrentState != MobState) + continue; + + var pathRange = SharedInteractionSystem.InteractionRange; + var path = await _pathfinding.GetPath(owner, mob, pathRange, cancelToken); + + if (path.Result == PathResult.NoPath) + return (false, null); + + return (true, new Dictionary() + { + {TargetKey, mob}, + {NearbyEntityTargetKey, entity}, + {TargetMoveKey, _entManager.GetComponent(mob).Coordinates}, + {NPCBlackboard.PathfindKey, path}, + }); + } + } + } + + return (false, null); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs index f351d582c6e..53164ad7ca4 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs @@ -3,20 +3,28 @@ using Content.Shared.NPC.Components; using Content.Server.NPC.Pathfinding; using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Interaction; using Content.Shared.Mobs.Components; using Content.Shared.Silicons.Bots; using Content.Shared.Emag.Components; +using Content.Shared.FixedPoint; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; public sealed partial class PickNearbyInjectableOperator : HTNOperator { [Dependency] private readonly IEntityManager _entManager = default!; - private EntityLookupSystem _lookup = default!; private MedibotSystem _medibot = default!; private PathfindingSystem _pathfinding = default!; + private DamageableSystem _damageable = default!; + + private EntityQuery _damageQuery = default!; + private EntityQuery _injectQuery = default!; + private EntityQuery _recentlyInjected = default!; + private EntityQuery _mobState = default!; + private EntityQuery _emaggedQuery = default!; [DataField("rangeKey")] public string RangeKey = NPCBlackboard.MedibotInjectRange; @@ -35,9 +43,15 @@ public sealed partial class PickNearbyInjectableOperator : HTNOperator public override void Initialize(IEntitySystemManager sysManager) { base.Initialize(sysManager); - _lookup = sysManager.GetEntitySystem(); _medibot = sysManager.GetEntitySystem(); _pathfinding = sysManager.GetEntitySystem(); + _damageable = sysManager.GetEntitySystem(); + + _damageQuery = _entManager.GetEntityQuery(); + _injectQuery = _entManager.GetEntityQuery(); + _recentlyInjected = _entManager.GetEntityQuery(); + _mobState = _entManager.GetEntityQuery(); + _emaggedQuery = _entManager.GetEntityQuery(); } public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, @@ -51,18 +65,16 @@ public override void Initialize(IEntitySystemManager sysManager) if (!_entManager.TryGetComponent(owner, out var medibot)) return (false, null); - var damageQuery = _entManager.GetEntityQuery(); - var injectQuery = _entManager.GetEntityQuery(); - var recentlyInjected = _entManager.GetEntityQuery(); - var mobState = _entManager.GetEntityQuery(); - var emaggedQuery = _entManager.GetEntityQuery(); - foreach (var entity in _lookup.GetEntitiesInRange(owner, range)) + if (!blackboard.TryGetValue>>("TargetList", out var patients, _entManager)) + return (false, null); + + foreach (var (entity, _) in patients) { - if (mobState.TryGetComponent(entity, out var state) && - injectQuery.HasComponent(entity) && - damageQuery.TryGetComponent(entity, out var damage) && - !recentlyInjected.HasComponent(entity)) + if (_mobState.TryGetComponent(entity, out var state) && + _injectQuery.HasComponent(entity) && + _damageQuery.TryGetComponent(entity, out var damage) && + !_recentlyInjected.HasComponent(entity)) { // no treating dead bodies if (!_medibot.TryGetTreatment(medibot, state.CurrentState, out var treatment)) @@ -71,7 +83,7 @@ public override void Initialize(IEntitySystemManager sysManager) // Only go towards a target if the bot can actually help them or if the medibot is emagged // note: this and the actual injecting don't check for specific damage types so for example, // radiation damage will trigger injection but the tricordrazine won't heal it. - if (!emaggedQuery.HasComponent(entity) && !treatment.IsValid(damage.TotalDamage)) + if (!_emaggedQuery.HasComponent(entity) && _damageable.GetTotalDamage((entity, damage)) == FixedPoint2.Zero) continue; //Needed to make sure it doesn't sometimes stop right outside it's interaction range diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs new file mode 100644 index 00000000000..4bbdbb34731 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs @@ -0,0 +1,47 @@ +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; + +/// +/// Raises an on the owner. The event will contain +/// the specified , and if not null, the value of . +/// +public sealed partial class RaiseEventForOwnerOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + /// + /// The conceptual "target" of this event. Note that this is NOT the entity for which the event is raised. If null, + /// will be null. + /// + [DataField] + public string? TargetKey; + + /// + /// The data contained in the raised event. Since is itself pretty meaningless, this is + /// included to give some context of what the event is actually supposed to mean. + /// + [DataField(required: true)] + public EntityEventArgs Args; + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + _entMan.EventBus.RaiseLocalEvent( + blackboard.GetValue(NPCBlackboard.Owner), + new HTNRaisedEvent( + blackboard.GetValue(NPCBlackboard.Owner), + TargetKey is { } targetKey ? blackboard.GetValue(targetKey) : null, + Args + ) + ); + + return HTNOperatorStatus.Finished; + } +} + +public sealed partial class HTNRaisedEvent(EntityUid owner, EntityUid? target, EntityEventArgs args) : EntityEventArgs +{ + // Owner and target are both included here in case we want to add a "RaiseEventForTargetOperator" in the future + // while reusing this event. + public EntityUid Owner = owner; + public EntityUid? Target = target; + public EntityEventArgs Args = args; +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs index 2bf9b09b106..16f18ae59a7 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/UtilityOperator.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; @@ -15,7 +16,9 @@ public sealed partial class UtilityOperator : HTNOperator { [Dependency] private readonly IEntityManager _entManager = default!; - [DataField("key")] public string Key = "Target"; + [DataField] public string Key = "Target"; + + [DataField] public ReturnTypeResult ReturnType = ReturnTypeResult.Highest; /// /// The EntityCoordinates of the specified target. @@ -30,19 +33,44 @@ public sealed partial class UtilityOperator : HTNOperator CancellationToken cancelToken) { var result = _entManager.System().GetEntities(blackboard, Prototype); - var target = result.GetHighest(); + Dictionary effects; - if (!target.IsValid()) + switch (ReturnType) { - return (false, new Dictionary()); - } + case ReturnTypeResult.Highest: + var target = result.GetHighest(); - var effects = new Dictionary() - { - {Key, target}, - {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)} - }; + if (!target.IsValid()) + { + return (false, new Dictionary()); + } + + effects = new Dictionary() + { + {Key, target}, + {KeyCoordinates, new EntityCoordinates(target, Vector2.Zero)}, + }; + + return (true, effects); + + case ReturnTypeResult.EnumerableDescending: + var targetList = result.GetEnumerable(); - return (true, effects); + effects = new Dictionary() + { + {"TargetList", targetList}, + }; + + return (true, effects); + + default: + throw new NotImplementedException(); + } + } + + public enum ReturnTypeResult + { + Highest, + EnumerableDescending } } diff --git a/Content.Server/NPC/Queries/Considerations/TargetIsVisibleCon.cs b/Content.Server/NPC/Queries/Considerations/TargetIsVisibleCon.cs new file mode 100644 index 00000000000..387c9e9809f --- /dev/null +++ b/Content.Server/NPC/Queries/Considerations/TargetIsVisibleCon.cs @@ -0,0 +1,3 @@ +namespace Content.Server.NPC.Queries.Considerations; + +public sealed partial class TargetIsVisibleCon : UtilityConsideration; diff --git a/Content.Server/NPC/Queries/Queries/ComponentFilter.cs b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs index 0df4bd902f1..5ba63bf6f76 100644 --- a/Content.Server/NPC/Queries/Queries/ComponentFilter.cs +++ b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs @@ -9,4 +9,11 @@ public sealed partial class ComponentFilter : UtilityQueryFilter /// [DataField("components", required: true)] public ComponentRegistry Components = new(); + + /// + /// If true, this filter retains entities with ALL of the specified components. If false, this filter removes + /// entities with ANY of the specified components. + /// + [DataField] + public bool RetainWithComp = true; } diff --git a/Content.Server/NPC/Queries/Queries/PuddlesQuery.cs b/Content.Server/NPC/Queries/Queries/PuddlesQuery.cs deleted file mode 100644 index 0c88cc2de6b..00000000000 --- a/Content.Server/NPC/Queries/Queries/PuddlesQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Server.NPC.Queries.Queries; - -public sealed partial class PuddlesQuery : UtilityQuery -{ - -} diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs index d4f97282935..34a1fe533d7 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs @@ -96,7 +96,7 @@ private void UpdateRanged(float frameTime) _combat.SetInCombatMode(uid, true, combatMode); } - if (!_gun.TryGetGun(uid, out var gunUid, out var gun)) + if (!_gun.TryGetGun(uid, out var gun)) { comp.Status = CombatStatus.NoWeapon; comp.ShootAccumulator = 0f; @@ -104,12 +104,12 @@ private void UpdateRanged(float frameTime) } var ammoEv = new GetAmmoCountEvent(); - RaiseLocalEvent(gunUid, ref ammoEv); + RaiseLocalEvent(gun, ref ammoEv); if (ammoEv.Count == 0) { // Recharging then? - if (_rechargeQuery.HasComponent(gunUid)) + if (_rechargeQuery.HasComponent(gun)) { continue; } @@ -200,12 +200,12 @@ private void UpdateRanged(float frameTime) comp.Status = CombatStatus.Normal; - if (gun.NextFire > _timing.CurTime) + if (gun.Comp.NextFire > _timing.CurTime) { return; } - _gun.AttemptShoot(uid, gunUid, gun, targetCordinates, comp.Target); + _gun.AttemptShoot(uid, gun, targetCordinates, comp.Target); } } } diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs index d6b2000f329..a970d34bda6 100644 --- a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs +++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs @@ -1,6 +1,7 @@ using Content.Server.NPC.Components; using Content.Shared.CombatMode; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; using Content.Shared.NPC.Systems; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index c083be6134f..40a3b43ea67 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -12,6 +12,7 @@ using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent; +using Robust.Shared.Random; namespace Content.Server.NPC.Systems; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs index a31c7850f38..e0886f141a0 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs @@ -12,6 +12,7 @@ using Robust.Shared.Utility; using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent; using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent; +using Robust.Shared.Random; namespace Content.Server.NPC.Systems; diff --git a/Content.Server/NPC/Systems/NPCSystem.cs b/Content.Server/NPC/Systems/NPCSystem.cs index 27b2a1691d6..5788d20ad8c 100644 --- a/Content.Server/NPC/Systems/NPCSystem.cs +++ b/Content.Server/NPC/Systems/NPCSystem.cs @@ -18,7 +18,7 @@ namespace Content.Server.NPC.Systems /// /// Handles NPCs running every tick. /// - public sealed partial class NPCSystem : EntitySystem + public sealed partial class NPCSystem : SharedNPCSystem { private static readonly Gauge ActiveGauge = Metrics.CreateGauge( "npc_active_count", @@ -63,9 +63,13 @@ public void OnPlayerNPCDetach(EntityUid uid, HTNComponent component, PlayerDetac WakeNPC(uid, component); } - public void OnNPCMapInit(EntityUid uid, HTNComponent component, MapInitEvent args) + public void OnNPCStartup(EntityUid uid, HTNComponent component, ComponentStartup args) { component.Blackboard.SetValue(NPCBlackboard.Owner, uid); + } + + public void OnNPCMapInit(EntityUid uid, HTNComponent component, MapInitEvent args) + { WakeNPC(uid, component); } @@ -74,6 +78,11 @@ public void OnNPCShutdown(EntityUid uid, HTNComponent component, ComponentShutdo SleepNPC(uid, component); } + public override bool IsNpc(EntityUid uid) + { + return HasComp(uid); + } + /// /// Is the NPC awake and updating? /// diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 813626a1c44..b30f813a66a 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -5,9 +5,7 @@ using Content.Server.NPC.Queries.Curves; using Content.Server.NPC.Queries.Queries; using Content.Server.Nutrition.Components; -using Content.Server.Temperature.Components; using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Fluids.Components; using Content.Shared.Inventory; @@ -30,6 +28,12 @@ using Robust.Shared.Utility; using Content.Shared.Atmos.Components; using System.Linq; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Mobs.Components; +using Content.Shared.Temperature.Components; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; namespace Content.Server.NPC.Systems; @@ -54,6 +58,8 @@ public sealed class NPCUtilitySystem : EntitySystem [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly MobThresholdSystem _thresholdSystem = default!; [Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedStealthSystem _stealth = default!; private EntityQuery _puddleQuery; private EntityQuery _xformQuery; @@ -185,7 +191,7 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon return 0f; var nutrition = _ingestion.TotalNutrition(targetUid, owner); - if (nutrition <= 1.0f) + if (nutrition == 0.0f) return 0f; return 1f; @@ -283,6 +289,15 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon return Math.Clamp(distance / radius, 0f, 1f); } + case TargetIsVisibleCon: + { + if (!TryComp(targetUid, out StealthComponent? stealth)) + return 1f; // If there is no StealthComponent, we see it. + + // Checking the visibility level + var visibility = _stealth.GetVisibility(targetUid, stealth); + return visibility >= 0.5f ? 1f : 0f; // Visibility threshold 0.5 + } case TargetAmmoCon: { if (!HasComp(targetUid)) @@ -302,11 +317,13 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon } case TargetHealthCon con: { - if (!TryComp(targetUid, out DamageableComponent? damage)) + if (!TryComp(targetUid, out DamageableComponent? damage) || !TryComp(targetUid, out MobThresholdsComponent? threshold)) return 0f; - if (con.TargetState != MobState.Invalid && _thresholdSystem.TryGetPercentageForState(targetUid, con.TargetState, damage.TotalDamage, out var percentage)) + + var totalDamage = _damageable.GetTotalDamage((targetUid, damage)); + if (con.TargetState != MobState.Invalid && _thresholdSystem.TryGetPercentageForState(targetUid, con.TargetState, totalDamage, out var percentage, threshold)) return Math.Clamp((float)(1 - percentage), 0f, 1f); - if (_thresholdSystem.TryGetIncapPercentage(targetUid, damage.TotalDamage, out var incapPercentage)) + if (_thresholdSystem.TryGetIncapPercentage(targetUid, totalDamage, out var incapPercentage, threshold)) return Math.Clamp((float)(1 - incapPercentage), 0f, 1f); return 0f; } @@ -504,7 +521,7 @@ private void Filter(NPCBlackboard blackboard, HashSet entities, Utili { switch (filter) { - case ComponentFilter compFilter: + case Content.Server.NPC.Queries.Queries.ComponentFilter compFilter: { _entityList.Clear(); @@ -512,11 +529,12 @@ private void Filter(NPCBlackboard blackboard, HashSet entities, Utili { foreach (var comp in compFilter.Components) { - if (HasComp(ent, comp.Value.Component.GetType())) - continue; - - _entityList.Add(ent); - break; + var hasComp = HasComp(ent, comp.Value.Component.GetType()); + if (!compFilter.RetainWithComp == hasComp) + { + _entityList.Add(ent); + break; + } } } @@ -601,4 +619,12 @@ public EntityUid GetLowest() return Entities.MinBy(x => x.Value).Key; } + + /// + /// Returns a GetEnumerable sorted in descending score. + /// + public IEnumerable> GetEnumerable() + { + return Entities.OrderByDescending(x => x.Value); + } } diff --git a/Content.Server/NameIdentifier/NameIdentifierSystem.cs b/Content.Server/NameIdentifier/NameIdentifierSystem.cs index 27e25a4b4f3..9f199c45996 100644 --- a/Content.Server/NameIdentifier/NameIdentifierSystem.cs +++ b/Content.Server/NameIdentifier/NameIdentifierSystem.cs @@ -7,10 +7,7 @@ namespace Content.Server.NameIdentifier; -/// -/// Handles unique name identifiers for entities e.g. `monkey (MK-912)` -/// -public sealed class NameIdentifierSystem : EntitySystem +public sealed class NameIdentifierSystem : SharedNameIdentifierSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; @@ -28,7 +25,6 @@ public override void Initialize() SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnComponentShutdown); - SubscribeLocalEvent(OnRefreshNameModifiers); SubscribeLocalEvent(CleanupIds); SubscribeLocalEvent(OnReloadPrototypes); @@ -122,24 +118,6 @@ private void OnMapInit(Entity ent, ref MapInitEvent arg _nameModifier.RefreshNameModifiers(ent.Owner); } - private void OnRefreshNameModifiers(Entity ent, ref RefreshNameModifiersEvent args) - { - if (ent.Comp.Group is null) - return; - - // Don't apply the modifier if the component is being removed - if (ent.Comp.LifeStage > ComponentLifeStage.Running) - return; - - if (!_prototypeManager.Resolve(ent.Comp.Group, out var group)) - return; - - var format = group.FullName ? "name-identifier-format-full" : "name-identifier-format-append"; - // We apply the modifier with a low priority to keep it near the base name - // "Beep (Si-4562) the zombie" instead of "Beep the zombie (Si-4562)" - args.AddModifier(format, -10, ("identifier", ent.Comp.FullIdentifier)); - } - private void InitialSetupPrototypes() { EnsureIds(); diff --git a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs index 71e38ed3f6e..98fae3ae131 100644 --- a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs +++ b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs @@ -1,12 +1,12 @@ using Content.Server.Ninja.Events; using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; using Content.Shared.Popups; -using Robust.Shared.Audio; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Robust.Shared.Audio.Systems; namespace Content.Server.Ninja.Systems; @@ -16,7 +16,7 @@ namespace Content.Server.Ninja.Systems; /// public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem { - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -37,7 +37,7 @@ private void OnBeforeInteractHand(Entity ent, ref Befor { var (uid, comp) = ent; var target = args.Target; - if (args.Handled || comp.BatteryUid is not {} battery || !HasComp(target)) + if (args.Handled || comp.BatteryUid is not { } battery || !HasComp(target)) return; // handles even if battery is full so you can actually see the poup @@ -70,7 +70,7 @@ protected override void OnDoAfterAttempt(Entity ent, re { base.OnDoAfterAttempt(ent, ref args); - if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery)) + if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery)) args.Cancel(); } @@ -84,28 +84,28 @@ protected override bool TryDrainPower(Entity ent, Entit if (!TryComp(target, out var targetBattery) || !TryComp(target, out var pnb)) return false; - if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0)) + var available = _battery.GetCharge((target, targetBattery)); + if (MathHelper.CloseToPercent(available, 0)) { _popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium); return false; } - var available = targetBattery.CurrentCharge; - var required = battery.MaxCharge - battery.CurrentCharge; + var required = battery.MaxCharge - _battery.GetCharge((comp.BatteryUid.Value, battery)); // higher tier storages can charge more var maxDrained = pnb.MaxSupply * comp.DrainTime; var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained); - if (!_battery.TryUseCharge(target, input, targetBattery)) + if (!_battery.TryUseCharge((target, targetBattery), input)) return false; var output = input * comp.DrainEfficiency; - _battery.SetCharge(comp.BatteryUid.Value, battery.CurrentCharge + output, battery); + _battery.ChangeCharge((comp.BatteryUid.Value, battery), output); // TODO: create effect message or something Spawn("EffectSparks", Transform(target).Coordinates); _audio.PlayPvs(comp.SparkSound, target); _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); // repeat the doafter until battery is full - return !_battery.IsFull(comp.BatteryUid.Value, battery); + return !_battery.IsFull((comp.BatteryUid.Value, battery)); } } diff --git a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs index d7a7be995db..cca80f2e42b 100644 --- a/Content.Server/Ninja/Systems/ItemCreatorSystem.cs +++ b/Content.Server/Ninja/Systems/ItemCreatorSystem.cs @@ -1,15 +1,15 @@ using Content.Server.Ninja.Events; -using Content.Server.Power.EntitySystems; using Content.Shared.Hands.EntitySystems; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; using Content.Shared.Popups; +using Content.Shared.Power.EntitySystems; namespace Content.Server.Ninja.Systems; public sealed class ItemCreatorSystem : SharedItemCreatorSystem { - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -24,7 +24,7 @@ public override void Initialize() private void OnCreateItem(Entity ent, ref CreateItemEvent args) { var (uid, comp) = ent; - if (comp.Battery is not {} battery) + if (comp.Battery is not { } battery) return; args.Handled = true; diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs index 62d3d0e3ca4..f6c4643cad4 100644 --- a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs +++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs @@ -1,11 +1,10 @@ -using Content.Server.Emp; using Content.Server.Ninja.Events; -using Content.Server.Power.Components; -using Content.Server.PowerCell; +using Content.Shared.Emp; using Content.Shared.Hands.EntitySystems; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; -using Content.Shared.Popups; +using Content.Shared.Power.Components; +using Content.Shared.PowerCell; using Content.Shared.PowerCell.Components; using Robust.Shared.Containers; @@ -13,10 +12,11 @@ namespace Content.Server.Ninja.Systems; /// /// Handles power cell upgrading and actions. +/// TODO: Move all of this to shared and predict it /// public sealed class NinjaSuitSystem : SharedNinjaSuitSystem { - [Dependency] private readonly EmpSystem _emp = default!; + [Dependency] private readonly SharedEmpSystem _emp = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SpaceNinjaSystem _ninja = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; @@ -30,7 +30,6 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnSuitInsertAttempt); - SubscribeLocalEvent(OnEmpAttempt); SubscribeLocalEvent(OnRecallKatana); SubscribeLocalEvent(OnEmp); } @@ -44,7 +43,7 @@ protected override void NinjaEquipped(Entity ent, Entity ent, Entity(args.EntityUid, out var inserting)) @@ -74,7 +71,7 @@ private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, Contain var user = Transform(uid).ParentUid; // can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power - if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery)) + if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value)) { args.Cancel(); Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user); @@ -95,18 +92,11 @@ private float GetCellScore(EntityUid uid, BatteryComponent battcomp) { // if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate, // this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high. - if (TryComp(uid, out var selfcomp) && selfcomp.AutoRecharge) - return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue); + if (TryComp(uid, out var selfcomp)) + return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue; return battcomp.MaxCharge; } - private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args) - { - // ninja suit (battery) is immune to emp - // powercell relays the event to suit - args.Cancel(); - } - protected override void UserUnequippedSuit(Entity ent, Entity user) { base.UserUnequippedSuit(ent, user); @@ -159,7 +149,6 @@ private void OnEmp(Entity ent, ref NinjaEmpEvent args) if (CheckDisabled(ent, user)) return; - var coords = _transform.GetMapCoordinates(user); - _emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration); + _emp.EmpPulse(Transform(user).Coordinates, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration, user); } } diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs index 1ece045774c..4f3a1873f5c 100644 --- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -1,26 +1,20 @@ using Content.Server.Communications; -using Content.Server.Chat.Managers; using Content.Server.CriminalRecords.Systems; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Objectives.Components; using Content.Server.Objectives.Systems; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; -using Content.Server.PowerCell; using Content.Server.Research.Systems; -using Content.Server.Roles; using Content.Shared.Alert; using Content.Shared.Doors.Components; using Content.Shared.IdentityManagement; using Content.Shared.Mind; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; +using Content.Shared.PowerCell; using Content.Shared.Popups; using Content.Shared.Rounding; -using Robust.Shared.Audio; -using Robust.Shared.Player; using System.Diagnostics.CodeAnalysis; -using Robust.Shared.Audio.Systems; namespace Content.Server.Ninja.Systems; @@ -30,7 +24,7 @@ namespace Content.Server.Ninja.Systems; public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem { [Dependency] private readonly AlertsSystem _alerts = default!; - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly CodeConditionSystem _codeCondition = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly SharedMindSystem _mind = default!; @@ -45,6 +39,8 @@ public override void Initialize() SubscribeLocalEvent(OnCriminalRecordsHacked); } + // TODO: Make this charge rate based instead of updating it every single tick. + // Or make it client side, since power cells are predicted. public override void Update(float frameTime) { var query = EntityQueryEnumerator(); @@ -68,7 +64,7 @@ private int Download(EntityUid uid, List ids) return newCount - oldCount; } - // TODO: can probably copy paste borg code here + // TODO: Generic charge indicator that is combined with borg code. /// /// Update the alert for the ninja's suit power indicator. /// @@ -81,10 +77,10 @@ public void SetSuitPowerAlert(Entity ent) return; } - if (GetNinjaBattery(uid, out _, out var battery)) + if (GetNinjaBattery(uid, out var batteryUid, out var batteryComp)) { - var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8); - _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity); + var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8); + _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity); } else { @@ -95,24 +91,26 @@ public void SetSuitPowerAlert(Entity ent) /// /// Get the battery component in a ninja's suit, if it's worn. /// - public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery) + public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? batteryComp) { if (TryComp(user, out var ninja) && ninja.Suit != null - && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery)) + && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery)) { + batteryUid = battery.Value.Owner; + batteryComp = battery.Value.Comp; return true; } - uid = null; - battery = null; + batteryUid = null; + batteryComp = null; return false; } /// public override bool TryUseCharge(EntityUid user, float charge) { - return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge(uid.Value, charge, battery); + return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge((uid.Value, battery), charge); } /// diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs index 8697692e5eb..387844b6b1a 100644 --- a/Content.Server/Ninja/Systems/StunProviderSystem.cs +++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs @@ -1,16 +1,14 @@ using Content.Server.Ninja.Events; -using Content.Server.Power.EntitySystems; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Interaction; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; using Content.Shared.Popups; +using Content.Shared.Power.EntitySystems; using Content.Shared.Stunnable; using Content.Shared.Timing; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; -using Robust.Shared.Timing; -using Robust.Shared.Prototypes; namespace Content.Server.Ninja.Systems; @@ -19,7 +17,7 @@ namespace Content.Server.Ninja.Systems; /// public sealed class StunProviderSystem : SharedStunProviderSystem { - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -62,7 +60,7 @@ private void OnBeforeInteractHand(Entity ent, ref BeforeI _audio.PlayPvs(comp.Sound, target); - _damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid); + _damageable.ChangeDamage(target, comp.StunDamage, origin: uid); _stun.TryAddParalyzeDuration(target, comp.StunTime); // short cooldown to prevent instant stunlocking diff --git a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs index 7b55e20f8a2..06b6359cd6c 100644 --- a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs +++ b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs @@ -2,7 +2,6 @@ using System.Linq; using Content.Server.Administration.Managers; using Content.Server.NodeContainer.NodeGroups; -using Content.Server.NodeContainer.Nodes; using Content.Shared.Administration; using Content.Shared.NodeContainer; using Content.Shared.NodeContainer.NodeGroups; @@ -350,12 +349,12 @@ private List FloodFillNode(Node rootNode) private IEnumerable GetCompatibleNodes(Node node, EntityQuery xformQuery, EntityQuery nodeQuery) { var xform = xformQuery.GetComponent(node.Owner); - TryComp(xform.GridUid, out var grid); + Entity? gridEnt = TryComp(xform.GridUid, out var grid) ? (xform.GridUid.Value, grid) : null; if (!node.Connectable(EntityManager, xform)) yield break; - foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager)) + foreach (var reachable in node.GetReachableNodes((node.Owner, xform), nodeQuery, xformQuery, gridEnt, EntityManager)) { DebugTools.Assert(reachable != node, "GetReachableNodes() should not include self."); @@ -447,6 +446,7 @@ private static Color CalcNodeGroupColor(BaseNodeGroup group) NodeGroupID.Pipe => Color.Blue, NodeGroupID.WireNet => Color.DarkMagenta, NodeGroupID.Teg => Color.Red, + NodeGroupID.ExCable => Color.Pink, _ => Color.White }; } diff --git a/Content.Server/NodeContainer/NodeGroups/PipeNet.cs b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs index 3c8e65ca91f..591278c8b00 100644 --- a/Content.Server/NodeContainer/NodeGroups/PipeNet.cs +++ b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs @@ -1,103 +1,92 @@ using System.Linq; -using Content.Server.Atmos; using Content.Server.Atmos.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.Atmos; using Content.Shared.NodeContainer; using Content.Shared.NodeContainer.NodeGroups; -using Robust.Shared.Utility; -namespace Content.Server.NodeContainer.NodeGroups +namespace Content.Server.NodeContainer.NodeGroups; + +[NodeGroup(NodeGroupID.Pipe)] +public sealed class PipeNet : BaseNodeGroup, IPipeNet { - public interface IPipeNet : INodeGroup, IGasMixtureHolder - { - /// - /// Causes gas in the PipeNet to react. - /// - void Update(); - } + [ViewVariables] public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C}; - [NodeGroup(NodeGroupID.Pipe)] - public sealed class PipeNet : BaseNodeGroup, IPipeNet - { - [ViewVariables] public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C}; + [ViewVariables] private AtmosphereSystem? _atmosphereSystem; - [ViewVariables] private AtmosphereSystem? _atmosphereSystem; + public EntityUid? Grid { get; private set; } - public EntityUid? Grid { get; private set; } + public override void Initialize(Node sourceNode, IEntityManager entMan) + { + base.Initialize(sourceNode, entMan); + + Grid = entMan.GetComponent(sourceNode.Owner).GridUid; - public override void Initialize(Node sourceNode, IEntityManager entMan) + if (Grid == null) { - base.Initialize(sourceNode, entMan); + // This is probably due to a canister or something like that being spawned in space. + return; + } - Grid = entMan.GetComponent(sourceNode.Owner).GridUid; + _atmosphereSystem = entMan.EntitySysManager.GetEntitySystem(); + _atmosphereSystem.AddPipeNet(Grid.Value, this); + } - if (Grid == null) - { - // This is probably due to a cannister or something like that being spawned in space. - return; - } + public void Update() + { + _atmosphereSystem?.React(Air, this); + } - _atmosphereSystem = entMan.EntitySysManager.GetEntitySystem(); - _atmosphereSystem.AddPipeNet(Grid.Value, this); - } + public override void LoadNodes(List groupNodes) + { + base.LoadNodes(groupNodes); - public void Update() + foreach (var node in groupNodes) { - _atmosphereSystem?.React(Air, this); + var pipeNode = (PipeNode) node; + Air.Volume += pipeNode.Volume; } + } - public override void LoadNodes(List groupNodes) - { - base.LoadNodes(groupNodes); - - foreach (var node in groupNodes) - { - var pipeNode = (PipeNode) node; - Air.Volume += pipeNode.Volume; - } - } + public override void RemoveNode(Node node) + { + base.RemoveNode(node); - public override void RemoveNode(Node node) - { - base.RemoveNode(node); + // if the node is simply being removed into a separate group, we do nothing, as gas redistribution will be + // handled by AfterRemake(). But if it is being deleted, we actually want to remove the gas stored in this node. + if (!node.Deleting || node is not PipeNode pipe) + return; - // if the node is simply being removed into a separate group, we do nothing, as gas redistribution will be - // handled by AfterRemake(). But if it is being deleted, we actually want to remove the gas stored in this node. - if (!node.Deleting || node is not PipeNode pipe) - return; + Air.Multiply(1f - pipe.Volume / Air.Volume); + Air.Volume -= pipe.Volume; + } - Air.Multiply(1f - pipe.Volume / Air.Volume); - Air.Volume -= pipe.Volume; - } + public override void AfterRemake(IEnumerable> newGroups) + { + RemoveFromGridAtmos(); - public override void AfterRemake(IEnumerable> newGroups) + var newAir = new List(newGroups.Count()); + foreach (var newGroup in newGroups) { - RemoveFromGridAtmos(); - - var newAir = new List(newGroups.Count()); - foreach (var newGroup in newGroups) - { - if (newGroup.Key is IPipeNet newPipeNet) - newAir.Add(newPipeNet.Air); - } - - _atmosphereSystem?.DivideInto(Air, newAir); + if (newGroup.Key is IPipeNet newPipeNet) + newAir.Add(newPipeNet.Air); } - private void RemoveFromGridAtmos() - { - if (Grid == null) - return; + _atmosphereSystem?.DivideInto(Air, newAir); + } - _atmosphereSystem?.RemovePipeNet(Grid.Value, this); - } + private void RemoveFromGridAtmos() + { + if (Grid == null) + return; - public override string GetDebugData() - { - return @$"Pressure: { Air.Pressure:G3} + _atmosphereSystem?.RemovePipeNet(Grid.Value, this); + } + + public override string GetDebugData() + { + return @$"Pressure: { Air.Pressure:G3} Temperature: {Air.Temperature:G3} Volume: {Air.Volume:G3}"; - } } } diff --git a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs index d719ccbff07..0a4e9fe4a12 100644 --- a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs +++ b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs @@ -1,5 +1,4 @@ using Content.Shared.NodeContainer; -using Robust.Shared.Map; using Robust.Shared.Map.Components; namespace Content.Server.NodeContainer.Nodes @@ -10,18 +9,20 @@ namespace Content.Server.NodeContainer.Nodes [DataDefinition] public sealed partial class AdjacentNode : Node { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex)) + foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node != this) yield return node; diff --git a/Content.Server/NodeContainer/Nodes/NodeHelpers.cs b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs index c2345fff760..10d29c31200 100644 --- a/Content.Server/NodeContainer/Nodes/NodeHelpers.cs +++ b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs @@ -10,9 +10,9 @@ namespace Content.Server.NodeContainer.Nodes /// public static class NodeHelpers { - public static IEnumerable GetNodesInTile(EntityQuery nodeQuery, MapGridComponent grid, Vector2i coords) + public static IEnumerable GetNodesInTile(EntityQuery nodeQuery, Entity grid, Vector2i coords, SharedMapSystem mapSystem) { - foreach (var entityUid in grid.GetAnchoredEntities(coords)) + foreach (var entityUid in mapSystem.GetAnchoredEntities(grid, coords)) { if (!nodeQuery.TryGetComponent(entityUid, out var container)) continue; @@ -24,13 +24,20 @@ public static IEnumerable GetNodesInTile(EntityQuery and SharedMapSystem")] + public static IEnumerable GetNodesInTile(EntityQuery nodeQuery, MapGridComponent grid, Vector2i coords) + { + return GetNodesInTile(nodeQuery, (grid.Owner, grid), coords, IoCManager.Resolve().System()); + } + public static IEnumerable<(Direction dir, Node node)> GetCardinalNeighborNodes( EntityQuery nodeQuery, - MapGridComponent grid, + Entity grid, Vector2i coords, + SharedMapSystem mapSystem, bool includeSameTile = true) { - foreach (var (dir, entityUid) in GetCardinalNeighborCells(grid, coords, includeSameTile)) + foreach (var (dir, entityUid) in GetCardinalNeighborCells(grid, coords, mapSystem, includeSameTile)) { if (!nodeQuery.TryGetComponent(entityUid, out var container)) continue; @@ -42,29 +49,49 @@ public static IEnumerable GetNodesInTile(EntityQuery and SharedMapSystem")] + public static IEnumerable<(Direction dir, Node node)> GetCardinalNeighborNodes( + EntityQuery nodeQuery, + MapGridComponent grid, + Vector2i coords, + bool includeSameTile = true) + { + return GetCardinalNeighborNodes(nodeQuery, (grid.Owner, grid), coords, IoCManager.Resolve().System(), includeSameTile); + } + [SuppressMessage("ReSharper", "EnforceForeachStatementBraces")] public static IEnumerable<(Direction dir, EntityUid entity)> GetCardinalNeighborCells( - MapGridComponent grid, + Entity grid, Vector2i coords, + SharedMapSystem mapSystem, bool includeSameTile = true) { if (includeSameTile) { - foreach (var uid in grid.GetAnchoredEntities(coords)) + foreach (var uid in mapSystem.GetAnchoredEntities(grid, coords)) yield return (Direction.Invalid, uid); } - foreach (var uid in grid.GetAnchoredEntities(coords + (0, 1))) + foreach (var uid in mapSystem.GetAnchoredEntities(grid, coords + (0, 1))) yield return (Direction.North, uid); - foreach (var uid in grid.GetAnchoredEntities(coords + (0, -1))) + foreach (var uid in mapSystem.GetAnchoredEntities(grid, coords + (0, -1))) yield return (Direction.South, uid); - foreach (var uid in grid.GetAnchoredEntities(coords + (1, 0))) + foreach (var uid in mapSystem.GetAnchoredEntities(grid, coords + (1, 0))) yield return (Direction.East, uid); - foreach (var uid in grid.GetAnchoredEntities(coords + (-1, 0))) + foreach (var uid in mapSystem.GetAnchoredEntities(grid, coords + (-1, 0))) yield return (Direction.West, uid); } + + [Obsolete("Use the overload that passes in Entity and SharedMapSystem")] + public static IEnumerable<(Direction dir, EntityUid entity)> GetCardinalNeighborCells( + MapGridComponent grid, + Vector2i coords, + bool includeSameTile = true) + { + return GetCardinalNeighborCells((grid.Owner, grid), coords, IoCManager.Resolve().System(), includeSameTile); + } } } diff --git a/Content.Server/NodeContainer/Nodes/PipeNode.cs b/Content.Server/NodeContainer/Nodes/PipeNode.cs index 3a76666a2c0..bf1d2616117 100644 --- a/Content.Server/NodeContainer/Nodes/PipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PipeNode.cs @@ -3,6 +3,7 @@ using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.NodeContainer; +using Content.Shared.NodeContainer.NodeGroups; using Robust.Shared.Map.Components; using Robust.Shared.Utility; @@ -156,10 +157,11 @@ public override void OnAnchorStateChanged(IEntityManager entityManager, bool anc CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(xform.LocalRotation); } - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { if (_alwaysReachable != null) @@ -180,10 +182,11 @@ public override IEnumerable GetReachableNodes(TransformComponent xform, } } - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var pos = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var pos = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); for (var i = 0; i < PipeDirectionHelpers.PipeDirections; i++) { @@ -192,7 +195,7 @@ public override IEnumerable GetReachableNodes(TransformComponent xform, if (!CurrentPipeDirection.HasDirection(pipeDir)) continue; - foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, nodeQuery)) + foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, gridEnt, nodeQuery, mapSystem)) { yield return pipe; } @@ -202,10 +205,14 @@ public override IEnumerable GetReachableNodes(TransformComponent xform, /// /// Gets the pipes that can connect to us from entities on the tile or adjacent in a direction. /// - private IEnumerable LinkableNodesInDirection(Vector2i pos, PipeDirection pipeDir, MapGridComponent grid, - EntityQuery nodeQuery) + private IEnumerable LinkableNodesInDirection( + Vector2i pos, + PipeDirection pipeDir, + Entity grid, + EntityQuery nodeQuery, + SharedMapSystem mapSystem) { - foreach (var pipe in PipesInDirection(pos, pipeDir, grid, nodeQuery)) + foreach (var pipe in PipesInDirection(pos, pipeDir, grid, nodeQuery, mapSystem)) { if (pipe.NodeGroupID == NodeGroupID && pipe.CurrentPipeLayer == CurrentPipeLayer @@ -219,12 +226,16 @@ private IEnumerable LinkableNodesInDirection(Vector2i pos, PipeDirecti /// /// Gets the pipes from entities on the tile adjacent in a direction. /// - protected IEnumerable PipesInDirection(Vector2i pos, PipeDirection pipeDir, MapGridComponent grid, - EntityQuery nodeQuery) + protected IEnumerable PipesInDirection( + Vector2i pos, + PipeDirection pipeDir, + Entity grid, + EntityQuery nodeQuery, + SharedMapSystem mapSystem) { var offsetPos = pos.Offset(pipeDir.ToDirection()); - foreach (var entity in grid.GetAnchoredEntities(offsetPos)) + foreach (var entity in mapSystem.GetAnchoredEntities(grid, offsetPos)) { if (!nodeQuery.TryGetComponent(entity, out var container)) continue; diff --git a/Content.Server/NodeContainer/Nodes/PortPipeNode.cs b/Content.Server/NodeContainer/Nodes/PortPipeNode.cs index 04e0dc0ab7a..f8b8b2ad8f2 100644 --- a/Content.Server/NodeContainer/Nodes/PortPipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PortPipeNode.cs @@ -7,18 +7,20 @@ namespace Content.Server.NodeContainer.Nodes [DataDefinition] public sealed partial class PortPipeNode : PipeNode { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node is PortablePipeNode) yield return node; diff --git a/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs b/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs index 427288ee502..0458c403588 100644 --- a/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PortablePipeNode.cs @@ -7,18 +7,20 @@ namespace Content.Server.NodeContainer.Nodes [DataDefinition] public sealed partial class PortablePipeNode : PipeNode { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node is PortPipeNode) yield return node; diff --git a/Content.Server/Nuke/Commands/ToggleNukeCommand.cs b/Content.Server/Nuke/Commands/ToggleNukeCommand.cs index e499afb225e..53bdbd646d6 100644 --- a/Content.Server/Nuke/Commands/ToggleNukeCommand.cs +++ b/Content.Server/Nuke/Commands/ToggleNukeCommand.cs @@ -64,7 +64,7 @@ public override CompletionResult GetCompletion(IConsoleShell shell, string[] arg { if (args.Length == 1) { - return CompletionResult.FromHint(Loc.GetString(Loc.GetString("cmd-nukearm-1-help"))); + return CompletionResult.FromHint(Loc.GetString("cmd-nukearm-1-help")); } if (args.Length == 2) diff --git a/Content.Server/Nuke/NukeComponent.cs b/Content.Server/Nuke/NukeComponent.cs index cd1c0227eb8..e46680460f3 100644 --- a/Content.Server/Nuke/NukeComponent.cs +++ b/Content.Server/Nuke/NukeComponent.cs @@ -5,6 +5,7 @@ using Content.Shared.Nuke; using Robust.Shared.Audio; using Robust.Shared.Map; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Nuke @@ -164,6 +165,14 @@ public sealed partial class NukeComponent : SharedNukeComponent [DataField] public string EnteredCode = ""; + /// + /// Time at which the last nuke code was entered. + /// Used to apply a cooldown to prevent clients from attempting to brute force the nuke code by sending keypad messages every tick. + /// + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan LastCodeEnteredAt = TimeSpan.Zero; + /// /// Current status of a nuclear bomb. /// diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index f110734a604..130cc196562 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Audio; using Content.Server.Chat.Systems; using Content.Server.Explosion.EntitySystems; -using Content.Server.Kitchen.Components; using Content.Server.Pinpointer; using Content.Server.Popups; using Content.Server.Station.Systems; @@ -11,6 +10,7 @@ using Content.Shared.Coordinates.Helpers; using Content.Shared.DoAfter; using Content.Shared.Examine; +using Content.Shared.Kitchen; using Content.Shared.Maps; using Content.Shared.Nuke; using Content.Shared.Popups; @@ -18,11 +18,10 @@ using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Utility; +using Robust.Shared.Timing; namespace Content.Server.Nuke; @@ -45,6 +44,7 @@ public sealed class NukeSystem : EntitySystem [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly IGameTiming _timing = default!; /// /// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx @@ -232,6 +232,12 @@ private void OnEnterButtonPressed(EntityUid uid, NukeComponent component, NukeKe if (component.Status != NukeStatus.AWAIT_CODE) return; + var curTime = _timing.CurTime; + if (curTime < component.LastCodeEnteredAt + SharedNukeComponent.EnterCodeCooldown) + return; // Validate that they are not entering codes faster than the cooldown. + + component.LastCodeEnteredAt = curTime; + UpdateStatus(uid, component); UpdateUserInterface(uid, component); } diff --git a/Content.Server/NukeOps/WarDeclaratorSystem.cs b/Content.Server/NukeOps/WarDeclaratorSystem.cs index a2d74e16b32..71e4431d735 100644 --- a/Content.Server/NukeOps/WarDeclaratorSystem.cs +++ b/Content.Server/NukeOps/WarDeclaratorSystem.cs @@ -44,8 +44,11 @@ private void OnAttemptOpenUI(Entity ent, ref Activatable { if (!_accessReaderSystem.IsAllowed(args.User, ent)) { - var msg = Loc.GetString("war-declarator-not-working"); - _popupSystem.PopupEntity(msg, ent); + if (!args.Silent) + { + var msg = Loc.GetString("war-declarator-not-working"); + _popupSystem.PopupEntity(msg, ent); + } args.Cancel(); return; } diff --git a/Content.Server/Nutrition/Components/BadFoodComponent.cs b/Content.Server/Nutrition/Components/BadFoodComponent.cs index 16f90533cba..40a805e4370 100644 --- a/Content.Server/Nutrition/Components/BadFoodComponent.cs +++ b/Content.Server/Nutrition/Components/BadFoodComponent.cs @@ -6,7 +6,5 @@ namespace Content.Server.Nutrition.Components; /// This component prevents NPC mobs like mice from wanting to eat something that is edible but is not exactly food. /// Including but not limited to: uranium, death pills, insulation /// -[RegisterComponent, Access(typeof(FoodSystem))] -public sealed partial class BadFoodComponent : Component -{ -} +[RegisterComponent] +public sealed partial class BadFoodComponent : Component; diff --git a/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs b/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs index f5b3c326f32..c0f2fefce48 100644 --- a/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs +++ b/Content.Server/Nutrition/Components/IgnoreBadFoodComponent.cs @@ -6,7 +6,5 @@ namespace Content.Server.Nutrition.Components; /// This component allows NPC mobs to eat food with BadFoodComponent. /// See MobMouseAdmeme for usage. /// -[RegisterComponent, Access(typeof(FoodSystem))] -public sealed partial class IgnoreBadFoodComponent : Component -{ -} +[RegisterComponent] +public sealed partial class IgnoreBadFoodComponent : Component; diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index 7164c701f54..202d03c6ea5 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -1,105 +1,5 @@ -using Content.Server.Fluids.EntitySystems; -using Content.Server.Nutrition.Components; -using Content.Server.Popups; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.IdentityManagement; -using Content.Shared.Nutrition; -using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Rejuvenate; -using Content.Shared.Throwing; -using Content.Shared.Trigger.Components; -using Content.Shared.Trigger.Systems; -using Content.Shared.Chemistry.EntitySystems; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -namespace Content.Server.Nutrition.EntitySystems -{ - [UsedImplicitly] - public sealed class CreamPieSystem : SharedCreamPieSystem - { - [Dependency] private readonly IngestionSystem _ingestion = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PuddleSystem _puddle = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; - [Dependency] private readonly TriggerSystem _trigger = default!; +namespace Content.Server.Nutrition.EntitySystems; - public override void Initialize() - { - base.Initialize(); - - // activate BEFORE entity is deleted and trash is spawned - SubscribeLocalEvent(OnConsume, before: [typeof(FoodSystem)]); - SubscribeLocalEvent(OnSlice); - - SubscribeLocalEvent(OnRejuvenate); - } - - protected override void SplattedCreamPie(Entity entity) - { - // The entity is deleted, so play the sound at its position rather than parenting - var coordinates = Transform(entity).Coordinates; - _audio.PlayPvs(_audio.ResolveSound(entity.Comp1.Sound), coordinates, AudioParams.Default.WithVariation(0.125f)); - - if (Resolve(entity, ref entity.Comp2, false)) - { - if (_solutions.TryGetSolution(entity.Owner, entity.Comp2.Solution, out _, out var solution)) - _puddle.TrySpillAt(entity.Owner, solution, out _, false); - - _ingestion.SpawnTrash((entity, entity.Comp2)); - } - - ActivatePayload(entity); - - QueueDel(entity); - } - - private void OnConsume(Entity entity, ref ConsumeDoAfterEvent args) - { - ActivatePayload(entity); - } - - private void OnSlice(Entity entity, ref SliceFoodEvent args) - { - ActivatePayload(entity); - } - - private void ActivatePayload(EntityUid uid) - { - if (_itemSlots.TryGetSlot(uid, CreamPieComponent.PayloadSlotName, out var itemSlot)) - { - if (_itemSlots.TryEject(uid, itemSlot, user: null, out var item)) - { - if (TryComp(item.Value, out var timerTrigger)) - { - _trigger.ActivateTimerTrigger((item.Value, timerTrigger)); - } - } - } - } - - protected override void CreamedEntity(EntityUid uid, CreamPiedComponent creamPied, ThrowHitByEvent args) - { - _popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message", - ("thrown", Identity.Entity(args.Thrown, EntityManager))), - uid, args.Target); - - var otherPlayers = Filter.PvsExcept(uid); - - _popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", - ("owner", Identity.Entity(uid, EntityManager)), - ("thrown", Identity.Entity(args.Thrown, EntityManager))), - uid, otherPlayers, false); - } - - private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) - { - SetCreamPied(entity, entity.Comp, false); - } - } -} +public sealed class CreamPieSystem : SharedCreamPieSystem; diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs index 6905979a5fa..547801c7aec 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs @@ -4,7 +4,7 @@ using Content.Server.Popups; using Content.Shared.Body.Components; using Content.Shared.Atmos; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Emag.Systems; using Content.Shared.IdentityManagement; @@ -42,7 +42,7 @@ private void OnVapeInteraction(Entity entity, ref AfterInteractEv if (!args.CanReach || !_solutionContainerSystem.TryGetRefillableSolution(entity.Owner, out _, out var solution) || !HasComp(args.Target) - || !_ingestion.HasMouthAvailable(args.Target.Value, args.User) + || !_ingestion.HasMouthAvailable(args.User, args.Target.Value) ) { return; diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index 4960ff1ba84..e9ecba3454a 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -106,7 +106,7 @@ private void OnSmokeableEquipEvent(Entity entity, ref GotEqui { if (args.Slot == "mask") { - _forensics.TransferDna(entity.Owner, args.Equipee, false); + _forensics.TransferDna(entity.Owner, args.EquipTarget, false); } } @@ -157,7 +157,7 @@ public override void Update(float frameTime) } _reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion); - _bloodstreamSystem.TryAddToChemicals((containerManager.Owner, bloodstream), inhaledSolution); + _bloodstreamSystem.TryAddToBloodstream((containerManager.Owner, bloodstream), inhaledSolution); } _timer -= UpdateTimer; diff --git a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs index 3eddd45208b..e2f7229fcad 100644 --- a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs @@ -24,7 +24,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 2) { - shell.WriteError(Loc.GetString(Loc.GetString("cmd-addobjective-invalid-args"))); + shell.WriteError(Loc.GetString("cmd-addobjective-invalid-args")); return; } @@ -68,6 +68,6 @@ public override CompletionResult GetCompletion(IConsoleShell shell, string[] arg return CompletionResult.FromHintOptions( _objectives.Objectives(), - Loc.GetString(Loc.GetString("cmd-add-objective-obj-completion"))); + Loc.GetString("cmd-add-objective-obj-completion")); } } diff --git a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs index 4b300cd1951..0624b79f3d2 100644 --- a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs @@ -19,7 +19,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { if (args.Length != 2) { - shell.WriteError(Loc.GetString(Loc.GetString("cmd-rmobjective-invalid-args"))); + shell.WriteError(Loc.GetString("cmd-rmobjective-invalid-args")); return; } diff --git a/Content.Server/Objectives/Components/CounterConditionComponent.cs b/Content.Server/Objectives/Components/CounterConditionComponent.cs new file mode 100644 index 00000000000..c89c9aa9e97 --- /dev/null +++ b/Content.Server/Objectives/Components/CounterConditionComponent.cs @@ -0,0 +1,12 @@ +namespace Content.Server.Objectives.Components; + +/// +/// This is used as a generic counter for objectives. +/// Requires to function. +/// +[RegisterComponent] +public sealed partial class CounterConditionComponent : Component +{ + [DataField] + public int Count; +} diff --git a/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs new file mode 100644 index 00000000000..416c4228379 --- /dev/null +++ b/Content.Server/Objectives/Components/HijackTradeStationConditionComponent.cs @@ -0,0 +1,11 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires the player to hijack the trade station. +/// +[RegisterComponent, Access(typeof(HijackTradeStationConditionSystem))] +public sealed partial class HijackTradeStationConditionComponent : Component +{ +} diff --git a/Content.Server/Objectives/Components/MailFraudConditionComponent.cs b/Content.Server/Objectives/Components/MailFraudConditionComponent.cs new file mode 100644 index 00000000000..850b7f2f13c --- /dev/null +++ b/Content.Server/Objectives/Components/MailFraudConditionComponent.cs @@ -0,0 +1,12 @@ +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires the player to cut into letters and packages addressed to others. +/// Requires to function. +/// Requires to function. +/// +[RegisterComponent] +public sealed partial class MailFraudConditionComponent : Component +{ + +} diff --git a/Content.Server/Objectives/Components/NotJobRequirementComponent.cs b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs index 6f6619da2b1..cfc007e1e16 100644 --- a/Content.Server/Objectives/Components/NotJobRequirementComponent.cs +++ b/Content.Server/Objectives/Components/NotJobRequirementComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.Roles; using Content.Shared.Roles.Jobs; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; /// /// Requires that the player not have a certain job to have this objective. @@ -9,9 +10,10 @@ [RegisterComponent, Access(typeof(NotJobRequirementSystem))] public sealed partial class NotJobRequirementComponent : Component { + /// - /// ID of the job to ban from having this objective. + /// List of job prototype IDs to ban from having this objective. /// - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Job = string.Empty; + [DataField] + public List> Jobs = new List>(); } diff --git a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs index 2c864a80d4d..16e539f320f 100644 --- a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs +++ b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs @@ -16,7 +16,7 @@ public sealed partial class PickRandomPersonComponent : Component /// A pool to pick potential targets from. /// [DataField] - public IMindPool Pool = new AliveHumansPool(); + public MindPool Pool = new AliveHumansPool(); /// /// Filters to apply to . diff --git a/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs b/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs index 9983b35969f..d6309ccdd34 100644 --- a/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs +++ b/Content.Server/Objectives/Components/SpiderChargeConditionComponent.cs @@ -1,5 +1,6 @@ using Content.Server.Ninja.Systems; using Content.Server.Objectives.Systems; +using Content.Shared.Whitelist; namespace Content.Server.Objectives.Components; @@ -14,4 +15,11 @@ public sealed partial class SpiderChargeConditionComponent : Component /// [DataField, ViewVariables(VVAccess.ReadWrite)] public EntityUid? Target; + + /// + /// Tags that should be used to exclude Warp Points + /// from the list of valid bombing targets + /// + [DataField] + public EntityWhitelist? Blacklist; } diff --git a/Content.Server/Objectives/Components/StealAreaComponent.cs b/Content.Server/Objectives/Components/StealAreaComponent.cs deleted file mode 100644 index 26e752f2f28..00000000000 --- a/Content.Server/Objectives/Components/StealAreaComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Server.Objectives.Systems; -using Content.Server.Thief.Systems; - -namespace Content.Server.Objectives.Components; - -/// -/// An abstract component that allows other systems to count adjacent objects as "stolen" when controlling other systems -/// -[RegisterComponent, Access(typeof(StealConditionSystem), typeof(ThiefBeaconSystem))] -public sealed partial class StealAreaComponent : Component -{ - [DataField] - public bool Enabled = true; - - [DataField] - public float Range = 1f; - - /// - /// all the minds that will be credited with stealing from this area. - /// - [DataField] - public HashSet Owners = new(); -} diff --git a/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs b/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs new file mode 100644 index 00000000000..038150d90c8 --- /dev/null +++ b/Content.Server/Objectives/Components/SupercriticalAnomaliesConditionComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Objectives.Components; + +/// +/// Objective condition that requires a certain number of anomalies (defined by ) to go supercritical while the objective is in play. +/// +[RegisterComponent] +public sealed partial class SupercriticalAnomaliesConditionComponent : Component +{ + /// + /// The number of anomalies that have gone supercritical since this objective was added. + /// + [DataField] + public int SupercriticalAnomalies = 0; +} diff --git a/Content.Server/Objectives/Systems/ConditionCounterSystem.cs b/Content.Server/Objectives/Systems/ConditionCounterSystem.cs new file mode 100644 index 00000000000..1cf9318b53c --- /dev/null +++ b/Content.Server/Objectives/Systems/ConditionCounterSystem.cs @@ -0,0 +1,48 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Objectives.Components; +using JetBrains.Annotations; + +namespace Content.Server.Objectives.Systems; + +/// +/// This system handles returning the progress for CounterConditionComponents, +/// which simple increment for traitor NumberObjectives, e.g. cut into 12 envelopes. +/// +public sealed class CounterConditionSystem : EntitySystem +{ + [Dependency] private readonly NumberObjectiveSystem _number = default!; + [Dependency] private EntityQuery _compQuery = default!; + + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnCounterGetProgress); + } + + private void OnCounterGetProgress(Entity ent, ref ObjectiveGetProgressEvent args) + { + args.Progress = GetProgress(ent, _number.GetTarget(ent.Owner)); + } + + private float GetProgress(Entity ent, int target) + { + // prevent divide-by-zero + if (target == 0) + return 1f; + + if (ent.Comp.Count >= target) + return 1f; + + return (float)ent.Comp.Count / target; + } + + [PublicAPI] + public void IncreaseCount(Entity objective) + { + if (_compQuery.Resolve(objective, ref objective.Comp)) + { + objective.Comp.Count++; + } + } +} diff --git a/Content.Server/Objectives/Systems/HijackShuttleConditionSystem.cs b/Content.Server/Objectives/Systems/HijackShuttleConditionSystem.cs index 9e0d2c3d5b5..063d5199262 100644 --- a/Content.Server/Objectives/Systems/HijackShuttleConditionSystem.cs +++ b/Content.Server/Objectives/Systems/HijackShuttleConditionSystem.cs @@ -61,7 +61,7 @@ private float GetProgress(EntityUid mindId, MindComponent mind) private bool IsShuttleHijacked(EntityUid shuttleGridId, EntityUid mindId) { var gridPlayers = Filter.BroadcastGrid(shuttleGridId).Recipients; - var humanoids = GetEntityQuery(); + var humanoids = GetEntityQuery(); var cuffable = GetEntityQuery(); EntityQuery(); diff --git a/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs new file mode 100644 index 00000000000..e5f88a28dda --- /dev/null +++ b/Content.Server/Objectives/Systems/HijackTradeStationConditionSystem.cs @@ -0,0 +1,35 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Cargo.Components; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; +using NetCord; + +namespace Content.Server.Objectives.Systems; + +/// +/// Handles the Hijack Trade Station objective. +/// +public sealed class HijackTradeStationConditionSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetProgress); + } + + private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args) + { + var enumerator = EntityQueryEnumerator(); + args.Progress = 0f; + // If there's any hacked trade station, succeed. + while (enumerator.MoveNext(out var comp)) + { + if (!comp.Hacked) + continue; + + args.Progress = 1f; + return; + } + } +} diff --git a/Content.Server/Objectives/Systems/MailFraudObjectiveSystem.cs b/Content.Server/Objectives/Systems/MailFraudObjectiveSystem.cs new file mode 100644 index 00000000000..a82005fa57f --- /dev/null +++ b/Content.Server/Objectives/Systems/MailFraudObjectiveSystem.cs @@ -0,0 +1,38 @@ +using Content.Server.Mind; +using Content.Server.Objectives.Components; +using Content.Shared.Delivery; +using Content.Shared.FingerprintReader; + +namespace Content.Server.Objectives.Systems; + +public sealed partial class MailFraudObjectiveSystem : EntitySystem +{ + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly FingerprintReaderSystem _fingerprintReader = default!; + [Dependency] private readonly CounterConditionSystem _counterCondition = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnDeliveryOpened); + } + + private void OnDeliveryOpened(Entity ent, ref DeliveryOpenedEvent args) + { + if (!ent.Comp.WasPenalized) + return; //not fraud + + if (_fingerprintReader.IsAllowed(ent.Owner, args.User, out var _, showPopup: false, checkGloves: false)) + return; //cutting open your own letter + + if (!_mind.TryGetMind(args.User, out _, out var mind)) + return; + + foreach (var obj in mind.Objectives) + { + if (HasComp(obj)) + { + _counterCondition.IncreaseCount(obj); + } + } + } +} diff --git a/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs b/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs index db788165037..9004e245ee3 100644 --- a/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs +++ b/Content.Server/Objectives/Systems/NinjaConditionsSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Objectives.Components; using Content.Shared.Objectives.Components; -using Content.Shared.Ninja.Components; using Content.Shared.Roles; using Content.Shared.Roles.Components; using Content.Shared.Warps; +using Content.Shared.Whitelist; using Robust.Shared.Random; namespace Content.Server.Objectives.Systems; @@ -14,6 +14,7 @@ namespace Content.Server.Objectives.Systems; /// public sealed class NinjaConditionsSystem : EntitySystem { + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly NumberObjectiveSystem _number = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -53,10 +54,13 @@ private void OnSpiderChargeRequirementCheck(EntityUid uid, SpiderChargeCondition // choose spider charge detonation point var warps = new List(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var warpUid, out _, out var warp)) + var allEnts = EntityQueryEnumerator(); + var bombingBlacklist = comp.Blacklist; + + while (allEnts.MoveNext(out var warpUid, out var warp)) { - if (warp.Location != null) + if (_whitelist.IsWhitelistFail(bombingBlacklist, warpUid) + && !string.IsNullOrWhiteSpace(warp.Location)) { warps.Add(warpUid); } diff --git a/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs index ac7e579c380..4c88bf03f38 100644 --- a/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/NotJobRequirementSystem.cs @@ -25,7 +25,7 @@ private void OnCheck(EntityUid uid, NotJobRequirementComponent comp, ref Require _jobs.MindTryGetJob(args.MindId, out var proto); // if player has no job then don't care - if (proto is not null && proto.ID == comp.Job) + if (proto is not null && comp.Jobs.Contains(proto.ID)) args.Cancelled = true; } } diff --git a/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs index 0671c6b67e4..e92943a65e5 100644 --- a/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/ObjectiveBlacklistRequirementSystem.cs @@ -25,7 +25,7 @@ private void OnCheck(EntityUid uid, ObjectiveBlacklistRequirementComponent comp, foreach (var objective in args.Mind.Objectives) { - if (_whitelistSystem.IsBlacklistPass(comp.Blacklist, objective)) + if (_whitelistSystem.IsWhitelistPass(comp.Blacklist, objective)) { args.Cancelled = true; return; diff --git a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs index b3075b22747..5b48b25ad4b 100644 --- a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs +++ b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Revolutionary.Components; using Robust.Shared.Random; using System.Linq; +using Content.Shared.Objectives.Systems; namespace Content.Server.Objectives.Systems; @@ -14,8 +15,8 @@ namespace Content.Server.Objectives.Systems; /// public sealed class PickObjectiveTargetSystem : EntitySystem { - [Dependency] private readonly TargetObjectiveSystem _target = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly TargetObjectiveSystem _objective = default!; + [Dependency] private readonly TargetSystem _target = default!; public override void Initialize() { @@ -51,7 +52,7 @@ private void OnSpecificPersonAssigned(Entity ent, r return; } - _target.SetTarget(ent.Owner, targetComp.Target.Value); + _objective.SetTarget(ent.Owner, targetComp.Target.Value); } private void OnRandomPersonAssigned(Entity ent, ref ObjectiveAssignedEvent args) @@ -68,12 +69,12 @@ private void OnRandomPersonAssigned(Entity ent, ref O return; // couldn't find a target :( - if (_mind.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked) + if (_target.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked) { args.Cancelled = true; return; } - _target.SetTarget(ent, picked, target); + _objective.SetTarget(ent, picked, target); } } diff --git a/Content.Server/Objectives/Systems/SpeciesRequirementSystem.cs b/Content.Server/Objectives/Systems/SpeciesRequirementSystem.cs index ab2d1d43703..1a3826d662a 100644 --- a/Content.Server/Objectives/Systems/SpeciesRequirementSystem.cs +++ b/Content.Server/Objectives/Systems/SpeciesRequirementSystem.cs @@ -21,7 +21,7 @@ private void OnCheck(Entity requirement, ref Requir if (args.Cancelled) return; - if (!TryComp(args.Mind.OwnedEntity, out var appearance)) { + if (!TryComp(args.Mind.OwnedEntity, out var appearance)) { args.Cancelled = true; return; } diff --git a/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs b/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs new file mode 100644 index 00000000000..93e649def25 --- /dev/null +++ b/Content.Server/Objectives/Systems/SupercriticalAnomaliesConditionSystem.cs @@ -0,0 +1,41 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Anomaly.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server.Objectives.Systems; + +public sealed partial class SupercriticalAnomaliesConditionSystem : EntitySystem +{ + [Dependency] private readonly NumberObjectiveSystem _numberObjective = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAnomalySupercrit); + SubscribeLocalEvent(OnGetProgress); + } + + private void OnAnomalySupercrit(ref AnomalyShutdownEvent args) + { + if (!args.Supercritical) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var comp)) + { + comp.SupercriticalAnomalies += 1; + } + } + + private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args) + { + var target = _numberObjective.GetTarget(ent); + if (target == 0) + { + args.Progress = 0f; + return; + } + args.Progress = MathF.Min((float)ent.Comp.SupercriticalAnomalies / target, 1f); + } +} diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs index 289b74b2582..a23defdceae 100644 --- a/Content.Server/PAI/PAISystem.cs +++ b/Content.Server/PAI/PAISystem.cs @@ -1,28 +1,23 @@ using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Server.Instruments; -using Content.Server.Kitchen.Components; -using Content.Server.Store.Systems; using Content.Shared.Interaction.Events; using Content.Shared.Mind.Components; +using Content.Shared.Kitchen; using Content.Shared.PAI; using Content.Shared.Popups; -using Content.Shared.Store; -using Content.Shared.Store.Components; using Content.Shared.Instruments; using Robust.Shared.Random; -using Robust.Shared.Prototypes; using System.Text; namespace Content.Server.PAI; -public sealed class PAISystem : SharedPAISystem +public sealed class PAISystem : EntitySystem { [Dependency] private readonly InstrumentSystem _instrumentSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly ToggleableGhostRoleSystem _toggleableGhostRole = default!; /// @@ -38,8 +33,6 @@ public override void Initialize() SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnMindRemoved); SubscribeLocalEvent(OnMicrowaved); - - SubscribeLocalEvent(OnShop); } private void OnUseInHand(EntityUid uid, PAIComponent component, UseInHandEvent args) @@ -106,15 +99,6 @@ private void ScrambleName(EntityUid uid, PAIComponent comp) var val = Loc.GetString("pai-system-pai-name-raw", ("name", name.ToString())); _metaData.SetEntityName(uid, val); } - - private void OnShop(Entity ent, ref PAIShopActionEvent args) - { - if (!TryComp(ent, out var store)) - return; - - _store.ToggleUi(args.Performer, ent, store); - } - public void PAITurningOff(EntityUid uid) { // Close the instrument interface if it was open diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index 282290e4694..724c9dc4914 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; using Content.Server.AlertLevel; using Content.Server.CartridgeLoader; @@ -17,6 +18,8 @@ using Content.Shared.Light.EntitySystems; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; +using Content.Shared.Store.Components; +using Content.Shared.VoiceMask; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -58,10 +61,11 @@ public override void Initialize() SubscribeLocalEvent(OnStationRenamed); SubscribeLocalEvent(OnEntityRenamed, after: new[] { typeof(IdCardSystem) }); SubscribeLocalEvent(OnAlertLevelChanged); - SubscribeLocalEvent>(ChameleonControllerOutfitItemSelected); + SubscribeLocalEvent>(OnRelayedEventToIdCard); + SubscribeLocalEvent>(OnRelayedEventToIdCard); } - private void ChameleonControllerOutfitItemSelected(Entity ent, ref InventoryRelayedEvent args) + private void OnRelayedEventToIdCard(Entity ent, ref InventoryRelayedEvent args) { // Relay it to your ID so it can update as well. if (ent.Comp.ContainedId != null) @@ -187,7 +191,7 @@ public override void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null) var address = GetDeviceNetAddress(uid); var hasInstrument = HasComp(uid); - var showUplink = HasComp(uid) && IsUnlocked(uid); + var showUplink = TryGetUnlockedStore(uid, out _); UpdateStationName(uid, pda); UpdateAlertLevel(uid, pda); @@ -272,8 +276,19 @@ private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowUplinkMessage m return; // check if its locked again to prevent malicious clients opening locked uplinks - if (HasComp(uid) && IsUnlocked(uid)) - _store.ToggleUi(msg.Actor, uid); + if (TryGetUnlockedStore(uid, out var store)) + { + if (store != uid) + { + if (TryComp(uid, out var remoteStore)) + remoteStore.Store = store; + _store.ToggleUi(msg.Actor, store.Value, remoteAccess: uid); + } + else + { + _store.ToggleUi(msg.Actor, store.Value); + } + } } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg) @@ -283,14 +298,24 @@ private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage m if (TryComp(uid, out var uplink)) { + if (TryComp(uid, out var remoteStore)) + remoteStore.Store = null; _ringer.LockUplink((uid, uplink)); UpdatePdaUi(uid, pda); } } - private bool IsUnlocked(EntityUid uid) + /// + /// Returns the currently unlocked store, if there is one. + /// + private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store) { - return !TryComp(uid, out var uplink) || uplink.Unlocked; + store = null; + if (!TryComp(uid, out var uplink) || !uplink.Unlocked) + return false; + + store = _store.GetStore(uid); + return store != null; } private void UpdateStationName(EntityUid uid, PdaComponent pda) diff --git a/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs b/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs new file mode 100644 index 00000000000..f7d124adef6 --- /dev/null +++ b/Content.Server/PDA/Ringer/RingerAccessUplinkComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.PDA; + +namespace Content.Server.PDA.Ringer; + +/// +/// Opens the store UI when a PDA's ringtone is set to the secret code. +/// Traitors are told the code when greeted. +/// +[RegisterComponent, Access(typeof(RingerSystem))] +public sealed partial class RingerAccessUplinkComponent : Component +{ + /// + /// Notes to set ringtone to in order to lock or unlock the uplink. + /// Set via GenerateUplinkCodeEvent. + /// + [DataField] + public Note[]? Code; + + /// + /// If set, the uplink store can only be opened with the given entity. + /// + [DataField] + public EntityUid? BoundEntity; +} diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index b47ca0fde3b..a86b44d3899 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Store.Systems; +using Content.Shared.GameTicking; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; -using Content.Shared.Store.Components; +using Content.Shared.Store; using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Server.PDA.Ringer; @@ -14,15 +16,35 @@ public sealed class RingerSystem : SharedRingerSystem { [Dependency] private readonly IRobustRandom _random = default!; + public static Note[] AllowedNotes = + { + Note.C, + Note.D, + Note.E, + Note.F, + Note.G, + Note.A, + Note.B + }; + + /// + /// Stores the serialized version of any ringtone that can be excluded from new ringtone generations. + /// + [ViewVariables] + public readonly HashSet ReservedSerializedRingtones = new(); + /// public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCurrencyInsert); - SubscribeLocalEvent(OnGenerateUplinkCode); + SubscribeLocalEvent(OnGenerateUplinkCode); + + SubscribeLocalEvent(CleanupReserved); + + InitialSetup(); } /// @@ -30,7 +52,11 @@ public override void Initialize() /// private void OnMapInit(Entity ent, ref MapInitEvent args) { - UpdateRingerRingtone(ent, GenerateRingtone()); + var ringtone = GenerateRingtone(); + + ringtone ??= new Note[RingtoneLength] { Note.A, Note.A, Note.A, Note.A, Note.A, Note.A }; // Fallback + + UpdateRingerRingtone(ent, ringtone); } /// @@ -53,72 +79,212 @@ private void OnCurrencyInsert(Entity ent, ref CurrencyInsertAtt /// /// Handles the for generating an uplink code. /// - private void OnGenerateUplinkCode(Entity ent, ref GenerateUplinkCodeEvent ev) + private void OnGenerateUplinkCode(Entity ent, ref GenerateUplinkCodeEvent ev) { - var code = GenerateRingtone(); + var code = GenerateRingtone(true, true); // Set the code on the component ent.Comp.Code = code; - // Return the code via the event ev.Code = code; } - /// - public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) + private void InitialSetup() { - if (!TryComp(uid, out var uplink)) - return false; - - if (!HasComp(uid)) - return false; + ReservedSerializedRingtones.Clear(); + } - // Wasn't generated yet - if (uplink.Code is null) + /// + public override bool TryToggleUplink(Entity entity, Note[] ringtone, EntityUid? user = null) + { + if (!Resolve(entity, ref entity.Comp)) return false; // On the server, we always check if the code matches - if (!uplink.Code.SequenceEqual(ringtone)) + if (!TryMatchRingtoneToStore(ringtone, out var store, entity)) return false; - return ToggleUplinkInternal((uid, uplink)); + // If the store is not this entity, we make sure to properly set the remote store. + if (store != entity.Owner) + Store.SetRemoteStore(entity.Owner, store); + + return ToggleUplinkInternal((entity, entity.Comp)); } /// - /// Generates a random ringtone using the C pentatonic scale. + /// Generates a random ringtone using the C major scale. /// + /// Exclude any ringtone registered to ReservedSerializedRingtones. + /// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true. /// An array of Notes representing the ringtone. /// The logic for this is on the Server so that we don't get a different result on the Client every time. - private Note[] GenerateRingtone() + private Note[]? GenerateRingtone(bool excludeReserved = false, bool reserveRingtone = false) { - // Default to using C pentatonic so it at least sounds not terrible. - return GenerateRingtone(new[] - { - Note.C, - Note.D, - Note.E, - Note.G, - Note.A - }); + // Default to using C major so it at least sounds not terrible. + return GenerateRingtone(AllowedNotes, excludeReserved, reserveRingtone); } /// /// Generates a random ringtone using the specified notes. /// /// The notes to choose from when generating the ringtone. + /// Exclude any ringtone registered to ReservedSerializedRingtones. + /// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true. /// An array of Notes representing the ringtone. /// The logic for this is on the Server so that we don't get a different result on the Client every time. - private Note[] GenerateRingtone(Note[] notes) + private Note[]? GenerateRingtone(Note[] notes, bool excludeReserved = false, bool reserveRingtone = false) { - var ringtone = new Note[RingtoneLength]; + var excludedRingtones = excludeReserved ? ReservedSerializedRingtones.ToArray() : null; - for (var i = 0; i < RingtoneLength; i++) + var maxPow = Math.Pow(notes.Length, RingtoneLength); + if (maxPow > int.MaxValue) { - ringtone[i] = _random.Pick(notes); + return null; } + var generatedRingtone = NextIntInRangeButExclude(0, Convert.ToInt32(maxPow) - 1, excludedRingtones); + + if (!TryDeserializeRingtone(notes, generatedRingtone, out var ringtone)) + return null; + + if (excludeReserved && reserveRingtone) + ReservedSerializedRingtones.Add(generatedRingtone); + return ringtone; } + + /// + /// Serialize a ringtone, representing it as an Int32. + /// + /// The array of notes used to generate the ringtone. + /// The ringtone which needs to be serialized. + /// The ringtone in a serialized format. + /// Whether the ringtone could be serialized or not. + private bool TrySerializeRingtone(Note[] allowedNotes, Note[] ringtone, [NotNullWhen(true)] out int? serializedRingtone) + { + var noteLength = allowedNotes.Length; + + // The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk. + // If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting. + var maxPow = Math.Pow(noteLength, ringtone.Length); + if (maxPow > int.MaxValue) + { + serializedRingtone = null; + return false; + } + + var serializationValue = 0; + + for (var i = 0; i < ringtone.Length; i++) + { + var pow = Math.Pow(noteLength, i); + var index = Array.IndexOf(allowedNotes, ringtone[i]); + if (index == -1) + { + serializedRingtone = null; + return false; + } + + serializationValue += Convert.ToInt32(pow) * index; + } + + serializedRingtone = serializationValue; + return true; + } + + /// + /// Deserialize a serialized ringtone into a Note array. + /// + /// The array of notes used to generate the ringtone. + /// The ringtone in a serialized format. + /// The ringtone resulting from the deserialization. + /// Whether the ringtone could be deserialized or not. + private bool TryDeserializeRingtone(Note[] allowedNotes, int serializedRingtone, [NotNullWhen(true)] out Note[]? ringtone) + { + var noteLength = allowedNotes.Length; + ringtone = new Note[RingtoneLength]; + + // The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk. + // If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting. + var maxPow = Math.Pow(noteLength, RingtoneLength); + if (maxPow > int.MaxValue) + { + ringtone = null; + return false; + } + + for (var i = 0; i < RingtoneLength; i++) + { + var pow = Math.Pow(noteLength, RingtoneLength - 1 - i); + var powInt = Convert.ToInt32(pow); + var val = serializedRingtone / powInt; + if (!AllowedNotes.TryGetValue(val, out var note)) + { + ringtone = null; + return false; + } + + ringtone[RingtoneLength - 1 - i] = note; + serializedRingtone -= val * powInt; + } + + return true; + } + + /// + /// Try to get the store entity that has the matching ringer access. + /// + /// Notes from the ringer. + /// The store entity, if there is one. + /// The entity providing the code. + public bool TryMatchRingtoneToStore(Note[] notes, [NotNullWhen(true)] out EntityUid? store, EntityUid? ringer = null) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.Code != null && notes.SequenceEqual(comp.Code)) + { + if (comp.BoundEntity != null && comp.BoundEntity != ringer) + break; + + store = uid; + return true; + } + } + + store = null; + return false; + } + + private void CleanupReserved(RoundRestartCleanupEvent ev) + { + ReservedSerializedRingtones.Clear(); + } + + private int NextIntInRangeButExclude(int start, int end, int[]? excludes) + { + excludes ??= new int[0]; + Array.Sort(excludes); + var rangeLength = end - start - excludes.Length; + var randomInt = _random.Next(rangeLength) + start; + + for (var i = 0; i < excludes.Length; i++) + { + if (excludes[i] > randomInt) + { + return randomInt; + } + + randomInt++; + } + + return randomInt; + } + + public void SetBoundUplinkEntity(Entity entity, EntityUid? targetEntity) + { + entity.Comp.BoundEntity = targetEntity; + } } /// diff --git a/Content.Server/Payload/EntitySystems/PayloadSystem.cs b/Content.Server/Payload/EntitySystems/PayloadSystem.cs index 542c3559d52..aed72a1b286 100644 --- a/Content.Server/Payload/EntitySystems/PayloadSystem.cs +++ b/Content.Server/Payload/EntitySystems/PayloadSystem.cs @@ -17,50 +17,49 @@ namespace Content.Server.Payload.EntitySystems; public sealed class PayloadSystem : EntitySystem { - [Dependency] private readonly TagSystem _tagSystem = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly ISerializationManager _serializationManager = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly TransformSystem _transform = default!; private static readonly ProtoId PayloadTag = "Payload"; + // TODO: Construction System Integration tests and remove the EnsureContainer from ConstructionSystem. :( + private static readonly string PayloadContainer = "payload"; + private static readonly string TriggerContainer = "payloadTrigger"; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnCaseTriggered); SubscribeLocalEvent(OnTriggerTriggered); + SubscribeLocalEvent(OnInsertAttempt); SubscribeLocalEvent(OnEntityInserted); SubscribeLocalEvent(OnEntityRemoved); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(HandleChemicalPayloadTrigger); } - public IEnumerable GetAllPayloads(EntityUid uid, ContainerManagerComponent? contMan = null) + public IEnumerable GetAllPayloads(EntityUid uid) { - if (!Resolve(uid, ref contMan, false)) + if (!_container.TryGetContainer(uid, PayloadContainer, out var container)) yield break; - foreach (var container in contMan.Containers.Values) + foreach (var entity in container.ContainedEntities) { - foreach (var entity in container.ContainedEntities) - { - if (_tagSystem.HasTag(entity, PayloadTag)) - yield return entity; - } + if (_tagSystem.HasTag(entity, PayloadTag)) + yield return entity; } } private void OnCaseTriggered(EntityUid uid, PayloadCaseComponent component, TriggerEvent args) { // TODO: Adjust to the new trigger system - - if (!TryComp(uid, out ContainerManagerComponent? contMan)) - return; - // Pass trigger event onto all contained payloads. Payload capacity configurable by construction graphs. - foreach (var ent in GetAllPayloads(uid, contMan)) + foreach (var ent in GetAllPayloads(uid)) { RaiseLocalEvent(ent, ref args, false); } @@ -82,9 +81,18 @@ private void OnTriggerTriggered(EntityUid uid, PayloadTriggerComponent component RaiseLocalEvent(parent, ref args); } + private void OnInsertAttempt(Entity ent, ref ContainerIsInsertingAttemptEvent args) + { + if (args.Container.ID == PayloadContainer && !_tagSystem.HasTag(args.EntityUid, PayloadTag)) + args.Cancel(); + + if (args.Container.ID == TriggerContainer && !HasComp(args.EntityUid)) + args.Cancel(); + } + private void OnEntityInserted(EntityUid uid, PayloadCaseComponent _, EntInsertedIntoContainerMessage args) { - if (!TryComp(args.Entity, out PayloadTriggerComponent? trigger)) + if (args.Container.ID != TriggerContainer || !TryComp(args.Entity, out PayloadTriggerComponent? trigger)) return; trigger.Active = true; @@ -114,7 +122,7 @@ private void OnEntityInserted(EntityUid uid, PayloadCaseComponent _, EntInserted private void OnEntityRemoved(EntityUid uid, PayloadCaseComponent component, EntRemovedFromContainerMessage args) { - if (!TryComp(args.Entity, out PayloadTriggerComponent? trigger)) + if (args.Container.ID != TriggerContainer || !TryComp(args.Entity, out PayloadTriggerComponent? trigger)) return; trigger.Active = false; diff --git a/Content.Server/Physics/Components/ChasingWalkComponent.cs b/Content.Server/Physics/Components/ChasingWalkComponent.cs index 819fd809399..3c66132bd31 100644 --- a/Content.Server/Physics/Components/ChasingWalkComponent.cs +++ b/Content.Server/Physics/Components/ChasingWalkComponent.cs @@ -1,4 +1,5 @@ +using Content.Server.Administration.Systems; using Content.Server.Physics.Controllers; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -8,7 +9,7 @@ namespace Content.Server.Physics.Components; /// /// A component which makes its entity chasing entity with selected component. /// -[RegisterComponent, Access(typeof(ChasingWalkSystem)), AutoGenerateComponentPause] +[RegisterComponent, Access(typeof(ChasingWalkSystem), typeof(AdminVerbSystem)), AutoGenerateComponentPause] public sealed partial class ChasingWalkComponent : Component { /// @@ -78,4 +79,16 @@ public sealed partial class ChasingWalkComponent : Component /// [DataField, ViewVariables(VVAccess.ReadWrite)] public EntityUid? ChasingEntity; + + /// + /// whether the entity should point in the direction its moving + /// + [DataField] + public bool RotateWithImpulse; + + /// + /// Sprite rotation offset. + /// + [DataField] + public Angle RotationAngleOffset = Angle.Zero; } diff --git a/Content.Server/Physics/Controllers/ChasingWalkSystem.cs b/Content.Server/Physics/Controllers/ChasingWalkSystem.cs index fa55ce024e4..ffeb53bea94 100644 --- a/Content.Server/Physics/Controllers/ChasingWalkSystem.cs +++ b/Content.Server/Physics/Controllers/ChasingWalkSystem.cs @@ -101,5 +101,11 @@ private void ForceImpulse(EntityUid uid, ChasingWalkComponent component) _physics.SetLinearVelocity(uid, speed); _physics.SetBodyStatus(uid, physics, BodyStatus.InAir); //If this is not done, from the explosion up close, the tesla will "Fall" to the ground, and almost stop moving. + + if (component.RotateWithImpulse) + { + var ang = speed.ToAngle() + Angle.FromDegrees(90); // we want "Up" to be forward, bullet convention. + _transform.SetWorldRotation(uid, ang + component.RotationAngleOffset); + } } } diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index 5c87de18638..f933c721039 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -1,18 +1,16 @@ +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; -using Content.Shared.Friction; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Prometheus; -using Robust.Shared.Physics.Components; using Robust.Shared.Player; -using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; -using Robust.Shared.Map.Components; +using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; namespace Content.Server.Physics.Controllers; @@ -20,20 +18,111 @@ public sealed class MoverController : SharedMoverController { private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge( "physics_active_mover_count", - "Active amount of InputMovers being processed by MoverController"); + "Amount of ActiveInputMovers being processed by MoverController"); [Dependency] private readonly ThrusterSystem _thruster = default!; - [Dependency] private readonly SharedTransformSystem _xformSystem = default!; private Dictionary)> _shuttlePilots = new(); + private EntityQuery _activeQuery; + private EntityQuery _droneQuery; + private EntityQuery _shuttleQuery; + + // Not needed for persistence; just used to save an alloc + private readonly HashSet _seenMovers = []; + private readonly HashSet _seenRelayMovers = []; + private readonly List> _moversToUpdate = []; + public override void Initialize() { base.Initialize(); + + SubscribeLocalEvent(OnEntityPaused); + SubscribeLocalEvent(OnEntityUnpaused); + SubscribeLocalEvent(OnRelayPlayerAttached); SubscribeLocalEvent(OnRelayPlayerDetached); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); + + _activeQuery = GetEntityQuery(); + _droneQuery = GetEntityQuery(); + _shuttleQuery = GetEntityQuery(); + } + + private void OnEntityPaused(Entity ent, ref EntityPausedEvent args) + { + // Become unactive [sic] if we don't have PhysicsComp.IgnorePaused + if (PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused) + return; + RemCompDeferred(ent); + } + + private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + UpdateMoverStatus((ent, ent.Comp)); + } + + protected override void OnMoverStartup(Entity ent, ref ComponentStartup args) + { + base.OnMoverStartup(ent, ref args); + UpdateMoverStatus((ent, ent.Comp)); + } + + protected override void OnTargetRelayShutdown(Entity ent, ref ComponentShutdown args) + { + base.OnTargetRelayShutdown(ent, ref args); + UpdateMoverStatus((ent, null, ent.Comp)); + } + + protected override void UpdateMoverStatus(Entity ent) + { + // Track that we aren't in a loop of movement relayers + _seenMovers.Clear(); + while (true) + { + if (!MoverQuery.Resolve(ent, ref ent.Comp1, logMissing: false)) + { + RemCompDeferred(ent); + break; + } + + var meta = MetaData(ent); + if (Terminating(ent, meta)) + break; + + ActiveInputMoverComponent? activeMover = null; + if (!meta.EntityPaused + || PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused) + activeMover = EnsureComp(ent); + + // If we're a relay target, make sure our drivers are InputMovers + if (RelayTargetQuery.Resolve(ent, ref ent.Comp2, logMissing: false) + // In case we're called from ComponentShutdown: + && ent.Comp2.LifeStage <= ComponentLifeStage.Running + && Exists(ent.Comp2.Source) + && !_seenMovers.Contains(ent.Comp2.Source)) + { + if (ent.Comp2.Source == ent.Owner) + { + Log.Error($"Entity {ToPrettyString(ent)} is attempting to relay movement to itself!"); + break; + } + + if (activeMover is not null) + activeMover.RelayedFrom = ent.Comp2.Source; + + ent = ent.Comp2.Source; + _seenMovers.Add(ent); + continue; + } + + // No longer a well-defined relay target + if (activeMover is not null) + activeMover.RelayedFrom = null; + + break; + } } private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args) @@ -63,48 +152,70 @@ protected override bool CanSound() return true; } - private HashSet _moverAdded = new(); - private List> _movers = new(); - - private void InsertMover(Entity source) + public override void UpdateBeforeSolve(bool prediction, float frameTime) { - if (TryComp(source, out MovementRelayTargetComponent? relay)) - { - if (TryComp(relay.Source, out InputMoverComponent? relayMover)) - { - InsertMover((relay.Source, relayMover)); - } - } + base.UpdateBeforeSolve(prediction, frameTime); - // Already added - if (!_moverAdded.Add(source.Owner)) - return; + // We use _seenMovers here as well as in UpdateMoverStatus—this means we + // cannot have any events get fired while we use it in this while loop. + _seenMovers.Clear(); + _moversToUpdate.Clear(); - _movers.Add(source); - } + // Don't use EntityQueryEnumerator because admin ghosts have to move on + // paused maps. Pausing movers is handled via ActiveInputMoverComponent. + var inputQueryEnumerator = AllEntityQuery(); + while (inputQueryEnumerator.MoveNext(out var uid, out var activeComp, out var moverComp)) + { + _seenRelayMovers.Clear(); // O(1) if already empty + QueueRelaySources(activeComp.RelayedFrom); - public override void UpdateBeforeSolve(bool prediction, float frameTime) - { - base.UpdateBeforeSolve(prediction, frameTime); + // If it's already inserted, that's fine—that means it'll still be + // handled before its child movers + AddMover((uid, moverComp)); + } - _moverAdded.Clear(); - _movers.Clear(); - var inputQueryEnumerator = AllEntityQuery(); + ActiveMoverGauge.Set(_moversToUpdate.Count); - // Need to order mob movement so that movers don't run before their relays. - while (inputQueryEnumerator.MoveNext(out var uid, out var mover)) + foreach (var ent in _moversToUpdate) { - InsertMover((uid, mover)); + HandleMobMovement(ent, frameTime); } - foreach (var mover in _movers) + HandleShuttleMovement(frameTime); + return; + + // When we insert a chain of relay sources we have to flip its ordering + // It's going to be extremely uncommon for a relay chain to be more than + // one entity so we just recurse as needed. + void QueueRelaySources(EntityUid? next) { - HandleMobMovement(mover, frameTime); + // We only care if it's still a mover + if (!_activeQuery.TryComp(next, out var nextActive) + || !MoverQuery.TryComp(next, out var nextMover) + || !_seenRelayMovers.Add(next.Value)) + return; + + Debug.Assert(next.Value != nextActive.RelayedFrom); + + // While it is (as of writing) currently true that this recursion + // should always terminate due to RelayedFrom always being written + // in a way that tracks if it's made a loop, we still take the extra + // memory (and small time cost) of making sure via _seenRelayMovers. + QueueRelaySources(nextActive.RelayedFrom); + AddMover((next.Value, nextMover)); } - ActiveMoverGauge.Set(_movers.Count); + // Track inserts so we have ~ O(1) inserts without duplicates. Hopefully + // it doesn't matter that both _seenMovers and _moversToUpdate are never + // trimmed? They should be pretty memory light anyway, and in general + // it'll be rare for there to be a decrease in movers. + void AddMover(Entity entity) + { + if (!_seenMovers.Add(entity)) + return; - HandleShuttleMovement(frameTime); + _moversToUpdate.Add(entity); + } } public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component) @@ -152,7 +263,7 @@ private void ResetSubtick(PilotComponent component) protected override void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) { - if (!TryComp(uid, out var pilot) || pilot.Console == null) + if (!PilotQuery.TryComp(uid, out var pilot) || pilot.Console == null) return; ResetSubtick(pilot); @@ -263,27 +374,25 @@ private void HandleShuttleMovement(float frameTime) // We just mark off their movement and the shuttle itself does its own movement var activePilotQuery = EntityQueryEnumerator(); - var shuttleQuery = GetEntityQuery(); while (activePilotQuery.MoveNext(out var uid, out var pilot, out var mover)) { var consoleEnt = pilot.Console; // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks - if (TryComp(consoleEnt, out var cargoConsole)) - { + if (_droneQuery.TryComp(consoleEnt, out var cargoConsole)) consoleEnt = cargoConsole.Entity; - } - if (!TryComp(consoleEnt, out TransformComponent? xform)) continue; + if (!XformQuery.TryComp(consoleEnt, out var xform)) + continue; var gridId = xform.GridUid; // This tries to see if the grid is a shuttle and if the console should work. - if (!TryComp(gridId, out var _) || - !shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) || + if (!MapGridQuery.HasComp(gridId) || + !_shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) || !shuttleComponent.Enabled) continue; - if (!newPilots.TryGetValue(gridId!.Value, out var pilots)) + if (!newPilots.TryGetValue(gridId.Value, out var pilots)) { pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>()); newPilots[gridId.Value] = pilots; @@ -305,13 +414,12 @@ private void HandleShuttleMovement(float frameTime) // Collate all of the linear / angular velocites for a shuttle // then do the movement input once for it. - var xformQuery = GetEntityQuery(); foreach (var (shuttleUid, (shuttle, pilots)) in _shuttlePilots) { - if (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body)) + if (Paused(shuttleUid) || CanPilot(shuttleUid) || !PhysicsQuery.TryComp(shuttleUid, out var body)) continue; - var shuttleNorthAngle = _xformSystem.GetWorldRotation(shuttleUid, xformQuery); + var shuttleNorthAngle = TransformSystem.GetWorldRotation(shuttleUid, XformQuery); // Collate movement linear and angular inputs together var linearInput = Vector2.Zero; @@ -321,7 +429,7 @@ private void HandleShuttleMovement(float frameTime) var brakeCount = 0; var angularCount = 0; - foreach (var (pilotUid, pilot, _, consoleXform) in pilots) + foreach (var (_, pilot, _, consoleXform) in pilots) { var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot); @@ -571,9 +679,9 @@ public static float Vector2Dot(Vector2 value1, Vector2 value2) private bool CanPilot(EntityUid shuttleUid) { - return TryComp(shuttleUid, out var ftl) + return FTLQuery.TryComp(shuttleUid, out var ftl) && (ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0 - || HasComp(shuttleUid); + || PreventPilotQuery.HasComp(shuttleUid); } } diff --git a/Content.Server/Pinpointer/PinpointerSystem.cs b/Content.Server/Pinpointer/PinpointerSystem.cs index 22eb26b16b0..d615a431faa 100644 --- a/Content.Server/Pinpointer/PinpointerSystem.cs +++ b/Content.Server/Pinpointer/PinpointerSystem.cs @@ -4,7 +4,6 @@ using System.Numerics; using Robust.Shared.Utility; using Content.Server.Shuttles.Events; -using Content.Shared.IdentityManagement; namespace Content.Server.Pinpointer; @@ -24,34 +23,35 @@ public override void Initialize() SubscribeLocalEvent(OnLocateTarget); } - public override bool TogglePinpointer(EntityUid uid, PinpointerComponent? pinpointer = null) + public override bool TogglePinpointer(Entity ent) { - if (!Resolve(uid, ref pinpointer)) + if (!Resolve(ent, ref ent.Comp)) return false; - var isActive = !pinpointer.IsActive; - SetActive(uid, isActive, pinpointer); - UpdateAppearance(uid, pinpointer); + var isActive = !ent.Comp.IsActive; + SetActive(ent, isActive); + UpdateAppearance(ent); return isActive; } - private void UpdateAppearance(EntityUid uid, PinpointerComponent pinpointer, AppearanceComponent? appearance = null) + private void UpdateAppearance(Entity ent) { - if (!Resolve(uid, ref appearance)) + if (!Resolve(ent, ref ent.Comp1) || !Resolve(ent, ref ent.Comp2)) return; - _appearance.SetData(uid, PinpointerVisuals.IsActive, pinpointer.IsActive, appearance); - _appearance.SetData(uid, PinpointerVisuals.TargetDistance, pinpointer.DistanceToTarget, appearance); + + _appearance.SetData(ent, PinpointerVisuals.IsActive, ent.Comp1.IsActive, ent.Comp2); + _appearance.SetData(ent, PinpointerVisuals.TargetDistance, ent.Comp1.DistanceToTarget, ent.Comp2); } - private void OnActivate(EntityUid uid, PinpointerComponent component, ActivateInWorldEvent args) + private void OnActivate(Entity ent, ref ActivateInWorldEvent args) { if (args.Handled || !args.Complex) return; - TogglePinpointer(uid, component); + TogglePinpointer(ent.AsNullable()); - if (!component.CanRetarget) - LocateTarget(uid, component); + if (!ent.Comp.CanRetarget) + LocateTarget(ent); args.Handled = true; } @@ -69,12 +69,14 @@ private void OnLocateTarget(ref FTLCompletedEvent ev) if (pinpointer.CanRetarget) continue; - LocateTarget(uid, pinpointer); + LocateTarget((uid, pinpointer)); } } - private void LocateTarget(EntityUid uid, PinpointerComponent component) + private void LocateTarget(Entity ent) { + var component = ent.Comp; + // try to find target from whitelist if (component.IsActive && component.Component != null) { @@ -85,8 +87,8 @@ private void LocateTarget(EntityUid uid, PinpointerComponent component) return; } - var target = FindTargetFromComponent(uid, reg.Type); - SetTarget(uid, target, component); + var target = FindTargetFromComponent(ent.Owner, reg.Type); + SetTarget(ent.AsNullable(), target); } } @@ -99,7 +101,7 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var pinpointer)) { - UpdateDirectionToTarget(uid, pinpointer); + UpdateDirectionToTarget((uid, pinpointer)); } } @@ -107,17 +109,15 @@ public override void Update(float frameTime) /// Try to find the closest entity from whitelist on a current map /// Will return null if can't find anything /// - private EntityUid? FindTargetFromComponent(EntityUid uid, Type whitelist, TransformComponent? transform = null) + private EntityUid? FindTargetFromComponent(Entity ent, Type whitelist) { - _xformQuery.Resolve(uid, ref transform, false); - - if (transform == null) + if (!Resolve(ent, ref ent.Comp)) return null; // sort all entities in distance increasing order - var mapId = transform.MapID; + var mapId = ent.Comp.MapID; var l = new SortedList(); - var worldPos = _transform.GetWorldPosition(transform); + var worldPos = _transform.GetWorldPosition(ent.Comp); foreach (var (otherUid, _) in EntityManager.GetAllComponents(whitelist)) { @@ -135,36 +135,38 @@ public override void Update(float frameTime) /// /// Update direction from pinpointer to selected target (if it was set) /// - protected override void UpdateDirectionToTarget(EntityUid uid, PinpointerComponent? pinpointer = null) + protected override void UpdateDirectionToTarget(Entity ent) { - if (!Resolve(uid, ref pinpointer)) + if (!Resolve(ent, ref ent.Comp)) return; + var pinpointer = ent.Comp; + if (!pinpointer.IsActive) return; var target = pinpointer.Target; if (target == null || !Exists(target.Value)) { - SetDistance(uid, Distance.Unknown, pinpointer); + SetDistance(ent, Distance.Unknown); return; } - var dirVec = CalculateDirection(uid, target.Value); + var dirVec = CalculateDirection(ent, target.Value); var oldDist = pinpointer.DistanceToTarget; if (dirVec != null) { var angle = dirVec.Value.ToWorldAngle(); - TrySetArrowAngle(uid, angle, pinpointer); + TrySetArrowAngle(ent, angle); var dist = CalculateDistance(dirVec.Value, pinpointer); - SetDistance(uid, dist, pinpointer); + SetDistance(ent, dist); } else { - SetDistance(uid, Distance.Unknown, pinpointer); + SetDistance(ent, Distance.Unknown); } if (oldDist != pinpointer.DistanceToTarget) - UpdateAppearance(uid, pinpointer); + UpdateAppearance(ent); } /// diff --git a/Content.Server/Pinpointer/StationMapSystem.cs b/Content.Server/Pinpointer/StationMapSystem.cs index b0b3141fb0e..1e5fa74b7cf 100644 --- a/Content.Server/Pinpointer/StationMapSystem.cs +++ b/Content.Server/Pinpointer/StationMapSystem.cs @@ -1,7 +1,10 @@ -using Content.Server.PowerCell; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Components; +using Content.Shared.PowerCell; using Content.Shared.Pinpointer; +using Content.Shared.Station; using Robust.Server.GameObjects; -using Robust.Shared.Player; namespace Content.Server.Pinpointer; @@ -9,19 +12,49 @@ public sealed class StationMapSystem : EntitySystem { [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly PowerCellSystem _cell = default!; + [Dependency] private readonly SharedStationSystem _station = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnUserParentChanged); - Subs.BuiEvents(StationMapUiKey.Key, subs => + SubscribeLocalEvent(OnNukeOpsStationMap); + SubscribeLocalEvent(OnNukeopsStationSelected); + + Subs.BuiEvents(StationMapUiKey.Key, + subs => { subs.Event(OnStationMapOpened); subs.Event(OnStationMapClosed); }); } + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (!ent.Comp.InitializeWithStation) + return; + + var ev = new ChooseStationMapEvent(); + RaiseLocalEvent(ent, ref ev); + if (ev.Handled) + { + ent.Comp.TargetGrid = ev.TargetGrid; + Dirty(ent); + return; + } + + var station = _station.GetStationInMap(_xform.GetMapId(ent.Owner)); + if (station != null) + { + ent.Comp.TargetGrid = _station.GetLargestGrid((station.Value, null)); + Dirty(ent); + } + } + private void OnStationMapClosed(EntityUid uid, StationMapComponent component, BoundUIClosedEvent args) { if (!Equals(args.UiKey, StationMapUiKey.Key)) @@ -43,4 +76,51 @@ private void OnStationMapOpened(EntityUid uid, StationMapComponent component, Bo var comp = EnsureComp(args.Actor); comp.Map = uid; } + + private void OnNukeOpsStationMap(Entity entity, ref ChooseStationMapEvent args) + { + // If we have this component, we don't want a fallback map! + args.Handle(); + + foreach (var rule in _gameTicker.GetActiveGameRules()) + { + if (rule.Comp.TargetStation == null) + continue; + + args.TargetGrid = _station.GetLargestGrid((rule.Comp.TargetStation.Value, null)); + return; + } + } + + private void OnNukeopsStationSelected(ref NukeopsTargetStationSelectedEvent args) + { + if (args.TargetStation == null || !TryComp(args.RuleEntity, out var ruleGrids)) + return; + + var mapquery = EntityQueryEnumerator(); + while (mapquery.MoveNext(out var uid, out _, out var map)) + { + if (Transform(uid).MapID != ruleGrids.Map) + continue; + + map.TargetGrid = _station.GetLargestGrid((args.TargetStation.Value, null)); + Dirty(uid, map); + } + } +} + +/// +/// Selects an alternative target for our station map! +/// If handled, this will not get the map of the current station. +/// +[ByRefEvent] +public record struct ChooseStationMapEvent +{ + public EntityUid? TargetGrid; + public bool Handled { get; private set; } + + public void Handle() + { + Handled = true; + } } diff --git a/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs b/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs index c47ffa691fa..635e99666f5 100644 --- a/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs +++ b/Content.Server/Players/JobWhitelist/JobWhitelistManager.cs @@ -10,7 +10,6 @@ using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Serilog; namespace Content.Server.Players.JobWhitelist; @@ -22,8 +21,10 @@ public sealed class JobWhitelistManager : IPostInjectInit [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly UserDbDataManager _userDb = default!; + [Dependency] private readonly ILogManager _logManager = default!; private readonly Dictionary> _whitelists = new(); + private ISawmill _sawmill = default!; public void Initialize() { @@ -79,7 +80,7 @@ public bool IsWhitelisted(NetUserId player, ProtoId job) { if (!_whitelists.TryGetValue(player, out var whitelists)) { - Log.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}", + _sawmill.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}", player, job, Environment.StackTrace); @@ -113,5 +114,6 @@ void IPostInjectInit.PostInject() _userDb.AddOnLoadPlayer(LoadData); _userDb.AddOnFinishLoad(FinishLoad); _userDb.AddOnPlayerDisconnect(ClientDisconnected); + _sawmill = _logManager.GetSawmill("job_whitelist"); } } diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs index 3ee83f31538..7d5f177a234 100644 --- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs +++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs @@ -21,7 +21,6 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using Content.Server.Imperial.Sponsors; namespace Content.Server.Players.PlayTimeTracking; @@ -35,7 +34,6 @@ public sealed class PlayTimeTrackingSystem : EntitySystem [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!; - [Dependency] private readonly SponsorsManager _sponsorsManager = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly PlayTimeTrackingManager _tracking = default!; @@ -78,7 +76,9 @@ private void CalcTrackers(ICommonSession player, HashSet trackers) { trackers.Add(PlayTimeTrackingShared.TrackerAdmin); trackers.Add(PlayTimeTrackingShared.TrackerOverall); - return; + + if (!_cfg.GetCVar(CCVars.GameAdminJobTracking)) + return; } if (!IsPlayerAlive(player)) diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index db7fb6a92f3..3df9b07d12c 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -2,9 +2,11 @@ using Content.Server.Humanoid; using Content.Server.Inventory; using Content.Server.Polymorph.Components; +using Content.Shared.Body; using Content.Shared.Buckle; using Content.Shared.Coordinates; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Destructible; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; @@ -33,13 +35,13 @@ public sealed partial class PolymorphSystem : EntitySystem [Dependency] private readonly SharedBuckleSystem _buckle = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; [Dependency] private readonly ServerInventorySystem _inventory = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; @@ -228,7 +230,7 @@ private void OnPolymorphedTerminating(Entity ent, re _mobThreshold.GetScaledDamage(uid, child, out var damage) && damage != null) { - _damageable.SetDamage(child, damageParent, damage); + _damageable.SetDamage((child, damageParent), damage); } if (configuration.Inventory == PolymorphInventoryChange.Transfer) @@ -261,7 +263,7 @@ private void OnPolymorphedTerminating(Entity ent, re if (configuration.TransferHumanoidAppearance) { - _humanoid.CloneAppearance(uid, child); + _visualBody.CopyAppearanceFrom(uid, child); } if (_mindSystem.TryGetMind(uid, out var mindId, out var mind)) @@ -323,7 +325,7 @@ private void OnPolymorphedTerminating(Entity ent, re _mobThreshold.GetScaledDamage(uid, parent, out var damage) && damage != null) { - _damageable.SetDamage(parent, damageParent, damage); + _damageable.SetDamage((parent, damageParent), damage); } if (component.Configuration.Inventory == PolymorphInventoryChange.Transfer) diff --git a/Content.Server/Power/Components/ActiveChargerComponent.cs b/Content.Server/Power/Components/ActiveChargerComponent.cs deleted file mode 100644 index f3d863c9e43..00000000000 --- a/Content.Server/Power/Components/ActiveChargerComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Power; - -namespace Content.Server.Power.Components -{ - [RegisterComponent] - public sealed partial class ActiveChargerComponent : Component - { - } -} diff --git a/Content.Server/Power/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs index 0bf9bc18721..c85f354d204 100644 --- a/Content.Server/Power/Components/ApcComponent.cs +++ b/Content.Server/Power/Components/ApcComponent.cs @@ -5,7 +5,7 @@ namespace Content.Server.Power.Components; -[RegisterComponent] +[RegisterComponent, AutoGenerateComponentPause] public sealed partial class ApcComponent : BaseApcNetComponent { [DataField("onReceiveMessageSound")] @@ -34,6 +34,32 @@ public sealed partial class ApcComponent : BaseApcNetComponent public const float HighPowerThreshold = 0.9f; public static TimeSpan VisualsChangeDelay = TimeSpan.FromSeconds(1); + /// + /// Maximum continuous load in Watts that this APC can supply to loads. Exceeding this starts a + /// timer, which after enough overloading causes the APC to "trip" off. + /// + [DataField] + public float MaxLoad = 40e3f; // Imperial X2 apc capacity + + /// + /// Time that the APC can be continuously overloaded before tripping off. + /// + [DataField] + public TimeSpan TripTime = TimeSpan.FromSeconds(3); + + /// + /// Time that overloading began. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan? TripStartTime; + + /// + /// Set to true if the APC tripped off. Used to indicate problems in the UI. Reset by switching + /// APC on. + /// + [DataField] + public bool TripFlag; + // TODO ECS power a little better! // End the suffering protected override void AddSelfToNet(IApcNet apcNet) diff --git a/Content.Server/Power/Components/BatteryChargerComponent.cs b/Content.Server/Power/Components/BatteryChargerComponent.cs index 99284e44acf..7f3dd39524c 100644 --- a/Content.Server/Power/Components/BatteryChargerComponent.cs +++ b/Content.Server/Power/Components/BatteryChargerComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Power.NodeGroups; +using Content.Shared.Power.Components; namespace Content.Server.Power.Components { diff --git a/Content.Server/Power/Components/BatteryComponent.cs b/Content.Server/Power/Components/BatteryComponent.cs deleted file mode 100644 index 96571bcca32..00000000000 --- a/Content.Server/Power/Components/BatteryComponent.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Content.Server.Power.EntitySystems; -using Content.Shared.Guidebook; - -namespace Content.Server.Power.Components -{ - /// - /// Battery node on the pow3r network. Needs other components to connect to actual networks. - /// - [RegisterComponent] - [Virtual] - [Access(typeof(BatterySystem))] - public partial class BatteryComponent : Component - { - public string SolutionName = "battery"; - - /// - /// Maximum charge of the battery in joules (ie. watt seconds) - /// - [DataField] - [GuidebookData] - public float MaxCharge; - - /// - /// Current charge of the battery in joules (ie. watt seconds) - /// - [DataField("startingCharge")] - public float CurrentCharge; - - /// - /// The price per one joule. Default is 1 credit for 10kJ. - /// - [DataField] - public float PricePerJoule = 0.0001f; - } - - /// - /// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage). - /// - [ByRefEvent] - public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge); - - /// - /// Raised when it is necessary to get information about battery charges. - /// - [ByRefEvent] - public sealed class GetChargeEvent : EntityEventArgs - { - public float CurrentCharge; - public float MaxCharge; - } - - /// - /// Raised when it is necessary to change the current battery charge to a some value. - /// - [ByRefEvent] - public sealed class ChangeChargeEvent : EntityEventArgs - { - public float OriginalValue; - public float ResidualValue; - - public ChangeChargeEvent(float value) - { - OriginalValue = value; - ResidualValue = value; - } - } -} diff --git a/Content.Server/Power/Components/BatterySelfRechargerComponent.cs b/Content.Server/Power/Components/BatterySelfRechargerComponent.cs deleted file mode 100644 index 1cb92d9cd63..00000000000 --- a/Content.Server/Power/Components/BatterySelfRechargerComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Content.Server.Power.Components -{ - /// - /// Self-recharging battery. - /// - [RegisterComponent] - public sealed partial class BatterySelfRechargerComponent : Component - { - /// - /// Does the entity auto recharge? - /// - [DataField] public bool AutoRecharge; - - /// - /// At what rate does the entity automatically recharge? - /// - [DataField] public float AutoRechargeRate; - - /// - /// Should this entity stop automatically recharging if a charge is used? - /// - [DataField] public bool AutoRechargePause = false; - - /// - /// How long should the entity stop automatically recharging if a charge is used? - /// - [DataField] public float AutoRechargePauseTime = 0f; - - /// - /// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system. - /// - [DataField] public TimeSpan NextAutoRecharge = TimeSpan.FromSeconds(0f); - } -} diff --git a/Content.Server/Power/Components/CablePlacerComponent.cs b/Content.Server/Power/Components/CablePlacerComponent.cs index d52cfa118a4..4674692c4a4 100644 --- a/Content.Server/Power/Components/CablePlacerComponent.cs +++ b/Content.Server/Power/Components/CablePlacerComponent.cs @@ -1,16 +1,35 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Content.Shared.Power; +using Content.Shared.Whitelist; namespace Content.Server.Power.Components { [RegisterComponent] public sealed partial class CablePlacerComponent : Component { + /// + /// The structure prototype for the cable coil to place. + /// [DataField("cablePrototypeID", customTypeSerializer:typeof(PrototypeIdSerializer))] public string? CablePrototypeId = "CableHV"; + /// + /// What kind of wire prevents placing this wire over it as CableType. + /// [DataField("blockingWireType")] public CableType BlockingCableType = CableType.HighVoltage; + + /// + /// Blacklist for things the cable cannot be placed over. For things that arent cables with CableTypes. + /// + [DataField] + public EntityWhitelist Blacklist = new(); + + /// + /// Whether the placed cable should go over tiles or not. + /// + [DataField] + public bool OverTile; } } diff --git a/Content.Server/Power/Components/ChargerComponent.cs b/Content.Server/Power/Components/ChargerComponent.cs deleted file mode 100644 index 4a3c83ae03b..00000000000 --- a/Content.Server/Power/Components/ChargerComponent.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Power; -using Content.Shared.Whitelist; - -namespace Content.Server.Power.Components -{ - [RegisterComponent] - public sealed partial class ChargerComponent : Component - { - [ViewVariables] - public CellChargerStatus Status; - - /// - /// The charge rate of the charger, in watts - /// - [DataField("chargeRate")] - public float ChargeRate = 20.0f; - - /// - /// The container ID that is holds the entities being charged. - /// - [DataField("slotId", required: true)] - public string SlotId = string.Empty; - - /// - /// A whitelist for what entities can be charged by this Charger. - /// - [DataField("whitelist")] - public EntityWhitelist? Whitelist; - - /// - /// Indicates whether the charger is portable and thus subject to EMP effects - /// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent. - /// - [DataField] - public bool Portable = false; - } -} diff --git a/Content.Server/Power/Components/ExaminableBatteryComponent.cs b/Content.Server/Power/Components/ExaminableBatteryComponent.cs deleted file mode 100644 index 4cf4abd3cfa..00000000000 --- a/Content.Server/Power/Components/ExaminableBatteryComponent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Server.Power.Components -{ - [RegisterComponent] - public sealed partial class ExaminableBatteryComponent : Component - {} -} diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs index 29c1431179d..19ddc6189c2 100644 --- a/Content.Server/Power/EntitySystems/ApcSystem.cs +++ b/Content.Server/Power/EntitySystems/ApcSystem.cs @@ -1,11 +1,14 @@ -using Content.Server.Emp; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.Pow3r; using Content.Shared.Access.Systems; +using Content.Shared.Administration.Logs; using Content.Shared.APC; +using Content.Shared.Database; using Content.Shared.Emag.Systems; +using Content.Shared.Emp; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Rounding; using Robust.Server.GameObjects; using Robust.Shared.Audio; @@ -17,6 +20,7 @@ namespace Content.Server.Power.EntitySystems; public sealed class ApcSystem : EntitySystem { [Dependency] private readonly AccessReaderSystem _accessReader = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly EmagSystem _emag = default!; [Dependency] private readonly PopupSystem _popup = default!; @@ -42,11 +46,12 @@ public override void Initialize() public override void Update(float deltaTime) { var query = EntityQueryEnumerator(); + var curTime = _gameTiming.CurTime; while (query.MoveNext(out var uid, out var apc, out var battery, out var ui)) { - if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key)) + if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < curTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key)) { - apc.LastUiUpdate = _gameTiming.CurTime; + apc.LastUiUpdate = curTime; UpdateUIState(uid, apc, battery); } @@ -54,13 +59,36 @@ public override void Update(float deltaTime) { UpdateApcState(uid, apc, battery); } + + // Overload + if (apc.MainBreakerEnabled && battery.CurrentSupply > apc.MaxLoad) + { + // Not already overloaded, start timer + if (apc.TripStartTime == null) + { + apc.TripStartTime = curTime; + } + else + { + if (curTime - apc.TripStartTime > apc.TripTime) + { + apc.TripFlag = true; + ApcToggleBreaker(uid, apc, battery); // off, we already checked MainBreakerEnabled above + } + } + } + else + { + apc.TripStartTime = null; + } } } // Change the APC's state only when the battery state changes, or when it's first created. private void OnBatteryChargeChanged(EntityUid uid, ApcComponent component, ref ChargeChangedEvent args) { - UpdateApcState(uid, component); + // Defer until the next tick. + component.NeedStateUpdate = true; } private static void OnApcStartup(EntityUid uid, ApcComponent component, ComponentStartup args) @@ -88,7 +116,7 @@ private void OnToggleMainBreaker(EntityUid uid, ApcComponent component, ApcToggl if (_accessReader.IsAllowed(args.Actor, uid)) { - ApcToggleBreaker(uid, component); + ApcToggleBreaker(uid, component, user: args.Actor); } else { @@ -97,7 +125,12 @@ private void OnToggleMainBreaker(EntityUid uid, ApcComponent component, ApcToggl } } - public void ApcToggleBreaker(EntityUid uid, ApcComponent? apc = null, PowerNetworkBatteryComponent? battery = null) + /// Toggles the enabled state of the APC's main breaker. + public void ApcToggleBreaker( + EntityUid uid, + ApcComponent? apc = null, + PowerNetworkBatteryComponent? battery = null, + EntityUid? user = null) { if (!Resolve(uid, ref apc, ref battery)) return; @@ -105,8 +138,18 @@ public void ApcToggleBreaker(EntityUid uid, ApcComponent? apc = null, PowerNetwo apc.MainBreakerEnabled = !apc.MainBreakerEnabled; battery.CanDischarge = apc.MainBreakerEnabled; + if (apc.MainBreakerEnabled) + apc.TripFlag = false; + UpdateUIState(uid, apc); _audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f)); + + if (user != null) + { + var humanReadableState = apc.MainBreakerEnabled ? "Enabled" : "Disabled"; + _adminLogger.Add(LogType.ItemConfigure, LogImpact.Medium, + $"{ToPrettyString(user):user} set the main breaker state of {ToPrettyString(uid):entity} to {humanReadableState:state}."); + } } private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args) @@ -168,7 +211,9 @@ public void UpdateUIState(EntityUid uid, var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, (int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState, - charge); + charge, + apc.MaxLoad, + apc.TripFlag); _ui.SetUiState((uid, ui), ApcUiKey.Key, state); } @@ -203,6 +248,9 @@ private ApcExternalPowerState CalcExtPowerState(EntityUid uid, PowerState.Batter return ApcExternalPowerState.Good; } + // TODO: This subscription should be in shared. + // But I am not moving ApcComponent to shared, this PR already got soaped enough and that component uses several layers of OOP. + // At least the EMP visuals won't mispredict, since all APCs also have the BatteryComponent, which also has a EMP effect and is in shared. private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args) { if (component.MainBreakerEnabled) diff --git a/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs b/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs index 33e3f8ff2c0..d2c95065db2 100644 --- a/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs +++ b/Content.Server/Power/EntitySystems/BatteryInterfaceSystem.cs @@ -2,6 +2,8 @@ using Content.Server.Power.Components; using Content.Shared.Database; using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Robust.Server.GameObjects; namespace Content.Server.Power.EntitySystems; @@ -23,6 +25,7 @@ public sealed class BatteryInterfaceSystem : EntitySystem { [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = null!; + [Dependency] private readonly SharedBatterySystem _battery = null!; public override void Initialize() { @@ -47,7 +50,7 @@ private void HandleSetInputBreaker(Entity ent, ref Ba var netBattery = Comp(ent); netBattery.CanCharge = args.On; - _adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}"); + _adminLog.Add(LogType.Action, $"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}"); } private void HandleSetOutputBreaker(Entity ent, ref BatterySetOutputBreakerMessage args) @@ -55,7 +58,7 @@ private void HandleSetOutputBreaker(Entity ent, ref B var netBattery = Comp(ent); netBattery.CanDischarge = args.On; - _adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}"); + _adminLog.Add(LogType.Action, $"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}"); } private void HandleSetChargeRate(Entity ent, ref BatterySetChargeRateMessage args) @@ -89,13 +92,14 @@ private void UpdateUI( if (!_uiSystem.IsUiOpen(uid, BatteryUiKey.Key)) return; + var currentCharge = _battery.GetCharge((uid, battery)); _uiSystem.SetUiState( uid, BatteryUiKey.Key, new BatteryBuiState { Capacity = battery.MaxCharge, - Charge = battery.CurrentCharge, + Charge = currentCharge, CanCharge = netBattery.CanCharge, CanDischarge = netBattery.CanDischarge, CurrentReceiving = netBattery.CurrentReceiving, diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs index f4e48f4a3dc..a9b750b6d40 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.cs @@ -1,251 +1,68 @@ -using Content.Server.Emp; using Content.Server.Power.Components; -using Content.Shared.Cargo; -using Content.Shared.Examine; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Rejuvenate; -using JetBrains.Annotations; using Robust.Shared.Utility; -using Robust.Shared.Timing; -namespace Content.Server.Power.EntitySystems +namespace Content.Server.Power.EntitySystems; + +public sealed class BatterySystem : SharedBatterySystem { - [UsedImplicitly] - public sealed class BatterySystem : EntitySystem + public override void Initialize() { - [Dependency] private readonly IGameTiming _timing = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnNetBatteryRejuvenate); - SubscribeLocalEvent(OnBatteryRejuvenate); - SubscribeLocalEvent(CalculateBatteryPrice); - SubscribeLocalEvent(OnEmpPulse); - SubscribeLocalEvent(OnChangeCharge); - SubscribeLocalEvent(OnGetCharge); - - SubscribeLocalEvent(PreSync); - SubscribeLocalEvent(PostSync); - } - - private void OnNetBatteryRejuvenate(EntityUid uid, PowerNetworkBatteryComponent component, RejuvenateEvent args) - { - component.NetworkBattery.CurrentStorage = component.NetworkBattery.Capacity; - } - - private void OnBatteryRejuvenate(EntityUid uid, BatteryComponent component, RejuvenateEvent args) - { - SetCharge(uid, component.MaxCharge, component); - } - - private void OnExamine(EntityUid uid, ExaminableBatteryComponent component, ExaminedEvent args) - { - if (!TryComp(uid, out var batteryComponent)) - return; - if (args.IsInDetailsRange) - { - var effectiveMax = batteryComponent.MaxCharge; - if (effectiveMax == 0) - effectiveMax = 1; - var chargeFraction = batteryComponent.CurrentCharge / effectiveMax; - var chargePercentRounded = (int) (chargeFraction * 100); - args.PushMarkup( - Loc.GetString( - "examinable-battery-component-examine-detail", - ("percent", chargePercentRounded), - ("markupPercentColor", "green") - ) - ); - } - } - - private void PreSync(NetworkBatteryPreSync ev) - { - // Ignoring entity pausing. If the entity was paused, neither component's data should have been changed. - var enumerator = AllEntityQuery(); - while (enumerator.MoveNext(out var netBat, out var bat)) - { - DebugTools.Assert(bat.CurrentCharge <= bat.MaxCharge && bat.CurrentCharge >= 0); - netBat.NetworkBattery.Capacity = bat.MaxCharge; - netBat.NetworkBattery.CurrentStorage = bat.CurrentCharge; - } - } - - private void PostSync(NetworkBatteryPostSync ev) - { - // Ignoring entity pausing. If the entity was paused, neither component's data should have been changed. - var enumerator = AllEntityQuery(); - while (enumerator.MoveNext(out var uid, out var netBat, out var bat)) - { - SetCharge(uid, netBat.NetworkBattery.CurrentStorage, bat); - } - } - - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp, out var batt)) - { - if (!comp.AutoRecharge || IsFull(uid, batt)) - continue; - - if (comp.AutoRechargePause) - { - if (comp.NextAutoRecharge > _timing.CurTime) - continue; - } - - SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt); - } - } - - /// - /// Gets the price for the power contained in an entity's battery. - /// - private void CalculateBatteryPrice(EntityUid uid, BatteryComponent component, ref PriceCalculationEvent args) - { - args.Price += component.CurrentCharge * component.PricePerJoule; - } - - private void OnEmpPulse(EntityUid uid, BatteryComponent component, ref EmpPulseEvent args) - { - args.Affected = true; - UseCharge(uid, args.EnergyConsumption, component); - // Apply a cooldown to the entity's self recharge if needed to avoid it immediately self recharging after an EMP. - TrySetChargeCooldown(uid); - } - - private void OnChangeCharge(Entity entity, ref ChangeChargeEvent args) - { - if (args.ResidualValue == 0) - return; - - args.ResidualValue -= ChangeCharge(entity, args.ResidualValue); - } - - private void OnGetCharge(Entity entity, ref GetChargeEvent args) - { - args.CurrentCharge += entity.Comp.CurrentCharge; - args.MaxCharge += entity.Comp.MaxCharge; - } - - public float UseCharge(EntityUid uid, float value, BatteryComponent? battery = null) - { - if (value <= 0 || !Resolve(uid, ref battery) || battery.CurrentCharge == 0) - return 0; - - return ChangeCharge(uid, -value, battery); - } - - public void SetMaxCharge(EntityUid uid, float value, BatteryComponent? battery = null) - { - if (!Resolve(uid, ref battery)) - return; - - var old = battery.MaxCharge; - battery.MaxCharge = Math.Max(value, 0); - battery.CurrentCharge = Math.Min(battery.CurrentCharge, battery.MaxCharge); - if (MathHelper.CloseTo(battery.MaxCharge, old)) - return; - - var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge); - RaiseLocalEvent(uid, ref ev); - } - - public void SetCharge(EntityUid uid, float value, BatteryComponent? battery = null) - { - if (!Resolve(uid, ref battery)) - return; - - var old = battery.CurrentCharge; - battery.CurrentCharge = MathHelper.Clamp(value, 0, battery.MaxCharge); - if (MathHelper.CloseTo(battery.CurrentCharge, old) && - !(old != battery.CurrentCharge && battery.CurrentCharge == battery.MaxCharge)) - { - return; - } - - var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge); - RaiseLocalEvent(uid, ref ev); - } - - /// - /// Changes the current battery charge by some value - /// - public float ChangeCharge(EntityUid uid, float value, BatteryComponent? battery = null) - { - if (!Resolve(uid, ref battery)) - return 0; - - var newValue = Math.Clamp(0, battery.CurrentCharge + value, battery.MaxCharge); - var delta = newValue - battery.CurrentCharge; - battery.CurrentCharge = newValue; - - TrySetChargeCooldown(uid); + base.Initialize(); - var ev = new ChargeChangedEvent(battery.CurrentCharge, battery.MaxCharge); - RaiseLocalEvent(uid, ref ev); - return delta; - } - - /// - /// Checks if the entity has a self recharge and puts it on cooldown if applicable. - /// - public void TrySetChargeCooldown(EntityUid uid, float value = -1) - { - if (!TryComp(uid, out var batteryself)) - return; - - if (!batteryself.AutoRechargePause) - return; - - // If no answer or a negative is given for value, use the default from AutoRechargePauseTime. - if (value < 0) - value = batteryself.AutoRechargePauseTime; + SubscribeLocalEvent(OnNetBatteryRejuvenate); + SubscribeLocalEvent(PreSync); + SubscribeLocalEvent(PostSync); + } - if (_timing.CurTime + TimeSpan.FromSeconds(value) <= batteryself.NextAutoRecharge) - return; + protected override void OnStartup(Entity ent, ref ComponentStartup args) + { + // Debug assert to prevent anyone from killing their networking performance by dirtying a battery's charge every single tick. + // This checks for components that interact with the power network, have a charge rate that ramps up over time and therefore + // have to set the charge in an update loop instead of using a subscription. + // This is usually the case for APCs, SMES, battery powered turrets or similar. + // For those entities you should disable net sync for the battery in your prototype, using + /// + /// - type: Battery + /// netSync: false + /// + /// This disables networking and prediction for this battery. + if (!ent.Comp.NetSyncEnabled) + return; + + DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!"); + DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!"); + DebugTools.Assert(!HasComp(ent), $"{ToPrettyString(ent.Owner)} has a predicted battery connected to the power net. Disable net sync!"); + } - SetChargeCooldown(uid, batteryself.AutoRechargePauseTime, batteryself); - } - /// - /// Puts the entity's self recharge on cooldown for the specified time. - /// - public void SetChargeCooldown(EntityUid uid, float value, BatterySelfRechargerComponent? batteryself = null) - { - if (!Resolve(uid, ref batteryself)) - return; - - if (value >= 0) - batteryself.NextAutoRecharge = _timing.CurTime + TimeSpan.FromSeconds(value); - else - batteryself.NextAutoRecharge = _timing.CurTime; - } + private void OnNetBatteryRejuvenate(Entity ent, ref RejuvenateEvent args) + { + ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity; + } - /// - /// If sufficient charge is available on the battery, use it. Otherwise, don't. - /// - public bool TryUseCharge(EntityUid uid, float value, BatteryComponent? battery = null) + private void PreSync(NetworkBatteryPreSync ev) + { + // Ignoring entity pausing. If the entity was paused, neither component's data should have been changed. + var enumerator = AllEntityQuery(); + while (enumerator.MoveNext(out var uid, out var netBat, out var bat)) { - if (!Resolve(uid, ref battery, false) || value > battery.CurrentCharge) - return false; - - UseCharge(uid, value, battery); - return true; + var currentCharge = GetCharge((uid, bat)); + DebugTools.Assert(currentCharge <= bat.MaxCharge && currentCharge >= 0); + netBat.NetworkBattery.Capacity = bat.MaxCharge; + netBat.NetworkBattery.CurrentStorage = currentCharge; } + } - /// - /// Returns whether the battery is full. - /// - public bool IsFull(EntityUid uid, BatteryComponent? battery = null) + private void PostSync(NetworkBatteryPostSync ev) + { + // Ignoring entity pausing. If the entity was paused, neither component's data should have been changed. + var enumerator = AllEntityQuery(); + while (enumerator.MoveNext(out var uid, out var netBat, out var bat)) { - if (!Resolve(uid, ref battery)) - return false; - - return battery.CurrentCharge >= battery.MaxCharge; + SetCharge((uid, bat), netBat.NetworkBattery.CurrentStorage); } } } diff --git a/Content.Server/Power/EntitySystems/CableSystem.Placer.cs b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs index 55d517cf7d3..ffe2998c744 100644 --- a/Content.Server/Power/EntitySystems/CableSystem.Placer.cs +++ b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs @@ -4,6 +4,7 @@ using Content.Shared.Interaction; using Content.Shared.Maps; using Content.Shared.Stacks; +using Content.Shared.Whitelist; using Robust.Shared.Map.Components; namespace Content.Server.Power.EntitySystems; @@ -13,6 +14,7 @@ public sealed partial class CableSystem [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private void InitializeCablePlacer() { @@ -35,17 +37,19 @@ private void OnCablePlacerAfterInteract(Entity placer, ref var snapPos = _map.TileIndicesFor((gridUid, grid), args.ClickLocation); var tileDef = (ContentTileDefinition)_tileManager[_map.GetTileRef(gridUid, grid, snapPos).Tile.TypeId]; - if (!tileDef.IsSubFloor || !tileDef.Sturdy) + if ((!component.OverTile && !tileDef.IsSubFloor) || !tileDef.Sturdy) return; - foreach (var anchored in _map.GetAnchoredEntities((gridUid, grid), snapPos)) { + if (_whitelistSystem.IsWhitelistPass(component.Blacklist, anchored)) + return; + if (TryComp(anchored, out var wire) && wire.CableType == component.BlockingCableType) return; } - if (TryComp(placer, out var stack) && !_stack.Use(placer, 1, stack)) + if (TryComp(placer, out var stack) && !_stack.TryUse((placer.Owner, stack), 1)) return; var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos)); diff --git a/Content.Server/Power/EntitySystems/ChargerSystem.cs b/Content.Server/Power/EntitySystems/ChargerSystem.cs deleted file mode 100644 index c128c846fba..00000000000 --- a/Content.Server/Power/EntitySystems/ChargerSystem.cs +++ /dev/null @@ -1,264 +0,0 @@ -using Content.Server.Power.Components; -using Content.Server.Emp; -using Content.Server.PowerCell; -using Content.Shared.Examine; -using Content.Shared.Power; -using Content.Shared.PowerCell.Components; -using Content.Shared.Emp; -using JetBrains.Annotations; -using Robust.Shared.Containers; -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Storage.Components; -using Robust.Server.Containers; -using Content.Shared.Whitelist; - -namespace Content.Server.Power.EntitySystems; - -[UsedImplicitly] -internal sealed class ChargerSystem : EntitySystem -{ - [Dependency] private readonly ContainerSystem _container = default!; - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - public override void Initialize() - { - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnInserted); - SubscribeLocalEvent(OnRemoved); - SubscribeLocalEvent(OnInsertAttempt); - SubscribeLocalEvent(OnEntityStorageInsertAttempt); - SubscribeLocalEvent(OnChargerExamine); - - SubscribeLocalEvent(OnEmpPulse); - } - - private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args) - { - UpdateStatus(uid, component); - } - - private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args) - { - using (args.PushGroup(nameof(ChargerComponent))) - { - // rate at which the charger charges - args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int) component.ChargeRate))); - - // try to get contents of the charger - if (!_container.TryGetContainer(uid, component.SlotId, out var container)) - return; - - if (HasComp(uid)) - return; - - // if charger is empty and not a power cell type charger, add empty message - // power cells have their own empty message by default, for things like flash lights - if (container.ContainedEntities.Count == 0) - { - args.PushMarkup(Loc.GetString("charger-empty")); - } - else - { - // add how much each item is charged it - foreach (var contained in container.ContainedEntities) - { - if (!TryComp(contained, out var battery)) - continue; - - var chargePercentage = (battery.CurrentCharge / battery.MaxCharge) * 100; - args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int) chargePercentage))); - } - } - } - } - - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _, out var charger, out var containerComp)) - { - if (!_container.TryGetContainer(uid, charger.SlotId, out var container, containerComp)) - continue; - - if (charger.Status == CellChargerStatus.Empty || charger.Status == CellChargerStatus.Charged || container.ContainedEntities.Count == 0) - continue; - - foreach (var contained in container.ContainedEntities) - { - TransferPower(uid, contained, charger, frameTime); - } - } - } - - private void OnPowerChanged(EntityUid uid, ChargerComponent component, ref PowerChangedEvent args) - { - UpdateStatus(uid, component); - } - - private void OnInserted(EntityUid uid, ChargerComponent component, EntInsertedIntoContainerMessage args) - { - if (!component.Initialized) - return; - - if (args.Container.ID != component.SlotId) - return; - - UpdateStatus(uid, component); - } - - private void OnRemoved(EntityUid uid, ChargerComponent component, EntRemovedFromContainerMessage args) - { - if (args.Container.ID != component.SlotId) - return; - - UpdateStatus(uid, component); - } - - /// - /// Verify that the entity being inserted is actually rechargeable. - /// - private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args) - { - if (!component.Initialized) - return; - - if (args.Container.ID != component.SlotId) - return; - - if (!TryComp(args.EntityUid, out var cellSlot)) - return; - - if (!cellSlot.FitsInCharger) - args.Cancel(); - } - - private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args) - { - if (!component.Initialized || args.Cancelled) - return; - - if (!TryComp(uid, out var cellSlot)) - return; - - if (!cellSlot.FitsInCharger) - args.Cancelled = true; - } - - private void UpdateStatus(EntityUid uid, ChargerComponent component) - { - var status = GetStatus(uid, component); - TryComp(uid, out AppearanceComponent? appearance); - - if (!_container.TryGetContainer(uid, component.SlotId, out var container)) - return; - - _appearance.SetData(uid, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance); - if (component.Status == status || !TryComp(uid, out ApcPowerReceiverComponent? receiver)) - return; - - component.Status = status; - - if (component.Status == CellChargerStatus.Charging) - { - AddComp(uid); - } - else - { - RemComp(uid); - } - - switch (component.Status) - { - case CellChargerStatus.Off: - receiver.Load = 0; - _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Off, appearance); - break; - case CellChargerStatus.Empty: - receiver.Load = 0; - _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Empty, appearance); - break; - case CellChargerStatus.Charging: - receiver.Load = component.ChargeRate; - _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charging, appearance); - break; - case CellChargerStatus.Charged: - receiver.Load = 0; - _appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charged, appearance); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args) - { - args.Affected = true; - args.Disabled = true; - } - - private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component) - { - if (!component.Portable) - { - if (!TryComp(uid, out TransformComponent? transformComponent) || !transformComponent.Anchored) - return CellChargerStatus.Off; - } - - if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent)) - return CellChargerStatus.Off; - - if (!component.Portable && !apcPowerReceiverComponent.Powered) - return CellChargerStatus.Off; - - if (HasComp(uid)) - return CellChargerStatus.Off; - - if (!_container.TryGetContainer(uid, component.SlotId, out var container)) - return CellChargerStatus.Off; - - if (container.ContainedEntities.Count == 0) - return CellChargerStatus.Empty; - - if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery)) - return CellChargerStatus.Off; - - if (_battery.IsFull(heldEnt.Value, heldBattery)) - return CellChargerStatus.Charged; - - return CellChargerStatus.Charging; - } - - private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime) - { - if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent)) - return; - - if (!receiverComponent.Powered) - return; - - if (_whitelistSystem.IsWhitelistFail(component.Whitelist, targetEntity)) - return; - - if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery)) - return; - - _battery.SetCharge(batteryUid.Value, heldBattery.CurrentCharge + component.ChargeRate * frameTime, heldBattery); - UpdateStatus(uid, component); - } - - private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component) - { - // try get a battery directly on the inserted entity - if (!TryComp(uid, out component)) - { - // or by checking for a power cell slot on the inserted entity - return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component); - } - batteryUid = uid; - return true; - } -} diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index be79d64fc9a..e0344f4d6c9 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -1,6 +1,4 @@ -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; -using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Server.Power.Nodes; using Content.Server.Power.NodeGroups; @@ -9,6 +7,8 @@ using Content.Shared.Pinpointer; using Content.Shared.Station.Components; using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Map.Components; @@ -23,6 +23,7 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori { [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; // Note: this data does not need to be saved private Dictionary> _gridPowerCableChunks = new(); @@ -511,7 +512,7 @@ private PowerStats GetPowerStats(EntityUid uid, PowerMonitoringDeviceComponent d if (effectiveMax == 0) effectiveMax = 1; - return battery.CurrentCharge / effectiveMax; + return _battery.GetCharge((uid, battery)) / effectiveMax; } private void GetSourcesForNode(EntityUid uid, Node node, out List sources) diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 6a69550fcfd..1c3f76fdee4 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -358,17 +358,18 @@ private void UpdateApcPowerReceiver(float frameTime) if (requireBattery) { - _battery.SetCharge(uid, battery.CurrentCharge - apcBattery.IdleLoad * frameTime, battery); + _battery.ChangeCharge((uid, battery), -apcBattery.IdleLoad * frameTime); } // Otherwise try to charge the battery - else if (powered && !_battery.IsFull(uid, battery)) + else if (powered && !_battery.IsFull((uid, battery))) { apcReceiver.Load += apcBattery.BatteryRechargeRate * apcBattery.BatteryRechargeEfficiency; - _battery.SetCharge(uid, battery.CurrentCharge + apcBattery.BatteryRechargeRate * frameTime, battery); + _battery.ChangeCharge((uid, battery), apcBattery.BatteryRechargeRate * frameTime); } // Enable / disable the battery if the state changed - var enableBattery = requireBattery && battery.CurrentCharge > 0; + var currentCharge = _battery.GetCharge((uid, battery)); + var enableBattery = requireBattery && currentCharge > 0; if (apcBattery.Enabled != enableBattery) { diff --git a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs index f3405486e67..f5ecb118dc5 100644 --- a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs @@ -161,11 +161,6 @@ public bool IsPowered(EntityUid uid, ApcPowerReceiverComponent? receiver = null) return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered; } - public void SetLoad(ApcPowerReceiverComponent comp, float load) - { - comp.Load = load; - } - public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component) { if (component != null) diff --git a/Content.Server/Power/EntitySystems/PowerStateSystem.cs b/Content.Server/Power/EntitySystems/PowerStateSystem.cs new file mode 100644 index 00000000000..92c103bf8ae --- /dev/null +++ b/Content.Server/Power/EntitySystems/PowerStateSystem.cs @@ -0,0 +1,21 @@ +using Content.Server.Power.Components; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; + +namespace Content.Server.Power.EntitySystems; + +public sealed class PowerStateSystem : SharedPowerStateSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentStartup); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + EnsureComp(ent); + SetWorkingState(ent.Owner, ent.Comp.IsWorking); + } +} diff --git a/Content.Server/Power/EntitySystems/RiggableSystem.cs b/Content.Server/Power/EntitySystems/RiggableSystem.cs index 26eaca80fe7..0bff0f4ba81 100644 --- a/Content.Server/Power/EntitySystems/RiggableSystem.cs +++ b/Content.Server/Power/EntitySystems/RiggableSystem.cs @@ -1,9 +1,12 @@ using Content.Server.Administration.Logs; using Content.Server.Explosion.EntitySystems; -using Content.Server.Kitchen.Components; using Content.Server.Power.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Database; +using Content.Shared.Kitchen; +using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Rejuvenate; namespace Content.Server.Power.EntitySystems; @@ -15,6 +18,7 @@ public sealed class RiggableSystem : EntitySystem { [Dependency] private readonly ExplosionSystem _explosionSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; public override void Initialize() { @@ -22,6 +26,7 @@ public override void Initialize() SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnMicrowaved); SubscribeLocalEvent(OnSolutionChanged); + SubscribeLocalEvent(OnChargeChanged); } private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) @@ -33,14 +38,13 @@ private void OnMicrowaved(Entity entity, ref BeingMicrowavedE { if (TryComp(entity, out var batteryComponent)) { - if (batteryComponent.CurrentCharge == 0) + var charge = _battery.GetCharge((entity, batteryComponent)); + if (charge == 0f) return; - } - - args.Handled = true; - // What the fuck are you doing??? - Explode(entity.Owner, batteryComponent, args.User); + Explode(entity, charge); + args.Handled = true; + } } private void OnSolutionChanged(Entity entity, ref SolutionContainerChangedEvent args) @@ -58,14 +62,26 @@ private void OnSolutionChanged(Entity entity, ref SolutionCon } } - public void Explode(EntityUid uid, BatteryComponent? battery = null, EntityUid? cause = null) + public void Explode(EntityUid uid, float charge, EntityUid? cause = null) { - if (!Resolve(uid, ref battery)) + var radius = MathF.Min(5, MathF.Sqrt(charge) / 9); + + _explosionSystem.TriggerExplosive(uid, radius: radius, user: cause); + QueueDel(uid); + } + + private void OnChargeChanged(Entity ent, ref ChargeChangedEvent args) + { + if (!ent.Comp.IsRigged) return; - var radius = MathF.Min(5, MathF.Sqrt(battery.CurrentCharge) / 9); + if (args.CurrentCharge == 0f) + return; // No charge to cause an explosion. - _explosionSystem.TriggerExplosive(uid, radius: radius, user:cause); - QueueDel(uid); + // Don't explode if we are not using any charge. + if (args.CurrentChargeRate == 0f && args.Delta == 0f) + return; + + Explode(ent, args.CurrentCharge); } } diff --git a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs index 92a353ccb11..d1056635539 100644 --- a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs +++ b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs @@ -127,18 +127,19 @@ public override void LoadNodes(List groupNodes) public sealed partial class TegNodeGenerator : Node { public override IEnumerable GetReachableNodes( - TransformComponent xform, + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - var dir = xform.LocalRotation.GetDir(); + var dir = xform.Comp.LocalRotation.GetDir(); var a = FindCirculator(dir); var b = FindCirculator(dir.GetOpposite()); @@ -152,7 +153,7 @@ public override IEnumerable GetReachableNodes( { var targetIdx = gridIndex.Offset(searchDir); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, targetIdx, mapSystem)) { if (node is not TegNodeCirculator circulator) continue; @@ -179,22 +180,23 @@ public override IEnumerable GetReachableNodes( public sealed partial class TegNodeCirculator : Node { public override IEnumerable GetReachableNodes( - TransformComponent xform, + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - var dir = xform.LocalRotation.GetDir(); + var dir = xform.Comp.LocalRotation.GetDir(); var searchDir = dir.GetClockwise90Degrees(); var targetIdx = gridIndex.Offset(searchDir); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, targetIdx, mapSystem)) { if (node is not TegNodeGenerator generator) continue; diff --git a/Content.Server/Power/Generation/Teg/TegSystem.cs b/Content.Server/Power/Generation/Teg/TegSystem.cs index ef578c81bf1..c8b75559be9 100644 --- a/Content.Server/Power/Generation/Teg/TegSystem.cs +++ b/Content.Server/Power/Generation/Teg/TegSystem.cs @@ -1,13 +1,10 @@ -using Content.Server.Atmos; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Audio; -using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork.Systems; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Examine; @@ -17,7 +14,6 @@ using Content.Shared.Power.Generation.Teg; using Content.Shared.Rounding; using Robust.Server.GameObjects; -using Robust.Shared.Utility; namespace Content.Server.Power.Generation.Teg; diff --git a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs index e3979a65192..d26977c87ae 100644 --- a/Content.Server/Power/Generator/GasPowerReceiverSystem.cs +++ b/Content.Server/Power/Generator/GasPowerReceiverSystem.cs @@ -1,10 +1,8 @@ using Content.Server.Atmos.EntitySystems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; -using Content.Server.Power.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Power; namespace Content.Server.Power.Generator; diff --git a/Content.Server/Power/Nodes/CableDeviceNode.cs b/Content.Server/Power/Nodes/CableDeviceNode.cs index 4089cd56579..98c0261198d 100644 --- a/Content.Server/Power/Nodes/CableDeviceNode.cs +++ b/Content.Server/Power/Nodes/CableDeviceNode.cs @@ -1,4 +1,3 @@ -using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Shared.NodeContainer; @@ -31,18 +30,20 @@ public override bool Connectable(IEntityManager entMan, TransformComponent? xfor return base.Connectable(entMan, xform); } - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (node is CableNode) yield return node; diff --git a/Content.Server/Power/Nodes/CableNode.cs b/Content.Server/Power/Nodes/CableNode.cs index f933285004f..18bd25e913f 100644 --- a/Content.Server/Power/Nodes/CableNode.cs +++ b/Content.Server/Power/Nodes/CableNode.cs @@ -1,7 +1,5 @@ -using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; using Content.Shared.NodeContainer; -using Robust.Shared.Map; using Robust.Shared.Map.Components; using Content.Shared.Glue; @@ -10,23 +8,25 @@ namespace Content.Server.Power.Nodes [DataDefinition] public sealed partial class CableNode : Node { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); // While we go over adjacent nodes, we build a list of blocked directions due to // incoming or outgoing wire terminals. var terminalDirs = 0; List<(Direction, Node)> nodeDirs = new(); - foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex)) + foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, gridEnt, gridIndex, mapSystem)) { if (entMan.HasComponent(Owner)) { diff --git a/Content.Server/Power/Nodes/CableTerminalNode.cs b/Content.Server/Power/Nodes/CableTerminalNode.cs index 8988a9950b0..6e113b2106a 100644 --- a/Content.Server/Power/Nodes/CableTerminalNode.cs +++ b/Content.Server/Power/Nodes/CableTerminalNode.cs @@ -9,21 +9,23 @@ namespace Content.Server.Power.Nodes [DataDefinition] public sealed partial class CableTerminalNode : CableDeviceNode { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - var dir = xform.LocalRotation.GetDir(); + var dir = xform.Comp.LocalRotation.GetDir(); var targetIdx = gridIndex.Offset(dir); - foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx)) + foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, gridEnt, targetIdx, mapSystem)) { if (node is CableTerminalPortNode) yield return node; diff --git a/Content.Server/Power/Nodes/CableTerminalPortNode.cs b/Content.Server/Power/Nodes/CableTerminalPortNode.cs index b2397ebb6ca..7cfb94f1138 100644 --- a/Content.Server/Power/Nodes/CableTerminalPortNode.cs +++ b/Content.Server/Power/Nodes/CableTerminalPortNode.cs @@ -1,7 +1,5 @@ -using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; using Content.Shared.NodeContainer; -using Robust.Shared.Map; using Robust.Shared.Map.Components; namespace Content.Server.Power.Nodes @@ -9,18 +7,20 @@ namespace Content.Server.Power.Nodes [DataDefinition] public sealed partial class CableTerminalPortNode : Node { - public override IEnumerable GetReachableNodes(TransformComponent xform, + public override IEnumerable GetReachableNodes( + Entity xform, EntityQuery nodeQuery, EntityQuery xformQuery, - MapGridComponent? grid, + Entity? grid, IEntityManager entMan) { - if (!xform.Anchored || grid == null) + if (!xform.Comp.Anchored || grid is not { } gridEnt) yield break; - var gridIndex = grid.TileIndicesFor(xform.Coordinates); + var mapSystem = entMan.System(); + var gridIndex = mapSystem.TileIndicesFor(gridEnt, xform.Comp.Coordinates); - var nodes = NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex, includeSameTile: false); + var nodes = NodeHelpers.GetCardinalNeighborNodes(nodeQuery, gridEnt, gridIndex, mapSystem, includeSameTile: false); foreach (var (dir, node) in nodes) { if (node is CableTerminalNode diff --git a/Content.Server/Power/SMES/SmesSystem.cs b/Content.Server/Power/SMES/SmesSystem.cs index ebd56ee3e1b..5a8169e1c92 100644 --- a/Content.Server/Power/SMES/SmesSystem.cs +++ b/Content.Server/Power/SMES/SmesSystem.cs @@ -1,6 +1,8 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Rounding; using Content.Shared.SMES; using JetBrains.Annotations; @@ -13,6 +15,7 @@ internal sealed class SmesSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; public override void Initialize() { @@ -60,7 +63,8 @@ private int CalcChargeLevel(EntityUid uid, BatteryComponent? battery = null) if (!Resolve(uid, ref battery, false)) return 0; - return ContentHelpers.RoundToLevels(battery.CurrentCharge, battery.MaxCharge, 6); + var currentCharge = _battery.GetCharge((uid, battery)); + return ContentHelpers.RoundToLevels(currentCharge, battery.MaxCharge, 6); } private ChargeState CalcChargeState(EntityUid uid, PowerNetworkBatteryComponent? netBattery = null) diff --git a/Content.Server/Power/SetBatteryPercentCommand.cs b/Content.Server/Power/SetBatteryPercentCommand.cs index 7ceee20172c..f13a1b820e4 100644 --- a/Content.Server/Power/SetBatteryPercentCommand.cs +++ b/Content.Server/Power/SetBatteryPercentCommand.cs @@ -1,7 +1,7 @@ using Content.Server.Administration; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Shared.Administration; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Robust.Shared.Console; namespace Content.Server.Power @@ -9,7 +9,7 @@ namespace Content.Server.Power [AdminCommand(AdminFlags.Debug)] public sealed class SetBatteryPercentCommand : LocalizedEntityCommands { - [Dependency] private readonly BatterySystem _batterySystem = default!; + [Dependency] private readonly SharedBatterySystem _batterySystem = default!; public override string Command => "setbatterypercent"; @@ -35,12 +35,13 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - if (!EntityManager.TryGetComponent(id, out var battery)) + if (EntityManager.TryGetComponent(id, out var battery)) + _batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100); + else { shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id))); return; } - _batterySystem.SetCharge(id.Value, battery.MaxCharge * percent / 100, battery); // Don't acknowledge b/c people WILL forall this } } diff --git a/Content.Server/PowerCell/PowerCellSystem.Draw.cs b/Content.Server/PowerCell/PowerCellSystem.Draw.cs deleted file mode 100644 index 9156d30b1fc..00000000000 --- a/Content.Server/PowerCell/PowerCellSystem.Draw.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Content.Server.Power.Components; -using Content.Shared.PowerCell; -using Content.Shared.PowerCell.Components; - -namespace Content.Server.PowerCell; - -public sealed partial class PowerCellSystem -{ - /* - * Handles PowerCellDraw - */ - - public override void Update(float frameTime) - { - base.Update(frameTime); - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var comp, out var slot)) - { - if (!comp.Enabled) - continue; - - if (Timing.CurTime < comp.NextUpdateTime) - continue; - - comp.NextUpdateTime += comp.Delay; - - if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot)) - continue; - - if (_battery.TryUseCharge(batteryEnt.Value, comp.DrawRate * (float)comp.Delay.TotalSeconds, battery)) - continue; - - var ev = new PowerCellSlotEmptyEvent(); - RaiseLocalEvent(uid, ref ev); - } - } - - private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component, ref ChargeChangedEvent args) - { - // Update the bools for client prediction. - var canUse = component.UseRate <= 0f || args.Charge > component.UseRate; - - var canDraw = component.DrawRate <= 0f || args.Charge > 0f; - - if (canUse != component.CanUse || canDraw != component.CanDraw) - { - component.CanDraw = canDraw; - component.CanUse = canUse; - Dirty(uid, component); - } - } - - private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component, PowerCellChangedEvent args) - { - var canDraw = !args.Ejected && HasCharge(uid, float.MinValue); - var canUse = !args.Ejected && HasActivatableCharge(uid, component); - - if (!canDraw) - { - var ev = new PowerCellSlotEmptyEvent(); - RaiseLocalEvent(uid, ref ev); - } - - if (canUse != component.CanUse || canDraw != component.CanDraw) - { - component.CanDraw = canDraw; - component.CanUse = canUse; - Dirty(uid, component); - } - } -} diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs deleted file mode 100644 index 01767d6c41e..00000000000 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ /dev/null @@ -1,266 +0,0 @@ -using Content.Server.Emp; -using Content.Server.Power.Components; -using Content.Shared.Examine; -using Content.Shared.PowerCell; -using Content.Shared.PowerCell.Components; -using Content.Shared.Rounding; -using Robust.Shared.Containers; -using System.Diagnostics.CodeAnalysis; -using Content.Server.Kitchen.Components; -using Content.Server.Power.EntitySystems; -using Content.Server.UserInterface; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Popups; -using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem; - -namespace Content.Server.PowerCell; - -/// -/// Handles Power cells -/// -public sealed partial class PowerCellSystem : SharedPowerCellSystem -{ - [Dependency] private readonly ActivatableUISystem _activatable = default!; - [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _sharedAppearanceSystem = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly RiggableSystem _riggableSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnChargeChanged); - SubscribeLocalEvent(OnCellExamined); - SubscribeLocalEvent(OnCellEmpAttempt); - - SubscribeLocalEvent(OnDrawChargeChanged); - SubscribeLocalEvent(OnDrawCellChanged); - - SubscribeLocalEvent(OnCellSlotExamined); - // funny - SubscribeLocalEvent(OnSlotMicrowaved); - - SubscribeLocalEvent(OnGetCharge); - SubscribeLocalEvent(OnChangeCharge); - } - - private void OnSlotMicrowaved(EntityUid uid, PowerCellSlotComponent component, BeingMicrowavedEvent args) - { - if (!_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out var slot)) - return; - - if (slot.Item == null) - return; - - RaiseLocalEvent(slot.Item.Value, args); - } - - private void OnChargeChanged(EntityUid uid, PowerCellComponent component, ref ChargeChangedEvent args) - { - if (TryComp(uid, out var rig) && rig.IsRigged) - { - _riggableSystem.Explode(uid, cause: null); - return; - } - - var frac = args.Charge / args.MaxCharge; - var level = (byte)ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels); - _sharedAppearanceSystem.SetData(uid, PowerCellVisuals.ChargeLevel, level); - - // If this power cell is inside a cell-slot, inform that entity that the power has changed (for updating visuals n such). - if (_containerSystem.TryGetContainingContainer((uid, null, null), out var container) - && TryComp(container.Owner, out PowerCellSlotComponent? slot) - && _itemSlotsSystem.TryGetSlot(container.Owner, slot.CellSlotId, out var itemSlot)) - { - if (itemSlot.Item == uid) - RaiseLocalEvent(container.Owner, new PowerCellChangedEvent(false)); - } - } - - protected override void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args) - { - base.OnCellRemoved(uid, component, args); - - if (args.Container.ID != component.CellSlotId) - return; - - var ev = new PowerCellSlotEmptyEvent(); - RaiseLocalEvent(uid, ref ev); - } - - #region Activatable - /// - public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null) - { - // Default to true if we don't have the components. - if (!Resolve(uid, ref battery, ref cell, false)) - return true; - - return HasCharge(uid, battery.UseRate, cell, user); - } - - /// - /// Tries to use the for this entity. - /// - /// Popup to this user with the relevant detail if specified. - public bool TryUseActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null) - { - // Default to true if we don't have the components. - if (!Resolve(uid, ref battery, ref cell, false)) - return true; - - if (TryUseCharge(uid, battery.UseRate, cell, user)) - { - _sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, HasActivatableCharge(uid, battery, cell, user)); - _activatable.CheckUsage(uid); - return true; - } - - return false; - } - - /// - public override bool HasDrawCharge( - EntityUid uid, - PowerCellDrawComponent? battery = null, - PowerCellSlotComponent? cell = null, - EntityUid? user = null) - { - if (!Resolve(uid, ref battery, ref cell, false)) - return true; - - return HasCharge(uid, battery.DrawRate, cell, user); - } - - #endregion - - /// - /// Returns whether the entity has a slotted battery and charge for the requested action. - /// - /// Popup to this user with the relevant detail if specified. - public bool HasCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null) - { - if (!TryGetBatteryFromSlot(uid, out var battery, component)) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value); - - return false; - } - - if (battery.CurrentCharge < charge) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value); - - return false; - } - - return true; - } - - /// - /// Tries to use charge from a slotted battery. - /// - public bool TryUseCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null) - { - if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, component)) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value); - - return false; - } - - if (!_battery.TryUseCharge(batteryEnt.Value, charge, battery)) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value); - - return false; - } - - _sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, battery.CurrentCharge > 0); - return true; - } - - public bool TryGetBatteryFromSlot(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, PowerCellSlotComponent? component = null) - { - return TryGetBatteryFromSlot(uid, out _, out battery, component); - } - - public bool TryGetBatteryFromSlot(EntityUid uid, - [NotNullWhen(true)] out EntityUid? batteryEnt, - [NotNullWhen(true)] out BatteryComponent? battery, - PowerCellSlotComponent? component = null) - { - if (!Resolve(uid, ref component, false)) - { - batteryEnt = null; - battery = null; - return false; - } - - if (_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out ItemSlot? slot)) - { - batteryEnt = slot.Item; - return TryComp(slot.Item, out battery); - } - - batteryEnt = null; - battery = null; - return false; - } - - private void OnCellExamined(EntityUid uid, PowerCellComponent component, ExaminedEvent args) - { - TryComp(uid, out var battery); - OnBatteryExamined(uid, battery, args); - } - - private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args) - { - var parent = Transform(uid).ParentUid; - // relay the attempt event to the slot so it can cancel it - if (HasComp(parent)) - RaiseLocalEvent(parent, args); - } - - private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args) - { - TryGetBatteryFromSlot(uid, out var battery); - OnBatteryExamined(uid, battery, args); - } - - private void OnBatteryExamined(EntityUid uid, BatteryComponent? component, ExaminedEvent args) - { - if (component != null) - { - var charge = component.CurrentCharge / component.MaxCharge * 100; - args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}"))); - } - else - { - args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery")); - } - } - - private void OnGetCharge(Entity entity, ref GetChargeEvent args) - { - if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _)) - return; - - RaiseLocalEvent(batteryUid.Value, ref args); - } - - private void OnChangeCharge(Entity entity, ref ChangeChargeEvent args) - { - if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _)) - return; - - RaiseLocalEvent(batteryUid.Value, ref args); - } -} diff --git a/Content.Server/PowerSink/PowerSinkSystem.cs b/Content.Server/PowerSink/PowerSinkSystem.cs index 6970675cb87..f86f9649dc1 100644 --- a/Content.Server/PowerSink/PowerSinkSystem.cs +++ b/Content.Server/PowerSink/PowerSinkSystem.cs @@ -1,13 +1,13 @@ -using Content.Server.Explosion.EntitySystems; -using Content.Server.Power.Components; -using Content.Shared.Examine; -using Robust.Shared.Utility; using Content.Server.Chat.Systems; +using Content.Server.Explosion.EntitySystems; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; using Content.Server.Station.Systems; -using Robust.Shared.Timing; -using Robust.Shared.Audio; +using Content.Shared.Examine; +using Content.Shared.Power.Components; using Robust.Shared.Audio.Systems; -using Content.Server.Power.EntitySystems; +using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.PowerSink { @@ -66,9 +66,9 @@ public override void Update(float frameTime) if (!transform.Anchored) continue; - _battery.SetCharge(entity, battery.CurrentCharge + networkLoad.NetworkLoad.ReceivingPower / 1000, battery); + _battery.ChangeCharge((entity, battery), networkLoad.NetworkLoad.ReceivingPower * frameTime); - var currentBatteryThreshold = battery.CurrentCharge / battery.MaxCharge; + var currentBatteryThreshold = _battery.GetChargeLevel((entity, battery)); // Check for warning message threshold if (!component.SentImminentExplosionWarningMessage && @@ -96,7 +96,7 @@ public override void Update(float frameTime) //Imperial Space Pirates: New Horizon; End // Check for explosion - if (battery.CurrentCharge < battery.MaxCharge) + if (!_battery.IsFull((entity, battery))) continue; if (component.ExplosionTime == null) diff --git a/Content.Server/Prayer/PrayerSystem.cs b/Content.Server/Prayer/PrayerSystem.cs index b3c5ebff209..32ef6860fa5 100644 --- a/Content.Server/Prayer/PrayerSystem.cs +++ b/Content.Server/Prayer/PrayerSystem.cs @@ -76,7 +76,7 @@ private void AddPrayVerb(EntityUid uid, PrayableComponent comp, GetVerbsEventThe IPlayerSession that sent the message /// The main message sent to the player via the chatbox /// The popup to notify the player, also prepended to the messageString - public void SendSubtleMessage(ICommonSession target, ICommonSession source, string messageString, string popupMessage) + public void SendSubtleMessage(ICommonSession target, ICommonSession? source, string messageString, string popupMessage) { if (target.AttachedEntity == null) return; @@ -85,7 +85,7 @@ public void SendSubtleMessage(ICommonSession target, ICommonSession source, stri _popupSystem.PopupEntity(popupMessage, target.AttachedEntity.Value, target, PopupType.Large); _chatManager.ChatMessageToOne(ChatChannel.Local, messageString, message, EntityUid.Invalid, false, target.Channel); - _adminLogger.Add(LogType.AdminMessage, LogImpact.Low, $"{ToPrettyString(target.AttachedEntity.Value):player} received subtle message from {source.Name}: {message}"); + _adminLogger.Add(LogType.AdminMessage, LogImpact.Low, $"{ToPrettyString(target.AttachedEntity.Value):player} received subtle message from {source?.Name ?? "unknown source"}: {message}"); } /// diff --git a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs index ed6f2b17e64..15620c462ec 100644 --- a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs @@ -20,10 +20,10 @@ public interface IServerPreferencesManager bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences); PlayerPreferences GetPreferences(NetUserId userId); PlayerPreferences? GetPreferencesOrNull(NetUserId? userId); - IEnumerable> GetSelectedProfilesForPlayers(List userIds); + IEnumerable> GetSelectedProfilesForPlayers(List userIds); bool HavePreferencesLoaded(ICommonSession session); - Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile); + Task SetProfile(NetUserId userId, int slot, HumanoidCharacterProfile profile); Task SetConstructionFavorites(NetUserId userId, List> favorites); } } diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index c7df0bf48a1..7f5d9c70f42 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -1,16 +1,26 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Content.Server.Database; +using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Construction.Prototypes; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; +using Content.Shared.Preferences.Loadouts; +using Content.Shared.Roles; +using Content.Shared.Traits; using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; using Content.Server.Imperial.Sponsors; @@ -31,6 +41,8 @@ public sealed class ServerPreferencesManager : IServerPreferencesManager, IPostI [Dependency] private readonly UserDbDataManager _userDb = default!; [Dependency] private readonly SponsorsManager _sponsorsManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MarkingManager _marking = default!; // Imperial Medieval + [Dependency] private readonly ISerializationManager _serialization = default!; // Imperial Medieval [Dependency] private readonly Imperial.Medieval.Flavors.ServerFlavorManager _serverFlavors = default!; // Imperial Medieval Flavor Images // Cache player prefs on the server so we don't need as much async hell related to them. @@ -51,6 +63,135 @@ public void Init() _sawmill = _log.GetSawmill("prefs"); } + private static TValue? TryDeserialize(JsonDocument document) where TValue : class + { + try + { + return document.Deserialize(); + } + catch (JsonException) + { + return null; + } + } + + internal PlayerPreferences ConvertPreferences(Preference prefs) + { + var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1; + var profiles = new Dictionary(maxSlot); + foreach (var profile in prefs.Profiles) + { + profiles[profile.Slot] = ConvertProfiles(profile); + } + + var constructionFavorites = new List>(prefs.ConstructionFavorites.Count); + foreach (var favorite in prefs.ConstructionFavorites) + constructionFavorites.Add(new ProtoId(favorite)); + + return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor), constructionFavorites); + } + + internal HumanoidCharacterProfile ConvertProfiles(Profile profile) + { + + var jobs = profile.Jobs.ToDictionary(j => new ProtoId(j.JobName), j => (JobPriority) j.Priority); + var antags = profile.Antags.Select(a => new ProtoId(a.AntagName)); + var traits = profile.Traits.Select(t => new ProtoId(t.TraitName)); + + var sex = Sex.Male; + if (Enum.TryParse(profile.Sex, true, out var sexVal)) + sex = sexVal; + + var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority; + + var gender = sex == Sex.Male ? Gender.Male : Gender.Female; + if (Enum.TryParse(profile.Gender, true, out var genderVal)) + gender = genderVal; + + + var markings = + new Dictionary, Dictionary>>(); + + var species = profile.Species; + if (!_prototypeManager.HasIndex(species)) + species = HumanoidCharacterProfile.DefaultSpecies; + + if (profile.OrganMarkings?.RootElement is { } element) + { + var data = element.ToDataNode(); + markings = _serialization + .Read, Dictionary>>>( + data, + notNullableOverride: true); + } + else if (profile.Markings is { } profileMarkings && TryDeserialize>(profileMarkings) is { } markingsRaw) + { + List markingsList = new(); + + foreach (var marking in markingsRaw) + { + var parsed = Marking.ParseFromDbString(marking); + + if (parsed is null) continue; + + markingsList.Add(parsed.Value); + } + + if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } facialMarking) + markingsList.Add(facialMarking); + + if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } hairMarking) + markingsList.Add(hairMarking); + + markings = _marking.ConvertMarkings(markingsList, species); + } + + var loadouts = new Dictionary(); + + foreach (var role in profile.Loadouts) + { + var loadout = new RoleLoadout(role.RoleName) + { + EntityName = role.EntityName, + }; + + foreach (var group in role.Groups) + { + var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName); + foreach (var profLoadout in group.Loadouts) + { + groupLoadouts.Add(new Loadout() + { + Prototype = profLoadout.LoadoutName, + }); + } + } + + loadouts[role.RoleName] = loadout; + } + + return new HumanoidCharacterProfile( + profile.CharacterName, + profile.FlavorText, + species, + profile.Age, + sex, + gender, + new HumanoidCharacterAppearance + ( + Color.FromHex(profile.EyeColor), + Color.FromHex(profile.SkinColor), + markings + ), + spawnPriority, + jobs, + (PreferenceUnavailableMode) profile.PreferenceUnavailable, + antags.ToHashSet(), + traits.ToHashSet(), + loadouts + ); + } + private async void HandleSelectCharacterMessage(MsgSelectCharacter message) { var index = message.SelectedCharacterIndex; @@ -94,7 +235,7 @@ private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message) await SetProfile(userId, message.Slot, message.Profile); } - public async Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile) + public async Task SetProfile(NetUserId userId, int slot, HumanoidCharacterProfile profile) { if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded) { @@ -110,7 +251,7 @@ public async Task SetProfile(NetUserId userId, int slot, ICharacterProfile profi profile.EnsureValid(session, _dependencies); - var profiles = new Dictionary(curPrefs.Characters) + var profiles = new Dictionary(curPrefs.Characters) { [slot] = profile }; @@ -171,7 +312,7 @@ private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message) nextSlot = ns; } - var arr = new Dictionary(curPrefs.Characters); + var arr = new Dictionary(curPrefs.Characters); arr.Remove(slot); prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites); @@ -234,7 +375,7 @@ public async Task LoadData(ICommonSession session, CancellationToken cancel) { PrefsLoaded = true, Prefs = new PlayerPreferences( - new[] { new KeyValuePair(0, HumanoidCharacterProfile.Random()) }, + new[] { new KeyValuePair(0, HumanoidCharacterProfile.Random()) }, 0, Color.Transparent, []) }; @@ -251,7 +392,7 @@ public async Task LoadData(ICommonSession session, CancellationToken cancel) async Task LoadPrefs() { var prefs = await GetOrCreatePreferencesAsync(session.UserId, cancel); - prefsData.Prefs = prefs; + prefsData.Prefs = ConvertPreferences(prefs); } } } @@ -334,12 +475,14 @@ public PlayerPreferences GetPreferences(NetUserId userId) return null; } - private async Task GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel) + private async Task GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel) { var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel); if (prefs is null) { - return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(), cancel); + var speciesToBlacklist = + new HashSet(_cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(",")); + return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(speciesToBlacklist), cancel); } return prefs; @@ -352,17 +495,17 @@ private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPref return new PlayerPreferences(prefs.Characters.Select(p => { - return new KeyValuePair(p.Key, p.Value.Validated(session, collection)); + return new KeyValuePair(p.Key, p.Value.Validated(session, collection)); }), prefs.SelectedCharacterIndex, prefs.AdminOOCColor, prefs.ConstructionFavorites); } - public IEnumerable> GetSelectedProfilesForPlayers( + public IEnumerable> GetSelectedProfilesForPlayers( List usernames) { return usernames .Select(p => (_cachedPlayerPrefs[p].Prefs, p)) .Where(p => p.Prefs != null) - .Select(p => new KeyValuePair(p.p, p.Prefs!.SelectedCharacter)); + .Select(p => new KeyValuePair(p.p, p.Prefs!.SelectedCharacter)); } internal static bool ShouldStorePrefs(LoginType loginType) diff --git a/Content.Server/Procedural/DungeonSystem.Rooms.cs b/Content.Server/Procedural/DungeonSystem.Rooms.cs index e5b0981b3db..0854fbb172d 100644 --- a/Content.Server/Procedural/DungeonSystem.Rooms.cs +++ b/Content.Server/Procedural/DungeonSystem.Rooms.cs @@ -7,6 +7,7 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Utility; +using Robust.Shared.Random; namespace Content.Server.Procedural; @@ -19,7 +20,7 @@ public sealed partial class DungeonSystem /// /// Gets a random dungeon room matching the specified area, whitelist and size. /// - public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, Random random, EntityWhitelist? whitelist = null) + public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, IRobustRandom random, EntityWhitelist? whitelist = null) { return GetRoomPrototype(random, whitelist, minSize: size, maxSize: size); } @@ -27,7 +28,7 @@ public sealed partial class DungeonSystem /// /// Gets a random dungeon room matching the specified area and whitelist and size range /// - public DungeonRoomPrototype? GetRoomPrototype(Random random, + public DungeonRoomPrototype? GetRoomPrototype(IRobustRandom random, EntityWhitelist? whitelist = null, Vector2i? minSize = null, Vector2i? maxSize = null) @@ -77,7 +78,7 @@ public void SpawnRoom( MapGridComponent grid, Vector2i origin, DungeonRoomPrototype room, - Random random, + IRobustRandom random, HashSet? reservedTiles, bool clearExisting = false, bool rotation = false) @@ -96,7 +97,7 @@ public void SpawnRoom( SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting); } - public Angle GetRoomRotation(DungeonRoomPrototype room, Random random) + public Angle GetRoomRotation(DungeonRoomPrototype room, IRobustRandom random) { var roomRotation = Angle.Zero; diff --git a/Content.Server/Procedural/RoomFillSystem.cs b/Content.Server/Procedural/RoomFillSystem.cs index f4ccab23673..96adb8dec30 100644 --- a/Content.Server/Procedural/RoomFillSystem.cs +++ b/Content.Server/Procedural/RoomFillSystem.cs @@ -1,4 +1,5 @@ using Robust.Shared.Map.Components; +using Robust.Shared.Random; namespace Content.Server.Procedural; @@ -6,6 +7,7 @@ public sealed class RoomFillSystem : EntitySystem { [Dependency] private readonly DungeonSystem _dungeon = default!; [Dependency] private readonly SharedMapSystem _maps = default!; + [Dependency] private readonly IRobustRandom _random = default!; public override void Initialize() { @@ -19,8 +21,7 @@ private void OnRoomFillMapInit(EntityUid uid, RoomFillComponent component, MapIn if (xform.GridUid != null) { - var random = new Random(); - var room = _dungeon.GetRoomPrototype(random, component.RoomWhitelist, component.MinSize, component.MaxSize); + var room = _dungeon.GetRoomPrototype(_random, component.RoomWhitelist, component.MinSize, component.MaxSize); if (room != null) { @@ -30,7 +31,7 @@ private void OnRoomFillMapInit(EntityUid uid, RoomFillComponent component, MapIn mapGrid, _maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates) - new Vector2i(room.Size.X/2,room.Size.Y/2), room, - random, + _random, null, clearExisting: component.ClearExisting, rotation: component.Rotation); diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs index cde69ed35ad..6c7435856c5 100644 --- a/Content.Server/Projectiles/ProjectileSystem.cs +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -4,6 +4,8 @@ using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Camera; using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Projectiles; @@ -60,58 +62,23 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St var damageRequired = _destructibleSystem.DestroyedAt(target); if (TryComp(target, out var damageableComponent)) { - damageRequired -= damageableComponent.TotalDamage; + damageRequired -= _damageableSystem.GetTotalDamage((target, damageableComponent)); damageRequired = FixedPoint2.Max(damageRequired, FixedPoint2.Zero); } - var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, damageable: damageableComponent, origin: component.Shooter); var deleted = Deleted(target); - if (modifiedDamage is not null && Exists(component.Shooter)) + if (_damageableSystem.TryChangeDamage((target, damageableComponent), ev.Damage, out var damage, component.IgnoreResistances, origin: component.Shooter) && Exists(component.Shooter)) { - if (modifiedDamage.AnyPositive() && !deleted) + if (!deleted) { _color.RaiseEffect(Color.Red, new List { target }, Filter.Pvs(target, entityManager: EntityManager)); } _adminLogger.Add(LogType.BulletHit, LogImpact.Medium, - $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {modifiedDamage.GetTotal():damage} damage"); - } - - // If penetration is to be considered, we need to do some checks to see if the projectile should stop. - if (modifiedDamage is not null && component.PenetrationThreshold != 0) - { - // If a damage type is required, stop the bullet if the hit entity doesn't have that type. - if (component.PenetrationDamageTypeRequirement != null) - { - var stopPenetration = false; - foreach (var requiredDamageType in component.PenetrationDamageTypeRequirement) - { - if (!modifiedDamage.DamageDict.Keys.Contains(requiredDamageType)) - { - stopPenetration = true; - break; - } - } - if (stopPenetration) - component.ProjectileSpent = true; - } + $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {damage:damage} damage"); - // If the object won't be destroyed, it "tanks" the penetration hit. - if (modifiedDamage.GetTotal() < damageRequired) - { - component.ProjectileSpent = true; - } - - if (!component.ProjectileSpent) - { - component.PenetrationAmount += damageRequired; - // The projectile has dealt enough damage to be spent. - if (component.PenetrationAmount >= component.PenetrationThreshold) - { - component.ProjectileSpent = true; - } - } + component.ProjectileSpent = !TryPenetrate((uid, component), damage, damageRequired); } else { @@ -120,7 +87,7 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St if (!deleted) { - _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound); + _guns.PlayImpactSound(target, damage, component.SoundHit, component.ForceSound); if (!args.OurBody.LinearVelocity.IsLengthZero()) _sharedCameraRecoil.KickCamera(target, args.OurBody.LinearVelocity.Normalized()); @@ -134,4 +101,41 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager)); } } + + private bool TryPenetrate(Entity projectile, DamageSpecifier damage, FixedPoint2 damageRequired) + { + // If penetration is to be considered, we need to do some checks to see if the projectile should stop. + if (projectile.Comp.PenetrationThreshold == 0) + return false; + + // If a damage type is required, stop the bullet if the hit entity doesn't have that type. + if (projectile.Comp.PenetrationDamageTypeRequirement != null) + { + foreach (var requiredDamageType in projectile.Comp.PenetrationDamageTypeRequirement) + { + if (damage.DamageDict.Keys.Contains(requiredDamageType)) + continue; + + return false; + } + } + + // If the object won't be destroyed, it "tanks" the penetration hit. + if (damage.GetTotal() < damageRequired) + { + return false; + } + + if (!projectile.Comp.ProjectileSpent) + { + projectile.Comp.PenetrationAmount += damageRequired; + // The projectile has dealt enough damage to be spent. + if (projectile.Comp.PenetrationAmount >= projectile.Comp.PenetrationThreshold) + { + return false; + } + } + + return true; + } } diff --git a/Content.Server/Radiation/Components/RadiationProtectionComponent.cs b/Content.Server/Radiation/Components/RadiationProtectionComponent.cs deleted file mode 100644 index 44b11af8347..00000000000 --- a/Content.Server/Radiation/Components/RadiationProtectionComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Prototypes; -using Content.Shared.Damage.Prototypes; - -namespace Content.Server.Radiation.Components; - -/// -/// Exists for use as a status effect. -/// Adds the DamageProtectionBuffComponent to the entity and adds the specified DamageModifierSet to its list of modifiers. -/// -[RegisterComponent] -public sealed partial class RadiationProtectionComponent : Component -{ - /// - /// The radiation damage modifier for entities with this component. - /// - [DataField("modifier")] - public ProtoId RadiationProtectionModifierSetId = "PotassiumIodide"; -} diff --git a/Content.Server/Radiation/Systems/GeigerSystem.cs b/Content.Server/Radiation/Systems/GeigerSystem.cs index 77df6c09e21..eb69e2ccd8e 100644 --- a/Content.Server/Radiation/Systems/GeigerSystem.cs +++ b/Content.Server/Radiation/Systems/GeigerSystem.cs @@ -47,7 +47,7 @@ private void OnEquipped(Entity geiger, ref GotEquippedEvent arg { if (geiger.Comp.AttachedToSuit) SetEnabled(geiger, true); - SetUser(geiger, args.Equipee); + SetUser(geiger, args.EquipTarget); } private void OnEquippedHand(Entity geiger, ref GotEquippedHandEvent args) diff --git a/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs b/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs deleted file mode 100644 index a32fa810c97..00000000000 --- a/Content.Server/Radiation/Systems/RadiationProtectionSystem.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Server.Radiation.Components; -using Content.Shared.Damage.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.Radiation.EntitySystems; - -public sealed class RadiationProtectionSystem : EntitySystem -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnShutdown); - } - - private void OnInit(EntityUid uid, RadiationProtectionComponent component, ComponentInit args) - { - if (!_prototypeManager.Resolve(component.RadiationProtectionModifierSetId, out var modifier)) - return; - var buffComp = EnsureComp(uid); - // add the damage modifier if it isn't in the dict yet - if (!buffComp.Modifiers.ContainsKey(component.RadiationProtectionModifierSetId)) - buffComp.Modifiers.Add(component.RadiationProtectionModifierSetId, modifier); - } - - private void OnShutdown(EntityUid uid, RadiationProtectionComponent component, ComponentShutdown args) - { - if (!TryComp(uid, out var buffComp)) - return; - // remove the damage modifier from the dict - buffComp.Modifiers.Remove(component.RadiationProtectionModifierSetId); - // if the dict is empty now, remove the buff component - if (buffComp.Modifiers.Count == 0) - RemComp(uid); - } -} diff --git a/Content.Server/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs index 34dbec6f864..3fbc7e0d28e 100644 --- a/Content.Server/Radiation/Systems/RadiationSystem.cs +++ b/Content.Server/Radiation/Systems/RadiationSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Radiation.Components; using Content.Shared.Radiation.Components; using Content.Shared.Radiation.Events; +using Content.Shared.Radiation.Systems; using Content.Shared.Stacks; using Robust.Shared.Configuration; using Robust.Shared.Map; @@ -8,7 +9,7 @@ namespace Content.Server.Radiation.Systems; -public sealed partial class RadiationSystem : EntitySystem +public sealed partial class RadiationSystem : SharedRadiationSystem { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; @@ -16,10 +17,9 @@ public sealed partial class RadiationSystem : EntitySystem [Dependency] private readonly SharedStackSystem _stack = default!; [Dependency] private readonly SharedMapSystem _maps = default!; - private EntityQuery _blockerQuery; - private EntityQuery _resistanceQuery; - private EntityQuery _gridQuery; - private EntityQuery _stackQuery; + [Dependency] private readonly EntityQuery _blockerQuery = default!; + [Dependency] private readonly EntityQuery _resistanceQuery = default!; + [Dependency] private readonly EntityQuery _gridQuery = default!; private float _accumulator; private List _sources = new(); @@ -29,11 +29,6 @@ public override void Initialize() base.Initialize(); SubscribeCvars(); InitRadBlocking(); - - _blockerQuery = GetEntityQuery(); - _resistanceQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); - _stackQuery = GetEntityQuery(); } public override void Update(float frameTime) @@ -49,9 +44,9 @@ public override void Update(float frameTime) _accumulator = 0f; } - public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time) + public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time, EntityUid? origin = null) { - var msg = new OnIrradiatedEvent(time, radsPerSecond, uid); + var msg = new OnIrradiatedEvent(time, radsPerSecond, origin); RaiseLocalEvent(uid, msg); } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 91abc9efd52..13b331c7780 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,5 +1,4 @@ -using Content.Server.Chat.Systems; -using Content.Server.Emp; +using Content.Shared.Chat; using Content.Shared.Inventory.Events; using Content.Shared.Radio; using Content.Shared.Radio.Components; @@ -21,8 +20,6 @@ public override void Initialize() SubscribeLocalEvent(OnKeysChanged); SubscribeLocalEvent(OnSpeak); - - SubscribeLocalEvent(OnEmpPulse); } private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args) @@ -61,7 +58,7 @@ protected override void OnGotEquipped(EntityUid uid, HeadsetComponent component, base.OnGotEquipped(uid, component, args); if (component.IsEquipped && component.Enabled) { - EnsureComp(args.Equipee).Headset = uid; + EnsureComp(args.EquipTarget).Headset = uid; UpdateRadioChannels(uid, component); } } @@ -69,9 +66,8 @@ protected override void OnGotEquipped(EntityUid uid, HeadsetComponent component, protected override void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args) { base.OnGotUnequipped(uid, component, args); - component.IsEquipped = false; RemComp(uid); - RemComp(args.Equipee); + RemComp(args.EquipTarget); } public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = null) @@ -82,6 +78,9 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = if (component.Enabled == value) return; + component.Enabled = value; + Dirty(uid, component); + if (!value) { RemCompDeferred(uid); @@ -113,13 +112,4 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad if (TryComp(parent, out ActorComponent? actor)) _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); } - - private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args) - { - if (component.Enabled) - { - args.Affected = true; - args.Disabled = true; - } - } } diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs index 02c9c64c6ef..812e0404b4e 100644 --- a/Content.Server/Radio/EntitySystems/JammerSystem.cs +++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs @@ -1,117 +1,33 @@ -using Content.Server.Power.EntitySystems; -using Content.Server.PowerCell; -using Content.Shared.DeviceNetwork.Components; -using Content.Shared.Interaction; -using Content.Shared.PowerCell.Components; using Content.Shared.Radio.EntitySystems; using Content.Shared.Radio.Components; -using Content.Shared.DeviceNetwork.Systems; namespace Content.Server.Radio.EntitySystems; public sealed class JammerSystem : SharedJammerSystem { - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnActivate); - SubscribeLocalEvent(OnPowerCellChanged); SubscribeLocalEvent(OnRadioSendAttempt); + SubscribeLocalEvent(OnRadioReceiveAttempt); } - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out var _, out var jam)) - { - - if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) - { - if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery)) - { - ChangeLEDState(uid, false); - RemComp(uid); - RemComp(uid); - } - else - { - var percentCharged = battery.CurrentCharge / battery.MaxCharge; - var chargeLevel = percentCharged switch - { - > 0.50f => RadioJammerChargeLevel.High, - < 0.15f => RadioJammerChargeLevel.Low, - _ => RadioJammerChargeLevel.Medium, - }; - ChangeChargeLevel(uid, chargeLevel); - } - - } - - } - } - - private void OnActivate(Entity ent, ref ActivateInWorldEvent args) - { - if (args.Handled || !args.Complex) - return; - - var activated = !HasComp(ent) && - _powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) && - battery.CurrentCharge > GetCurrentWattage(ent); - if (activated) - { - ChangeLEDState(ent.Owner, true); - EnsureComp(ent); - EnsureComp(ent, out var jammingComp); - _jammer.SetRange((ent, jammingComp), GetCurrentRange(ent)); - _jammer.AddJammableNetwork((ent, jammingComp), DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString()); - - // Add excluded frequencies using the system method - if (ent.Comp.FrequenciesExcluded != null) - { - foreach (var freq in ent.Comp.FrequenciesExcluded) - { - _jammer.AddExcludedFrequency((ent, jammingComp), (uint)freq); - } - } - } - else - { - ChangeLEDState(ent.Owner, false); - RemCompDeferred(ent); - RemCompDeferred(ent); - } - var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state"); - var message = Loc.GetString("radio-jammer-component-on-use", ("state", state)); - Popup.PopupEntity(message, args.User, args.User); - args.Handled = true; - } - - private void OnPowerCellChanged(Entity ent, ref PowerCellChangedEvent args) + private void OnRadioSendAttempt(ref RadioSendAttemptEvent args) { - if (args.Ejected) - { - ChangeLEDState(ent.Owner, false); - RemCompDeferred(ent); - } + if (ShouldCancel(args.RadioSource, args.Channel.Frequency)) + args.Cancelled = true; } - private void OnRadioSendAttempt(ref RadioSendAttemptEvent args) + private void OnRadioReceiveAttempt(ref RadioReceiveAttemptEvent args) { - if (ShouldCancelSend(args.RadioSource, args.Channel.Frequency)) - { + if (ShouldCancel(args.RadioReceiver, args.Channel.Frequency)) args.Cancelled = true; - } } - private bool ShouldCancelSend(EntityUid sourceUid, int frequency) + private bool ShouldCancel(EntityUid sourceUid, int frequency) { var source = Transform(sourceUid).Coordinates; var query = EntityQueryEnumerator(); @@ -119,7 +35,7 @@ private bool ShouldCancelSend(EntityUid sourceUid, int frequency) while (query.MoveNext(out var uid, out _, out var jam, out var transform)) { // Check if this jammer excludes the frequency - if (jam.FrequenciesExcluded != null && jam.FrequenciesExcluded.Contains(frequency)) + if (jam.FrequenciesExcluded.Contains(frequency)) continue; if (_transform.InRange(source, transform.Coordinates, GetCurrentRange((uid, jam)))) diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index b0f38c92fae..68067ae6f5e 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -3,16 +3,16 @@ using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.Power.EntitySystems; -using Content.Shared.Radio.Components; +using Content.Shared.Chat; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Power; using Content.Shared.Radio; +using Content.Shared.Radio.Components; +using Content.Shared.Radio.EntitySystems; using Content.Shared.Speech; using Content.Shared.Speech.Components; -using Content.Shared.Chat; using Robust.Shared.Prototypes; -using Content.Shared.Radio.EntitySystems; namespace Content.Server.Radio.EntitySystems; diff --git a/Content.Server/RatKing/RatKingSystem.cs b/Content.Server/RatKing/RatKingSystem.cs index eb3bef6c023..a7d6ecfd479 100644 --- a/Content.Server/RatKing/RatKingSystem.cs +++ b/Content.Server/RatKing/RatKingSystem.cs @@ -6,6 +6,7 @@ using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Shared.Atmos; +using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; @@ -13,7 +14,6 @@ using Content.Shared.Random.Helpers; using Content.Shared.RatKing; using Robust.Shared.Map; -using Robust.Shared.Random; namespace Content.Server.RatKing { diff --git a/Content.Server/Remotes/DoorRemoteSystem.cs b/Content.Server/Remotes/DoorRemoteSystem.cs index c3425f347ae..6d2219bae13 100644 --- a/Content.Server/Remotes/DoorRemoteSystem.cs +++ b/Content.Server/Remotes/DoorRemoteSystem.cs @@ -1,108 +1,5 @@ -using Content.Server.Administration.Logs; -using Content.Server.Doors.Systems; -using Content.Server.Power.EntitySystems; -using Content.Shared.Access.Components; -using Content.Shared.Database; -using Content.Shared.Doors.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; -using Content.Shared.Remotes.Components; using Content.Shared.Remotes.EntitySystems; -namespace Content.Shared.Remotes -{ - public sealed class DoorRemoteSystem : SharedDoorRemoteSystem - { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly AirlockSystem _airlock = default!; - [Dependency] private readonly DoorSystem _doorSystem = default!; - [Dependency] private readonly ExamineSystemShared _examine = default!; +namespace Content.Server.Remotes; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnBeforeInteract); - } - - private void OnBeforeInteract(Entity entity, ref BeforeRangedInteractEvent args) - { - bool isAirlock = TryComp(args.Target, out var airlockComp); - - if (args.Handled - || args.Target == null - || !TryComp(args.Target, out var doorComp) // If it isn't a door we don't use it - // Only able to control doors if they are within your vision and within your max range. - // Not affected by mobs or machines anymore. - || !_examine.InRangeUnOccluded(args.User, - args.Target.Value, - SharedInteractionSystem.MaxRaycastRange, - null)) - - { - return; - } - - args.Handled = true; - - if (!this.IsPowered(args.Target.Value, EntityManager)) - { - Popup.PopupEntity(Loc.GetString("door-remote-no-power"), args.User, args.User); - return; - } - - var accessTarget = args.Used; - // This covers the accesses the REMOTE has, and is not effected by the user's ID card. - if (entity.Comp.IncludeUserAccess) // Allows some door remotes to inherit the user's access. - { - accessTarget = args.User; - // This covers the accesses the USER has, which always includes the remote's access since holding a remote acts like holding an ID card. - } - - if (TryComp(args.Target, out var accessComponent) - && !_doorSystem.HasAccess(args.Target.Value, accessTarget, doorComp, accessComponent)) - { - if (isAirlock) - _doorSystem.Deny(args.Target.Value, doorComp, accessTarget); - Popup.PopupEntity(Loc.GetString("door-remote-denied"), args.User, args.User); - return; - } - - switch (entity.Comp.Mode) - { - case OperatingMode.OpenClose: - if (_doorSystem.TryToggleDoor(args.Target.Value, doorComp, accessTarget)) - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)}: {doorComp.State}"); - break; - case OperatingMode.ToggleBolts: - if (TryComp(args.Target, out var boltsComp)) - { - if (!boltsComp.BoltWireCut) - { - _doorSystem.SetBoltsDown((args.Target.Value, boltsComp), !boltsComp.BoltsDown, accessTarget); - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to {(boltsComp.BoltsDown ? "" : "un")}bolt it"); - } - } - - break; - case OperatingMode.ToggleEmergencyAccess: - if (airlockComp != null) - { - _airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess); - _adminLogger.Add(LogType.Action, - LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}"); - } - - break; - default: - throw new InvalidOperationException( - $"{nameof(DoorRemoteComponent)} had invalid mode {entity.Comp.Mode}"); - } - } - } -} +public sealed class DoorRemoteSystem : SharedDoorRemoteSystem; diff --git a/Content.Server/Research/Systems/ResearchSystem.Client.cs b/Content.Server/Research/Systems/ResearchSystem.Client.cs index 3f759824255..509d1d20de7 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Client.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Client.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Power.EntitySystems; using Content.Shared.Research.Components; +using Robust.Shared.Utility; namespace Content.Server.Research.Systems; @@ -62,10 +63,8 @@ private void OnClientRegistrationChanged(EntityUid uid, ResearchClientComponent private void OnClientMapInit(EntityUid uid, ResearchClientComponent component, MapInitEvent args) { - var allServers = GetServers(uid).ToList(); - - if (allServers.Count > 0) - RegisterClient(uid, allServers[0], component, allServers[0]); + if (GetServers(uid).FirstOrNull() is { } server) + RegisterClient(uid, server, component, server); } private void OnClientShutdown(EntityUid uid, ResearchClientComponent component, ComponentShutdown args) @@ -85,10 +84,8 @@ private void OnClientAnchorStateChanged(Entity ent, ref if (ent.Comp.Server is not null) return; - var allServers = GetServers(ent).ToList(); - - if (allServers.Count > 0) - RegisterClient(ent, allServers[0], ent, allServers[0]); + if (GetServers(ent).FirstOrNull() is { } server) + RegisterClient(ent, server, ent, server); } else { diff --git a/Content.Server/Research/Systems/ResearchSystem.cs b/Content.Server/Research/Systems/ResearchSystem.cs index 5cad4d51bad..c6a2a0125d5 100644 --- a/Content.Server/Research/Systems/ResearchSystem.cs +++ b/Content.Server/Research/Systems/ResearchSystem.cs @@ -23,8 +23,6 @@ public sealed partial class ResearchSystem : SharedResearchSystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly RadioSystem _radio = default!; - private static readonly HashSet> ClientLookup = new(); - public override void Initialize() { base.Initialize(); @@ -49,7 +47,7 @@ public bool TryGetServerById(EntityUid client, int id, [NotNullWhen(true)] out E serverUid = null; serverComponent = null; - var query = GetServers(client).ToList(); + var query = GetServers(client); foreach (var (uid, server) in query) { if (server.Id != id) @@ -67,15 +65,7 @@ public bool TryGetServerById(EntityUid client, int id, [NotNullWhen(true)] out E /// public string[] GetServerNames(EntityUid client) { - var allServers = GetServers(client).ToArray(); - var list = new string[allServers.Length]; - - for (var i = 0; i < allServers.Length; i++) - { - list[i] = allServers[i].Comp.ServerName; - } - - return list; + return GetServers(client).Select(x => x.Comp.ServerName).ToArray(); } /// @@ -84,27 +74,18 @@ public string[] GetServerNames(EntityUid client) /// public int[] GetServerIds(EntityUid client) { - var allServers = GetServers(client).ToArray(); - var list = new int[allServers.Length]; - - for (var i = 0; i < allServers.Length; i++) - { - list[i] = allServers[i].Comp.Id; - } - - return list; + return GetServers(client).Select(x => x.Comp.Id).ToArray(); } public HashSet> GetServers(EntityUid client) { - ClientLookup.Clear(); - var clientXform = Transform(client); if (clientXform.GridUid is not { } grid) - return ClientLookup; + return []; - _lookup.GetGridEntities(grid, ClientLookup); - return ClientLookup; + var set = new HashSet>(); + _lookup.GetGridEntities(grid, set); + return set; } public override void Update(float frameTime) diff --git a/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs b/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs index 93d5bb43b69..e474f77c7ab 100644 --- a/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs +++ b/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs @@ -1,6 +1,5 @@ using Robust.Shared.Audio; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Research.TechnologyDisk.Components; @@ -10,24 +9,24 @@ public sealed partial class DiskConsoleComponent : Component /// /// How much it costs to print a disk /// - [DataField("pricePerDisk"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public int PricePerDisk = 1000; /// /// The prototype of what's being printed /// - [DataField("diskPrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] - public string DiskPrototype = "TechnologyDisk"; + [DataField] + public EntProtoId DiskPrototype = "TechnologyDisk"; /// /// How long it takes to print /// - [DataField("printDuration"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan PrintDuration = TimeSpan.FromSeconds(1); /// /// The sound made when printing occurs /// - [DataField("printSound")] + [DataField] public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg"); } diff --git a/Content.Server/Resist/ResistLockerSystem.cs b/Content.Server/Resist/ResistLockerSystem.cs index 3698306b31e..df77dcb2727 100644 --- a/Content.Server/Resist/ResistLockerSystem.cs +++ b/Content.Server/Resist/ResistLockerSystem.cs @@ -57,9 +57,12 @@ private void AttemptResist(EntityUid user, EntityUid target, EntityStorageCompon NeedHand = false, //No hands 'cause we be kickin' }; + // Make sure the do after is able to start + if (!_doAfterSystem.TryStartDoAfter(doAfterEventArgs)) + return; + resistLockerComponent.IsResisting = true; _popupSystem.PopupEntity(Loc.GetString("resist-locker-component-start-resisting"), user, user, PopupType.Large); - _doAfterSystem.TryStartDoAfter(doAfterEventArgs); } private void OnDoAfter(EntityUid uid, ResistLockerComponent component, DoAfterEvent args) diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index 38c3480078e..6bf94327992 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -76,7 +76,7 @@ private void OnInteract(EntityUid uid, RevenantComponent component, UserActivate return; } - if (!HasComp(target) || !HasComp(target) || HasComp(target)) + if (!HasComp(target) || !HasComp(target) || HasComp(target)) return; args.Handled = true; @@ -213,7 +213,7 @@ private void OnHarvest(EntityUid uid, RevenantComponent component, HarvestEvent return; DamageSpecifier dspec = new(); dspec.DamageDict.Add("Cold", damage.Value); - _damage.TryChangeDamage(args.Args.Target, dspec, true, origin: uid); + _damage.ChangeDamage(args.Args.Target.Value, dspec, true, origin: uid); args.Handled = true; } @@ -344,7 +344,7 @@ private void OnMalfunctionAction(EntityUid uid, RevenantComponent component, Rev foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MalfunctionRadius)) { if (_whitelistSystem.IsWhitelistFail(component.MalfunctionWhitelist, ent) || - _whitelistSystem.IsBlacklistPass(component.MalfunctionBlacklist, ent)) + _whitelistSystem.IsWhitelistPass(component.MalfunctionBlacklist, ent)) continue; _emagSystem.TryEmagEffect(uid, uid, ent); diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index b89f10934d9..19b6a155300 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -1,10 +1,10 @@ using System.Numerics; using Content.Server.Actions; +using Content.Server.Atmos.EntitySystems; using Content.Server.GameTicking; -using Content.Server.Store.Components; using Content.Server.Store.Systems; using Content.Shared.Alert; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Eye; @@ -21,7 +21,6 @@ using Content.Shared.Stunnable; using Content.Shared.Tag; using Robust.Server.GameObjects; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Revenant.EntitySystems; @@ -29,8 +28,8 @@ namespace Content.Server.Revenant.EntitySystems; public sealed partial class RevenantSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly DamageableSystem _damage = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly GameTicker _ticker = default!; @@ -46,17 +45,12 @@ public sealed partial class RevenantSystem : EntitySystem [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly VisibilitySystem _visibility = default!; [Dependency] private readonly TurfSystem _turf = default!; - - private static readonly EntProtoId RevenantShopId = "ActionRevenantShop"; - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnShop); SubscribeLocalEvent(OnDamage); SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnStatusAdded); @@ -94,11 +88,6 @@ private void OnStartup(EntityUid uid, RevenantComponent component, ComponentStar _eye.RefreshVisibilityMask(uid); } - private void OnMapInit(EntityUid uid, RevenantComponent component, MapInitEvent args) - { - _action.AddAction(uid, ref component.Action, RevenantShopId); - } - private void OnStatusAdded(EntityUid uid, RevenantComponent component, StatusEffectAddedEvent args) { if (args.Key == "Stun") @@ -182,13 +171,6 @@ private bool TryUseAbility(EntityUid uid, RevenantComponent component, FixedPoin return true; } - private void OnShop(EntityUid uid, RevenantComponent component, RevenantShopActionEvent args) - { - if (!TryComp(uid, out var store)) - return; - _store.ToggleUi(uid, uid, store); - } - public void MakeVisible(bool visible) { var query = EntityQueryEnumerator(); @@ -208,6 +190,23 @@ public void MakeVisible(bool visible) } } + /// + /// Cools the area around the revenant. + /// The more essence they have, the colder it gets, up to a certain point. + /// + /// The revenant entity. + private void ChillArea(Entity ent) + { + var effectiveEssence = Math.Clamp(ent.Comp.Essence.Int(), 0, ent.Comp.ChillUpperBound.Float()); + // Parabolic curve based on essence, more essence = more delta q, flattening as upper bound is reached + var dQ = + 200 / ent.Comp.ChillScaling.Float() * MathF.Pow(effectiveEssence - ent.Comp.ChillUpperBound.Float(), 2) - + ent.Comp.ChillScaling.Float(); + + if (_atmosphere.GetContainingMixture(ent.Owner, true, true) is { } air) + _atmosphere.AddHeat(air, dQ); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -225,6 +224,8 @@ public override void Update(float frameTime) { ChangeEssenceAmount(uid, rev.EssencePerSecond, rev, regenCap: true); } + + ChillArea((uid, rev)); } } } diff --git a/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs b/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs new file mode 100644 index 00000000000..61acf26d5b7 --- /dev/null +++ b/Content.Server/Revolutionary/Components/RevolutionaryConverterComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.Revolutionary.Components; + +/// +/// This is a marker component that indicates that a flash can be used to convert someone into a revolutionary. +/// Viva la revolution! +/// +[RegisterComponent] +public sealed partial class RevolutionaryConverterComponent : Component; diff --git a/Content.Server/Roles/RoleSystem.cs b/Content.Server/Roles/RoleSystem.cs index 71fb8a9cb57..abf9dd50ecb 100644 --- a/Content.Server/Roles/RoleSystem.cs +++ b/Content.Server/Roles/RoleSystem.cs @@ -15,7 +15,7 @@ public sealed class RoleSystem : SharedRoleSystem { if (mindId == null) { - Log.Error($"MingGetBriefing failed for mind {mindId}"); + Log.Error($"MindGetBriefing failed for mind {mindId}"); return null; } @@ -23,7 +23,7 @@ public sealed class RoleSystem : SharedRoleSystem if (mindComp is null) { - Log.Error($"MingGetBriefing failed for mind {mindId}"); + Log.Error($"MindGetBriefing failed for mind {mindId}"); return null; } diff --git a/Content.Server/Rootable/RootableSystem.cs b/Content.Server/Rootable/RootableSystem.cs deleted file mode 100644 index cd18315bd05..00000000000 --- a/Content.Server/Rootable/RootableSystem.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Fluids.Components; -using Content.Shared.Rootable; -using Robust.Shared.Timing; - -namespace Content.Server.Rootable; - -// TODO: Move all of this to shared -/// -/// Adds an action to toggle rooting to the ground, primarily for the Diona species. -/// -public sealed class RootableSystem : SharedRootableSystem -{ - [Dependency] private readonly ISharedAdminLogManager _logger = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - [Dependency] private readonly ReactiveSystem _reactive = default!; - [Dependency] private readonly BloodstreamSystem _blood = default!; - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - var curTime = _timing.CurTime; - while (query.MoveNext(out var uid, out var rooted, out var bloodstream)) - { - if (!rooted.Rooted || rooted.PuddleEntity == null || curTime < rooted.NextUpdate || !PuddleQuery.TryComp(rooted.PuddleEntity, out var puddleComp)) - continue; - - rooted.NextUpdate += rooted.TransferFrequency; - - PuddleReact((uid, rooted, bloodstream), (rooted.PuddleEntity.Value, puddleComp!)); - } - } - - /// - /// Determines if the puddle is set up properly and if so, moves on to reacting. - /// - private void PuddleReact(Entity entity, Entity puddleEntity) - { - if (!_solutionContainer.ResolveSolution(puddleEntity.Owner, puddleEntity.Comp.SolutionName, ref puddleEntity.Comp.Solution, out var solution) || - solution.Contents.Count == 0) - { - return; - } - - ReactWithEntity(entity, puddleEntity, solution); - } - - /// - /// Attempt to transfer an amount of the solution to the entity's bloodstream. - /// - private void ReactWithEntity(Entity entity, Entity puddleEntity, Solution solution) - { - if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp2.ChemicalSolutionName, ref entity.Comp2.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0) - return; - - var availableTransfer = FixedPoint2.Min(solution.Volume, entity.Comp1.TransferRate); - var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume); - var transferSolution = _solutionContainer.SplitSolution(puddleEntity.Comp.Solution!.Value, transferAmount); - - _reactive.DoEntityReaction(entity, transferSolution, ReactionMethod.Ingestion); - - if (_blood.TryAddToChemicals((entity, entity.Comp2), transferSolution)) - { - // Log solution addition by puddle - _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} absorbed puddle {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}"); - } - } -} diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index c82d2db9895..1dcf95b9b95 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -57,6 +57,11 @@ public sealed class RoundEndSystem : EntitySystem public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart; public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime; + /// + /// If the shuttle can't be recalled. if set to true, the station wont be able to recall + /// + public bool CantRecall = false; + public TimeSpan AutoCallStartTime; private bool _autoCalledBefore = false; @@ -88,6 +93,8 @@ private void Reset() _cooldownTokenSource = null; } + CantRecall = false; + LastCountdownStart = null; ExpectedCountdownEnd = null; SetAutoCallTime(); @@ -119,7 +126,7 @@ private void Reset() public bool CanCallOrRecall() { - return _cooldownTokenSource == null; + return _cooldownTokenSource == null && !CantRecall; } public bool IsRoundEndRequested() @@ -127,7 +134,16 @@ public bool IsRoundEndRequested() return _countdownTokenSource != null; } - public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement") + /// + /// Starts the process of ending the round by calling evac + /// + /// who called evac + /// machine used to call evac + /// + /// text in the announcement of shuttle calling + /// name in the announcement of shuttle calling + /// if the station shouldn't be able to recall the shuttle + public void RequestRoundEnd(EntityUid? requester = null, EntityUid? machine = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement", bool cantRecall = false) { var duration = DefaultCountdownDuration; @@ -142,10 +158,20 @@ public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = tr } } - RequestRoundEnd(duration, requester, checkCooldown, text, name); + RequestRoundEnd(duration, requester, machine, checkCooldown, text, name, cantRecall); } - public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement") + /// + /// Starts the process of ending the round by calling evac + /// + /// time for evac to arrive + /// who called evac + /// machine used to call evac + /// + /// text in the announcement of shuttle calling + /// name in the announcement of shuttle calling + /// if the station shouldn't be able to recall the shuttle + public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, EntityUid? machine = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement", bool cantRecall = false) { if (_gameTicker.RunLevel != GameRunLevel.InRound) return; @@ -157,15 +183,13 @@ public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, return; _countdownTokenSource = new(); + CantRecall = cantRecall; + var what = machine != null ? $" with {ToPrettyString(machine.Value):entity} " : ""; if (requester != null) - { - _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):user}"); - } + _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):player}{what}"); else - { - _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called"); - } + _adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called{what}"); // I originally had these set up here but somehow time gets passed as 0 to Loc so IDEK. int time; @@ -217,23 +241,25 @@ public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, } } - public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true) + public void CancelRoundEndCountdown(EntityUid? requester = null, EntityUid? machine = null, bool forceRecall = false) { - if (_gameTicker.RunLevel != GameRunLevel.InRound) return; - if (checkCooldown && _cooldownTokenSource != null) return; + if (_gameTicker.RunLevel != GameRunLevel.InRound) + return; + + if (!forceRecall && (CantRecall || _cooldownTokenSource != null)) + return; + + if (_countdownTokenSource == null) + return; - if (_countdownTokenSource == null) return; _countdownTokenSource.Cancel(); _countdownTokenSource = null; + var what = machine != null ? $" with {ToPrettyString(machine.Value):entity} " : ""; if (requester != null) - { - _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):user}"); - } + _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):player}{what}"); else - { - _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled"); - } + _adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled{what}"); _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"), Loc.GetString("round-end-system-shuttle-sender-announcement"), false, colorOverride: Color.Gold); @@ -290,7 +316,7 @@ public void EndRound(TimeSpan? countdownTime = null) Loc.GetString( "round-end-system-round-restart-eta-announcement", ("time", time), - ("units", Loc.GetString(unitsLocString)))); + ("units", Loc.GetString(unitsLocString, ("amount", time))))); Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token); } @@ -323,8 +349,8 @@ public void DoRoundEndBehavior(RoundEndBehavior behavior, } else { - RequestRoundEnd(time, null, false, textCall, - Loc.GetString(sender)); + RequestRoundEnd(time, checkCooldown: false, text: textCall, + name: Loc.GetString(sender)); } break; } @@ -360,7 +386,7 @@ public override void Update(float frameTime) { if (!_shuttle.EmergencyShuttleArrived && ExpectedCountdownEnd is null) { - RequestRoundEnd(null, false, "round-end-system-shuttle-auto-called-announcement"); + RequestRoundEnd(checkCooldown: false, text: "round-end-system-shuttle-auto-called-announcement"); _autoCalledBefore = true; } diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index adc97ac8242..5e3eda8ff16 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -8,6 +8,7 @@ using Robust.Shared.CPUJob.JobQueues; using Robust.Shared.CPUJob.JobQueues.Queues; using Robust.Shared.GameStates; +using Robust.Shared.Random; namespace Content.Server.Salvage; @@ -17,7 +18,9 @@ public sealed partial class SalvageSystem * Handles setup / teardown of salvage expeditions. */ - private const int MissionLimit = 3; + private const int MissionLimit = 5; // IMPERIAL IMPROVED SALVAGE + + private readonly string[] MissionsDiff = ["Doomed", "Cakewalk", "Hard", "Infestation", "Suicidal", "Moderate"]; // Imperial Improved Salvage private readonly JobQueue _salvageQueue = new(); private readonly List<(SpawnSalvageMissionJob Job, CancellationTokenSource CancelToken)> _salvageJobs = new(); @@ -148,7 +151,7 @@ private void GenerateMissions(SalvageExpeditionDataComponent component) { Index = component.NextIndex, Seed = _random.Next(), - Difficulty = "Moderate", + Difficulty = _random.Pick(MissionsDiff), // Imperial Improved Salvage }; component.Missions[component.NextIndex++] = mission; diff --git a/Content.Server/Salvage/SalvageSystem.Magnet.cs b/Content.Server/Salvage/SalvageSystem.Magnet.cs index ee5cd792c69..9133f1239b6 100644 --- a/Content.Server/Salvage/SalvageSystem.Magnet.cs +++ b/Content.Server/Salvage/SalvageSystem.Magnet.cs @@ -9,6 +9,7 @@ using Robust.Shared.Exceptions; using Robust.Shared.Map; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Server.Salvage; diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index ceee9c37844..e838ceb08ec 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -40,7 +40,7 @@ private void OnConsoleFTLAttempt(ref ConsoleFTLAttemptEvent ev) } // TODO: This is terrible but need bluespace harnesses or something. - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out var mobState, out var mobXform)) { diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index f212fa4a833..841a0a28113 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -1,19 +1,16 @@ -using System.Collections; using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; -using Content.Server.Atmos; -using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Robust.Shared.CPUJob.JobQueues; using Content.Server.Ghost.Roles.Components; using Content.Server.Parallax; using Content.Server.Procedural; using Content.Server.Salvage.Expeditions; +using Content.Server.Shuttles.Components; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; using Content.Shared.Construction.EntitySystems; -using Content.Shared.Dataset; using Content.Shared.Gravity; using Content.Shared.Parallax.Biomes; using Content.Shared.Physics; @@ -24,15 +21,13 @@ using Content.Shared.Salvage.Expeditions; using Content.Shared.Salvage.Expeditions.Modifiers; using Content.Shared.Shuttles.Components; -using Content.Shared.Storage; using Robust.Shared.Collections; -using Robust.Shared.Map; +using Robust.Shared.CPUJob.JobQueues; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Content.Server.Shuttles.Components; namespace Content.Server.Salvage; @@ -112,7 +107,7 @@ protected override async Task Process() // Setup mission configs // As we go through the config the rating will deplete so we'll go for most important to least important. - var difficultyId = "Moderate"; + var difficultyId = _missionParams.Difficulty; // Imperial Improved Salvage var difficultyProto = _prototypeManager.Index(difficultyId); var mission = _entManager.System() diff --git a/Content.Server/Sandbox/SandboxSystem.cs b/Content.Server/Sandbox/SandboxSystem.cs index 30fe4e0fe95..430e87c019c 100644 --- a/Content.Server/Sandbox/SandboxSystem.cs +++ b/Content.Server/Sandbox/SandboxSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Server.GameTicking; using Content.Shared.Access; using Content.Shared.Access.Components; @@ -7,6 +6,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; +using Content.Shared.Overlays; using Content.Shared.PDA; using Content.Shared.Sandbox; using Robust.Server.Console; @@ -15,6 +15,7 @@ using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using System.Linq; namespace Content.Server.Sandbox { @@ -50,6 +51,7 @@ public override void Initialize() SubscribeNetworkEvent(SandboxGiveAccessReceived); SubscribeNetworkEvent(SandboxGiveAghostReceived); SubscribeNetworkEvent(SandboxSuicideReceived); + SubscribeNetworkEvent(UpdateSandboxThermalVision); SubscribeLocalEvent(GameTickerOnOnRunLevelChanged); @@ -193,5 +195,18 @@ private void UpdateSandboxStatusForAll() { RaiseNetworkEvent(new MsgSandboxStatus { SandboxAllowed = IsSandboxEnabled }); } + + private void UpdateSandboxThermalVision(MsgSandboxThermalVision message, EntitySessionEventArgs args) + { + if (!IsSandboxEnabled) + return; + + var ent = args.SenderSession.AttachedEntity; + if (ent == null) return; + if (HasComp(ent)) + RemComp(ent.Value); + else + EnsureComp(ent.Value); + } } } diff --git a/Content.Server/SensorMonitoring/BatterySensorComponent.cs b/Content.Server/SensorMonitoring/BatterySensorComponent.cs index 2f0c97e6ec6..a5a48ddc911 100644 --- a/Content.Server/SensorMonitoring/BatterySensorComponent.cs +++ b/Content.Server/SensorMonitoring/BatterySensorComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Power.Components; +using Content.Shared.Power.Components; namespace Content.Server.SensorMonitoring; diff --git a/Content.Server/SensorMonitoring/BatterySensorSystem.cs b/Content.Server/SensorMonitoring/BatterySensorSystem.cs index 5047cd1d297..2c7666ba968 100644 --- a/Content.Server/SensorMonitoring/BatterySensorSystem.cs +++ b/Content.Server/SensorMonitoring/BatterySensorSystem.cs @@ -1,8 +1,9 @@ -using Content.Server.DeviceNetwork; -using Content.Server.DeviceNetwork.Systems; +using Content.Server.DeviceNetwork.Systems; using Content.Server.Power.Components; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Events; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; namespace Content.Server.SensorMonitoring; @@ -11,6 +12,7 @@ public sealed class BatterySensorSystem : EntitySystem public const string DeviceNetworkCommandSyncData = "bat_sync_data"; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; public override void Initialize() { @@ -26,13 +28,14 @@ private void PacketReceived(EntityUid uid, BatterySensorComponent component, Dev { case DeviceNetworkCommandSyncData: var battery = Comp(uid); + var currentCharge = _battery.GetCharge((uid, battery)); var netBattery = Comp(uid); var payload = new NetworkPayload { [DeviceNetworkConstants.Command] = DeviceNetworkCommandSyncData, [DeviceNetworkCommandSyncData] = new BatterySensorData( - battery.CurrentCharge, + currentCharge, battery.MaxCharge, netBattery.CurrentReceiving, netBattery.MaxChargeRate, diff --git a/Content.Server/SensorMonitoring/SensorMonitoringConsoleSystem.cs b/Content.Server/SensorMonitoring/SensorMonitoringConsoleSystem.cs index ebe8f304bab..99779e2a0ee 100644 --- a/Content.Server/SensorMonitoring/SensorMonitoringConsoleSystem.cs +++ b/Content.Server/SensorMonitoring/SensorMonitoringConsoleSystem.cs @@ -1,9 +1,8 @@ using Content.Server.Atmos.Monitor.Components; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Atmos.Piping.Components; -using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.Power.Generation.Teg; +using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Atmos.Piping.Unary.Components; diff --git a/Content.Server/Shuttles/Components/IFFConsoleComponent.cs b/Content.Server/Shuttles/Components/IFFConsoleComponent.cs index 562bec51c26..c2cf913c2c4 100644 --- a/Content.Server/Shuttles/Components/IFFConsoleComponent.cs +++ b/Content.Server/Shuttles/Components/IFFConsoleComponent.cs @@ -11,4 +11,10 @@ public sealed partial class IFFConsoleComponent : Component /// [ViewVariables(VVAccess.ReadWrite), DataField("allowedFlags")] public IFFFlags AllowedFlags = IFFFlags.HideLabel; + + /// + /// If true, automatically applies all supported IFF flags to the console's grid on MapInitEvent. + /// + [DataField] + public bool HideOnInit = false; } diff --git a/Content.Server/Shuttles/Components/ShuttleComponent.cs b/Content.Server/Shuttles/Components/ShuttleComponent.cs index f677a7c82cb..cfd86874dae 100644 --- a/Content.Server/Shuttles/Components/ShuttleComponent.cs +++ b/Content.Server/Shuttles/Components/ShuttleComponent.cs @@ -68,5 +68,12 @@ public sealed partial class ShuttleComponent : Component /// [DataField] public float DampingModifier; + + /// + /// Optional override for the FTL cooldown for this shuttle. + /// If not null, then the value will be used instead of the shuttle.cooldown CCVar. + /// + [DataField] + public TimeSpan? FTLCooldownOverride = null; } } diff --git a/Content.Server/Shuttles/Components/StationEmergencyShuttleComponent.cs b/Content.Server/Shuttles/Components/StationEmergencyShuttleComponent.cs index d4fcfb81c66..cfb667e1f0a 100644 --- a/Content.Server/Shuttles/Components/StationEmergencyShuttleComponent.cs +++ b/Content.Server/Shuttles/Components/StationEmergencyShuttleComponent.cs @@ -26,35 +26,42 @@ public sealed partial class StationEmergencyShuttleComponent : Component /// /// The announcement made when the shuttle has successfully docked with the station. /// + [DataField] public LocId DockedAnnouncement = "emergency-shuttle-docked"; /// /// Sound played when the shuttle has successfully docked with the station. /// + [DataField] public SoundSpecifier DockedAudio = new SoundPathSpecifier("/Audio/Announcements/shuttle_dock.ogg"); /// /// The announcement made when the shuttle is unable to dock and instead parks in nearby space. /// + [DataField] public LocId NearbyAnnouncement = "emergency-shuttle-nearby"; /// /// Sound played when the shuttle is unable to dock and instead parks in nearby space. /// + [DataField] public SoundSpecifier NearbyAudio = new SoundPathSpecifier("/Audio/Misc/notice1.ogg"); /// /// The announcement made when the shuttle is unable to find a station. /// + [DataField] public LocId FailureAnnouncement = "emergency-shuttle-good-luck"; /// /// Sound played when the shuttle is unable to find a station. /// + [DataField] public SoundSpecifier FailureAudio = new SoundPathSpecifier("/Audio/Misc/notice1.ogg"); /// /// Text appended to the docking announcement if the launch time has been extended. /// + [DataField] public LocId LaunchExtendedMessage = "emergency-shuttle-extended"; } diff --git a/Content.Server/Shuttles/Components/ThrusterComponent.cs b/Content.Server/Shuttles/Components/ThrusterComponent.cs index 20f020743d1..2a68b4f2909 100644 --- a/Content.Server/Shuttles/Components/ThrusterComponent.cs +++ b/Content.Server/Shuttles/Components/ThrusterComponent.cs @@ -10,6 +10,12 @@ namespace Content.Server.Shuttles.Components [Access(typeof(ThrusterSystem))] public sealed partial class ThrusterComponent : Component { + /// + /// If the thruster can be toggled on or off via interaction + /// + [DataField] + public bool CanToggle = true; + /// /// Whether the thruster has been force to be enabled / disabled (e.g. VV, interaction, etc.) /// diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs index 20976ca0195..af0e1240adb 100644 --- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs +++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Numerics; using Content.Server.Administration; +using Content.Server.Antag; using Content.Server.Chat.Managers; using Content.Server.DeviceNetwork.Systems; using Content.Server.GameTicking; @@ -61,6 +62,7 @@ public sealed class ArrivalsSystem : EntitySystem [Dependency] private readonly ShuttleSystem _shuttles = default!; [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; private EntityQuery _pendingQuery; private EntityQuery _blacklistQuery; @@ -273,6 +275,9 @@ private void OnArrivalsFTL(EntityUid shuttleUid, ArrivalsShuttleComponent compon if (ArrivalsGodmode) RemCompDeferred(pUid); + + if (_actor.TryGetSession(pUid, out var session) && session is not null) + _antag.TryMakeLateJoinAntag(session); } } @@ -443,6 +448,26 @@ private bool TryGetArrivals(out EntityUid uid) return false; } + /// + /// Check if an entity is on the arrivals grid. + /// + /// Entity to check. + /// True if the entity is on the arrivals grid. Returns false if not on arrivals, or there is no arrivals grid. + public bool IsOnArrivals(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (!TryGetArrivals(out var arrivals)) + return false; + + var arrivalsGridUid = Transform(arrivals).GridUid; + if (!arrivalsGridUid.HasValue) + return false; + + return entity.Comp.GridUid == Transform(arrivals).GridUid; + } + public TimeSpan? NextShuttleArrival() { var query = EntityQueryEnumerator(); diff --git a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs index 5e3029333e8..57ebf13e730 100644 --- a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs +++ b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs @@ -203,7 +203,8 @@ private List GetDockingConfigs( var spawnPosition = new EntityCoordinates(targetGridXform.MapUid!.Value, _transform.ToMapCoordinates(gridPosition).Position); // TODO: use tight bounds - var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetAngle, spawnPosition.Position); + var targetWorldAngle = (targetGridAngle + targetAngle).Reduced(); + var dockedBounds = new Box2Rotated(shuttleAABB.Translated(spawnPosition.Position), targetWorldAngle, spawnPosition.Position); // Check if there's no intersecting grids (AKA oh god it's docking at cargo). grids.Clear(); diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs index c8cdf17d30c..3b28d007f6d 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs @@ -16,6 +16,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Timer = Robust.Shared.Timing.Timer; +using Robust.Shared.Random; namespace Content.Server.Shuttles.Systems; @@ -260,8 +261,7 @@ private void OnEmergencyRepeal(EntityUid uid, EmergencyShuttleConsoleComponent c return; } - // TODO: This is fucking bad - if (!component.AuthorizedEntities.Remove(MetaData(idCard).EntityName)) + if (!component.AuthorizedEntities.Remove(idCard.Owner)) return; _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch REPEAL by {args.Actor:user}"); @@ -281,10 +281,13 @@ private void OnEmergencyAuthorize(EntityUid uid, EmergencyShuttleConsoleComponen return; } - // TODO: This is fucking bad - if (!component.AuthorizedEntities.Add(MetaData(idCard).EntityName)) + var idCardUid = idCard.Owner; + + if (component.AuthorizedEntities.ContainsKey(idCardUid)) return; + component.AuthorizedEntities[idCardUid] = MetaData(idCard).EntityName; + _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle early launch AUTH by {args.Actor:user}"); var remaining = component.AuthorizationsRequired - component.AuthorizedEntities.Count; @@ -327,7 +330,7 @@ private void UpdateConsoleState(EntityUid uid, EmergencyShuttleConsoleComponent { var auths = new List(); - foreach (var auth in component.AuthorizedEntities) + foreach (var auth in component.AuthorizedEntities.Values) { auths.Add(auth); } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index a63359077f3..54bfdbad140 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -4,11 +4,9 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.Station.Events; -using Content.Shared.Body.Components; +using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Database; -using Content.Shared.Ghost; -using Content.Shared.Maps; using Content.Shared.Parallax; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; @@ -26,6 +24,7 @@ using Robust.Shared.Player; using Robust.Shared.Utility; using FTLMapComponent = Content.Shared.Shuttles.Components.FTLMapComponent; +using Robust.Shared.Random; namespace Content.Server.Shuttles.Systems; @@ -48,7 +47,8 @@ public sealed partial class ShuttleSystem public float DefaultStartupTime; public float DefaultTravelTime; public float DefaultArrivalTime; - private float FTLCooldown; + private TimeSpan FTLCooldown; + private TimeSpan ArrivalsFTLCooldown; public float FTLMassLimit; private TimeSpan _hyperspaceKnockdownTime = TimeSpan.FromSeconds(5); @@ -87,7 +87,8 @@ private void InitializeFTL() _cfg.OnValueChanged(CCVars.FTLStartupTime, time => DefaultStartupTime = time, true); _cfg.OnValueChanged(CCVars.FTLTravelTime, time => DefaultTravelTime = time, true); _cfg.OnValueChanged(CCVars.FTLArrivalTime, time => DefaultArrivalTime = time, true); - _cfg.OnValueChanged(CCVars.FTLCooldown, time => FTLCooldown = time, true); + _cfg.OnValueChanged(CCVars.FTLCooldown, time => FTLCooldown = TimeSpan.FromSeconds(time), true); + _cfg.OnValueChanged(CCVars.ArrivalsFTLCooldown, time => ArrivalsFTLCooldown = TimeSpan.FromSeconds(time), true); _cfg.OnValueChanged(CCVars.FTLMassLimit, time => FTLMassLimit = time, true); _cfg.OnValueChanged(CCVars.HyperspaceKnockdownTime, time => _hyperspaceKnockdownTime = TimeSpan.FromSeconds(time), true); } @@ -542,7 +543,10 @@ private void UpdateFTLArriving(Entity entity) } comp.State = FTLState.Cooldown; - comp.StateTime = StartEndTime.FromCurTime(_gameTiming, FTLCooldown); + var cooldown = entity.Comp2.FTLCooldownOverride ?? (HasComp(uid) + ? ArrivalsFTLCooldown + : FTLCooldown); + comp.StateTime = StartEndTime.FromCurTime(_gameTiming, cooldown); _console.RefreshShuttleConsoles(uid); _mapSystem.SetPaused(mapId, false); Smimsh(uid, xform: xform); @@ -997,7 +1001,7 @@ private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridCom { _logger.Add(LogType.Gib, LogImpact.Extreme, $"{ToPrettyString(ent):player} got gibbed by the shuttle" + $" {ToPrettyString(uid)} arriving from FTL at {xform.Coordinates:coordinates}"); - var gibs = _bobby.GibBody(ent, body: mob); + var gibs = _gibbing.Gib(ent); _immuneEnts.UnionWith(gibs); continue; } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs index 5e746fd4953..738fb765e14 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.IFF.cs @@ -12,7 +12,7 @@ private void InitializeIFF() { SubscribeLocalEvent(OnIFFConsoleAnchor); SubscribeLocalEvent(OnIFFShow); - SubscribeLocalEvent(OnIFFShowVessel); + SubscribeLocalEvent(OnInitIFFConsole); SubscribeLocalEvent(OnGridSplit); } @@ -37,37 +37,32 @@ private void OnGridSplit(ref GridSplitEvent ev) private void OnIFFShow(EntityUid uid, IFFConsoleComponent component, IFFShowIFFMessage args) { - if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null || - (component.AllowedFlags & IFFFlags.HideLabel) == 0x0) + if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null) { return; } if (!args.Show) { - AddIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel); + AddAllSupportedIFFFlags(xform, component); } else { RemoveIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel); + RemoveIFFFlag(xform.GridUid.Value, IFFFlags.Hide); } } - private void OnIFFShowVessel(EntityUid uid, IFFConsoleComponent component, IFFShowVesselMessage args) + private void OnInitIFFConsole(EntityUid uid, IFFConsoleComponent component, MapInitEvent args) { - if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null || - (component.AllowedFlags & IFFFlags.Hide) == 0x0) + if (!TryComp(uid, out TransformComponent? xform) || xform.GridUid == null) { return; } - if (!args.Show) + if (component.HideOnInit) { - AddIFFFlag(xform.GridUid.Value, IFFFlags.Hide); - } - else - { - RemoveIFFFlag(xform.GridUid.Value, IFFFlags.Hide); + AddAllSupportedIFFFlags(xform, component); } } @@ -111,4 +106,25 @@ protected override void UpdateIFFInterfaces(EntityUid gridUid, IFFComponent comp }); } } + + // Made this method to avoid copy and pasting. + /// + /// Adds all IFF flags that are allowed by AllowedFlags to the grid. + /// + private void AddAllSupportedIFFFlags(TransformComponent xform, IFFConsoleComponent component) + { + if (xform.GridUid == null) + { + return; + } + + if ((component.AllowedFlags & IFFFlags.HideLabel) != 0x0) + { + AddIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel); + } + if ((component.AllowedFlags & IFFFlags.Hide) != 0x0) + { + AddIFFFlag(xform.GridUid.Value, IFFFlags.Hide); + } + } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs index b5adeb04dbd..cbc48b3ccad 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs @@ -20,6 +20,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Numerics; +using Content.Shared.Damage.Components; namespace Content.Server.Shuttles.Systems; @@ -230,35 +231,58 @@ private void ThrowEntitiesOnGrid(EntityUid gridUid, TransformComponent xform, Ve var knockdownTime = TimeSpan.FromSeconds(5); var minsq = _minThrowVelocity * _minThrowVelocity; - // iterate all entities on the grid - // TODO: only iterate non-static entities - var childEnumerator = xform.ChildEnumerator; - while (childEnumerator.MoveNext(out var uid)) - { - // don't throw static bodies - if (!_physicsQuery.TryGetComponent(uid, out var physics) || (physics.BodyType & BodyType.Static) != 0) - continue; + // iterate all dynamic entities on the grid + if (!TryComp(gridUid, out var lookup) || !_gridQuery.TryComp(gridUid, out var gridComp)) + return; + + var gridBox = gridComp.LocalAABB; + List> list = new(); + HashSet processed = new(); + var state = (list, processed, _physicsQuery); + lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true); + lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true); + + foreach (var ent in list) + { // don't throw if buckled - if (_buckle.IsBuckled(uid, _buckleQuery.CompOrNull(uid))) + if (_buckle.IsBuckled(ent, _buckleQuery.CompOrNull(ent))) continue; // don't throw them if they have magboots - if (movedByPressureQuery.TryComp(uid, out var moved) && !moved.Enabled) + if (movedByPressureQuery.TryComp(ent, out var moved) && !moved.Enabled) continue; if (direction.LengthSquared() > minsq) { - _stuns.TryCrawling(uid, knockdownTime); - _throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false); + _stuns.TryCrawling(ent.Owner, knockdownTime); + _throwing.TryThrow(ent, direction, ent.Comp, Transform(ent), direction.Length(), playSound: false); } else { - _physics.ApplyLinearImpulse(uid, direction * physics.Mass, body: physics); + _physics.ApplyLinearImpulse(ent, direction * ent.Comp.Mass, body: ent.Comp); } } } + private static bool GridQueryCallback( + ref (List> List, HashSet Processed, EntityQuery PhysicsQuery) state, + in EntityUid uid) + { + if (state.Processed.Add(uid) && state.PhysicsQuery.TryComp(uid, out var body)) + state.List.Add((uid, body)); + + return true; + } + + private static bool GridQueryCallback( + ref (List> List, HashSet Processed, EntityQuery PhysicsQuery) state, + in FixtureProxy proxy) + { + var owner = proxy.Entity; + return GridQueryCallback(ref state, in owner); + } + /// /// Structure to hold impact tile processing data for batch processing /// @@ -372,7 +396,7 @@ private void ProcessTileBatch( damageSpec.DamageDict["Blunt"] = scaledDamage; damageSpec.DamageDict["Structural"] = scaledDamage * _structuralDamage; - _damageSys.TryChangeDamage(localEnt, damageSpec, damageable: damageable); + _damageSys.ChangeDamage((localEnt, damageable), damageSpec); } // might've been destroyed if (TerminatingOrDeleted(localEnt) || EntityManager.IsQueuedForDeletion(localEnt)) @@ -390,7 +414,7 @@ private void ProcessTileBatch( else { var direction = throwDirection * tileData.DistanceFactor; - _throwing.TryThrow(localEnt, direction, physics, localEnt.Comp, _projQuery, direction.Length(), playSound: false); + _throwing.TryThrow(localEnt, direction, physics, localEnt.Comp, direction.Length(), playSound: false); } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index cea7fbfc091..43607ea1ea2 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Administration.Logs; -using Content.Server.Body.Systems; using Content.Server.Buckle.Systems; using Content.Server.Parallax; using Content.Server.Procedural; @@ -8,7 +7,8 @@ using Content.Server.Station.Systems; using Content.Server.Stunnable; using Content.Shared.Buckle.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Gibbing; using Content.Shared.Light.Components; using Content.Shared.Movement.Events; using Content.Shared.Salvage; @@ -42,7 +42,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly BiomeSystem _biomes = default!; - [Dependency] private readonly BodySystem _bobby = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly BuckleSystem _buckle = default!; [Dependency] private readonly DamageableSystem _damageSys = default!; [Dependency] private readonly DockingSystem _dockSystem = default!; @@ -104,7 +104,12 @@ private void OnGridInit(GridInitializeEvent ev) return; EnsureComp(ev.EntityUid); - EnsureComp(ev.EntityUid); + + // This and RoofComponent should be mutually exclusive, so ImplicitRoof should be removed if the grid has RoofComponent + if (HasComp(ev.EntityUid)) + RemComp(ev.EntityUid); + else + EnsureComp(ev.EntityUid); } private void OnShuttleStartup(EntityUid uid, ShuttleComponent component, ComponentStartup args) diff --git a/Content.Server/Shuttles/Systems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs index f7f0a8b2511..3d3cf1aa285 100644 --- a/Content.Server/Shuttles/Systems/ThrusterSystem.cs +++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs @@ -1,16 +1,14 @@ using System.Numerics; using Content.Server.Audio; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Shuttles.Components; using Content.Shared.Temperature; -using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; @@ -137,7 +135,7 @@ private void OnShuttleTileChange(EntityUid uid, ShuttleComponent component, ref private void OnActivateThruster(EntityUid uid, ThrusterComponent component, ActivateInWorldEvent args) { - if (args.Handled || !args.Complex) + if (args.Handled || !args.Complex || !component.CanToggle) return; component.Enabled ^= true; diff --git a/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs b/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs deleted file mode 100644 index b41f1397ec0..00000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Mind.Components; -using Content.Shared.Roles; -using Content.Shared.Roles.Components; -using Content.Shared.Silicons.Borgs.Components; -using Robust.Shared.Containers; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - - [Dependency] private readonly SharedRoleSystem _roles = default!; - - public void InitializeMMI() - { - SubscribeLocalEvent(OnMMIInit); - SubscribeLocalEvent(OnMMIEntityInserted); - SubscribeLocalEvent(OnMMIMindAdded); - SubscribeLocalEvent(OnMMIMindRemoved); - - SubscribeLocalEvent(OnMMILinkedMindAdded); - SubscribeLocalEvent(OnMMILinkedRemoved); - } - - private void OnMMIInit(EntityUid uid, MMIComponent component, ComponentInit args) - { - if (!TryComp(uid, out var itemSlots)) - return; - - if (ItemSlots.TryGetSlot(uid, component.BrainSlotId, out var slot, itemSlots)) - component.BrainSlot = slot; - else - ItemSlots.AddItemSlot(uid, component.BrainSlotId, component.BrainSlot, itemSlots); - } - - private void OnMMIEntityInserted(EntityUid uid, MMIComponent component, EntInsertedIntoContainerMessage args) - { - if (args.Container.ID != component.BrainSlotId) - return; - - var ent = args.Entity; - var linked = EnsureComp(ent); - linked.LinkedMMI = uid; - Dirty(uid, component); - - if (_mind.TryGetMind(ent, out var mindId, out var mind)) - { - _mind.TransferTo(mindId, uid, true, mind: mind); - - if (!_roles.MindHasRole(mindId)) - _roles.MindAddRole(mindId, "MindRoleSiliconBrain", silent: true); - } - - _appearance.SetData(uid, MMIVisuals.BrainPresent, true); - } - - private void OnMMIMindAdded(EntityUid uid, MMIComponent component, MindAddedMessage args) - { - _appearance.SetData(uid, MMIVisuals.HasMind, true); - } - - private void OnMMIMindRemoved(EntityUid uid, MMIComponent component, MindRemovedMessage args) - { - _appearance.SetData(uid, MMIVisuals.HasMind, false); - } - - private void OnMMILinkedMindAdded(EntityUid uid, MMILinkedComponent component, MindAddedMessage args) - { - if (!_mind.TryGetMind(uid, out var mindId, out var mind) || - component.LinkedMMI == null) - return; - - _mind.TransferTo(mindId, component.LinkedMMI, true, mind: mind); - } - - private void OnMMILinkedRemoved(EntityUid uid, MMILinkedComponent component, EntGotRemovedFromContainerMessage args) - { - if (Terminating(uid)) - return; - - if (component.LinkedMMI is not { } linked) - return; - RemComp(uid, component); - - if (_mind.TryGetMind(linked, out var mindId, out var mind)) - { - if (_roles.MindHasRole(mindId)) - _roles.MindRemoveRole(mindId); - - _mind.TransferTo(mindId, uid, true, mind: mind); - } - - _appearance.SetData(linked, MMIVisuals.BrainPresent, false); - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs deleted file mode 100644 index bbd62d7a015..00000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs +++ /dev/null @@ -1,415 +0,0 @@ -using System.Linq; -using Content.Shared.Hands.Components; -using Content.Shared.Interaction.Components; -using Content.Shared.Silicons.Borgs.Components; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - public void InitializeModules() - { - SubscribeLocalEvent(OnModuleGotInserted); - SubscribeLocalEvent(OnModuleGotRemoved); - - SubscribeLocalEvent(OnSelectableInstalled); - SubscribeLocalEvent(OnSelectableUninstalled); - SubscribeLocalEvent(OnSelectableAction); - - SubscribeLocalEvent(OnProvideItemStartup); - SubscribeLocalEvent(OnItemModuleSelected); - SubscribeLocalEvent(OnItemModuleUnselected); - } - - private void OnModuleGotInserted(EntityUid uid, BorgModuleComponent component, EntGotInsertedIntoContainerMessage args) - { - var chassis = args.Container.Owner; - - if (!TryComp(chassis, out var chassisComp) || - args.Container != chassisComp.ModuleContainer || - !Toggle.IsActivated(chassis)) - return; - - if (!_powerCell.HasDrawCharge(uid)) - return; - - InstallModule(chassis, uid, chassisComp, component); - } - - private void OnModuleGotRemoved(EntityUid uid, BorgModuleComponent component, EntGotRemovedFromContainerMessage args) - { - var chassis = args.Container.Owner; - - if (!TryComp(chassis, out var chassisComp) || - args.Container != chassisComp.ModuleContainer) - return; - - UninstallModule(chassis, uid, chassisComp, component); - } - - private void OnProvideItemStartup(EntityUid uid, ItemBorgModuleComponent component, ComponentStartup args) - { - Container.EnsureContainer(uid, component.HoldingContainer); - } - - private void OnSelectableInstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleInstalledEvent args) - { - var chassis = args.ChassisEnt; - - if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid)) - { - var actEnt = (component.ModuleSwapActionEntity.Value, action); - _actions.SetEntityIcon(actEnt, uid); - if (TryComp(uid, out var moduleIconComp)) - _actions.SetIcon(actEnt, moduleIconComp.Icon); - } - - if (!TryComp(chassis, out BorgChassisComponent? chassisComp)) - return; - - if (chassisComp.SelectedModule == null) - SelectModule(chassis, uid, chassisComp, component); - } - - private void OnSelectableUninstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleUninstalledEvent args) - { - var chassis = args.ChassisEnt; - _actions.RemoveProvidedActions(chassis, uid); - if (!TryComp(chassis, out BorgChassisComponent? chassisComp)) - return; - - if (chassisComp.SelectedModule == uid) - UnselectModule(chassis, chassisComp); - } - - private void OnSelectableAction(EntityUid uid, SelectableBorgModuleComponent component, BorgModuleActionSelectedEvent args) - { - var chassis = args.Performer; - if (!TryComp(chassis, out var chassisComp)) - return; - - var selected = chassisComp.SelectedModule; - - args.Handled = true; - UnselectModule(chassis, chassisComp); - - if (selected != uid) - { - SelectModule(chassis, uid, chassisComp, component); - } - } - - /// - /// Selects a module, enabling the borg to use its provided abilities. - /// - public void SelectModule(EntityUid chassis, - EntityUid moduleUid, - BorgChassisComponent? chassisComp = null, - SelectableBorgModuleComponent? selectable = null, - BorgModuleComponent? moduleComp = null) - { - if (LifeStage(chassis) >= EntityLifeStage.Terminating) - return; - - if (!Resolve(chassis, ref chassisComp)) - return; - - if (!Resolve(moduleUid, ref moduleComp) || !moduleComp.Installed || moduleComp.InstalledEntity != chassis) - { - Log.Error($"{ToPrettyString(chassis)} attempted to select uninstalled module {ToPrettyString(moduleUid)}"); - return; - } - - if (selectable == null && !HasComp(moduleUid)) - { - Log.Error($"{ToPrettyString(chassis)} attempted to select invalid module {ToPrettyString(moduleUid)}"); - return; - } - - if (!chassisComp.ModuleContainer.Contains(moduleUid)) - { - Log.Error($"{ToPrettyString(chassis)} does not contain the installed module {ToPrettyString(moduleUid)}"); - return; - } - - if (chassisComp.SelectedModule != null) - return; - - if (chassisComp.SelectedModule == moduleUid) - return; - - UnselectModule(chassis, chassisComp); - - var ev = new BorgModuleSelectedEvent(chassis); - RaiseLocalEvent(moduleUid, ref ev); - chassisComp.SelectedModule = moduleUid; - Dirty(chassis, chassisComp); - } - - /// - /// Unselects a module, removing its provided abilities - /// - public void UnselectModule(EntityUid chassis, BorgChassisComponent? chassisComp = null) - { - if (LifeStage(chassis) >= EntityLifeStage.Terminating) - return; - - if (!Resolve(chassis, ref chassisComp)) - return; - - if (chassisComp.SelectedModule == null) - return; - - var ev = new BorgModuleUnselectedEvent(chassis); - RaiseLocalEvent(chassisComp.SelectedModule.Value, ref ev); - chassisComp.SelectedModule = null; - Dirty(chassis, chassisComp); - } - - private void OnItemModuleSelected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleSelectedEvent args) - { - ProvideItems(args.Chassis, uid, component: component); - } - - private void OnItemModuleUnselected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleUnselectedEvent args) - { - RemoveProvidedItems(args.Chassis, uid, component: component); - } - - private void ProvideItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null) - { - if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component)) - return; - - if (!TryComp(chassis, out var hands)) - return; - - if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container)) - return; - - var xform = Transform(chassis); - - for (var i = 0; i < component.Hands.Count; i++) - { - var hand = component.Hands[i]; - var handId = $"{uid}-hand-{i}"; - - _hands.AddHand((chassis, hands), handId, hand.Hand); - EntityUid? item = null; - - if (component.StoredItems is not null) - { - if (component.StoredItems.TryGetValue(handId, out var storedItem)) - { - item = storedItem; - _container.Remove(storedItem, container, force: true); - } - } - else if (hand.Item is { } itemProto) - { - item = Spawn(itemProto, xform.Coordinates); - } - - if (item is { } pickUp) - { - _hands.DoPickup(chassis, handId, pickUp, hands); - if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null) - { - EnsureComp(pickUp); - } - } - } - - Dirty(uid, component); - } - - private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null) - { - if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component)) - return; - - if (!TryComp(chassis, out var hands)) - return; - - if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container)) - return; - - if (TerminatingOrDeleted(uid)) - return; - - component.StoredItems ??= new(); - - for (var i = 0; i < component.Hands.Count; i++) - { - var handId = $"{uid}-hand-{i}"; - - if (_hands.TryGetHeldItem(chassis, handId, out var held)) - { - RemComp(held.Value); - _container.Insert(held.Value, container); - component.StoredItems[handId] = held.Value; - } - else - { - component.StoredItems.Remove(handId); - } - - _hands.RemoveHand(chassis, handId); - } - - Dirty(uid, component); - } - - /// - /// Checks if a given module can be inserted into a borg - /// - public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponent? component = null, BorgModuleComponent? moduleComponent = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return false; - - if (component.ModuleContainer.ContainedEntities.Count >= component.MaxModules) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-too-many"), uid, user.Value); - return false; - } - - if (_whitelistSystem.IsWhitelistFail(component.ModuleWhitelist, module)) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-whitelist-deny"), uid, user.Value); - return false; - } - - if (TryComp(module, out var itemModuleComp)) - { - foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities) - { - if (!TryComp(containedModuleUid, out var containedItemModuleComp)) - continue; - - if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count && - containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains)) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value); - return false; - } - } - } - - return true; - } - - /// - /// Check if a module can be removed from a borg. - /// - /// The borg that the module is being removed from. - /// The module to remove from the borg. - /// The user attempting to remove the module. - /// True if the module can be removed. - public bool CanRemoveModule( - Entity borg, - Entity module, - EntityUid? user = null) - { - if (module.Comp.DefaultModule) - return false; - - return true; - } - - /// - /// Installs and activates all modules currently inside the borg's module container - /// - public void InstallAllModules(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var query = GetEntityQuery(); - foreach (var moduleEnt in new List(component.ModuleContainer.ContainedEntities)) - { - if (!query.TryGetComponent(moduleEnt, out var moduleComp)) - continue; - - InstallModule(uid, moduleEnt, component, moduleComp); - } - } - - /// - /// Deactivates all modules currently inside the borg's module container - /// - /// - /// - public void DisableAllModules(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var query = GetEntityQuery(); - foreach (var moduleEnt in new List(component.ModuleContainer.ContainedEntities)) - { - if (!query.TryGetComponent(moduleEnt, out var moduleComp)) - continue; - - UninstallModule(uid, moduleEnt, component, moduleComp); - } - } - - /// - /// Installs a single module into a borg. - /// - public void InstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return; - - if (moduleComponent.Installed) - return; - - moduleComponent.InstalledEntity = uid; - var ev = new BorgModuleInstalledEvent(uid); - RaiseLocalEvent(module, ref ev); - } - - /// - /// Uninstalls a single module from a borg. - /// - public void UninstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return; - - if (!moduleComponent.Installed) - return; - - moduleComponent.InstalledEntity = null; - var ev = new BorgModuleUninstalledEvent(uid); - RaiseLocalEvent(module, ref ev); - } - - /// - /// Sets . - /// - /// The borg to modify. - /// The new max module count. - public void SetMaxModules(Entity ent, int maxModules) - { - ent.Comp.MaxModules = maxModules; - } - - /// - /// Sets . - /// - /// The borg to modify. - /// The new module whitelist. - public void SetModuleWhitelist(Entity ent, EntityWhitelist? whitelist) - { - ent.Comp.ModuleWhitelist = whitelist; - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs index 082e6776f08..d181c7e228b 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs @@ -1,9 +1,7 @@ -using Content.Shared.Containers.ItemSlots; using Content.Shared.DeviceNetwork; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.FixedPoint; using Content.Shared.Mobs; -using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Popups; using Content.Shared.Robotics; @@ -18,19 +16,13 @@ namespace Content.Server.Silicons.Borgs; /// public sealed partial class BorgSystem { - [Dependency] private readonly EmagSystem _emag = default!; - [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; - private void InitializeTransponder() { SubscribeLocalEvent(OnPacketReceived); } - public override void Update(float frameTime) + public void UpdateTransponder(float frameTime) { - base.Update(frameTime); - var now = _timing.CurTime; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta)) @@ -41,9 +33,9 @@ public override void Update(float frameTime) if (now < comp.NextBroadcast) continue; - var charge = 0f; + var chargeFraction = 0f; if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) - charge = battery.CurrentCharge / battery.MaxCharge; + chargeFraction = _battery.GetChargeLevel(battery.Value.AsNullable()); var hpPercent = CalcHP(uid); @@ -54,7 +46,7 @@ public override void Update(float frameTime) comp.Sprite, comp.Name, meta.EntityName, - charge, + chargeFraction, hpPercent, chassis.ModuleCount, hasBrain, @@ -85,7 +77,7 @@ private void DoDisable(Entity ent, ref DeviceNe if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND) Disable(ent); else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND) - Destroy(ent); + Destroy(ent.Owner); } private void Disable(Entity ent) @@ -113,13 +105,20 @@ private void Disable(Entity ent if (CheckEmagged(ent, "disabled")) ent.Comp1.FakeDisabling = true; else - Popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); + _popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay; } - private void Destroy(Entity ent) + /// + /// Makes a borg with explode + /// + /// the entity of the borg + public void Destroy(Entity ent) { + if (!Resolve(ent, ref ent.Comp)) + return; + // this is stealthy until someone realises you havent exploded if (CheckEmagged(ent, "destroyed")) { @@ -129,7 +128,7 @@ private void Destroy(Entity ent) } var message = Loc.GetString(ent.Comp.DestroyingPopup, ("name", Name(ent))); - Popup.PopupEntity(message, ent); + _popup.PopupEntity(message, ent); _trigger.ActivateTimerTrigger(ent.Owner); // prevent a shitter borg running into people @@ -140,7 +139,7 @@ private bool CheckEmagged(EntityUid uid, string name) { if (_emag.CheckFlag(uid, EmagType.Interaction)) { - Popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution); + _popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution); return true; } @@ -180,7 +179,7 @@ private float CalcHP(EntityUid uid) return 1; } - return 1 - ((FixedPoint2)(damageable.TotalDamage / threshold)).Float(); + return 1 - ((FixedPoint2)(_damageable.GetTotalDamage((uid, damageable)) / threshold)).Float(); } /// diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs deleted file mode 100644 index 58cd7135af9..00000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Linq; -using Content.Shared.UserInterface; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Content.Shared.NameIdentifier; -using Content.Shared.PowerCell.Components; -using Content.Shared.Preferences; -using Content.Shared.Silicons.Borgs; -using Content.Shared.Silicons.Borgs.Components; -using Robust.Shared.Configuration; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - // CCvar. - private int _maxNameLength; - - public void InitializeUI() - { - SubscribeLocalEvent(OnBeforeBorgUiOpen); - SubscribeLocalEvent(OnEjectBrainBuiMessage); - SubscribeLocalEvent(OnEjectBatteryBuiMessage); - SubscribeLocalEvent(OnSetNameBuiMessage); - SubscribeLocalEvent(OnRemoveModuleBuiMessage); - - Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); - } - - private void OnBeforeBorgUiOpen(EntityUid uid, BorgChassisComponent component, BeforeActivatableUIOpenEvent args) - { - UpdateUI(uid, component); - } - - private void OnEjectBrainBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBrainBuiMessage args) - { - if (component.BrainEntity is not { } brain) - return; - - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Actor):player} removed brain {ToPrettyString(brain)} from borg {ToPrettyString(uid)}"); - _container.Remove(brain, component.BrainContainer); - _hands.TryPickupAnyHand(args.Actor, brain); - UpdateUI(uid, component); - } - - private void OnEjectBatteryBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBatteryBuiMessage args) - { - if (TryEjectPowerCell(uid, component, out var ents)) - _hands.TryPickupAnyHand(args.Actor, ents.First()); - } - - private void OnSetNameBuiMessage(EntityUid uid, BorgChassisComponent component, BorgSetNameBuiMessage args) - { - if (args.Name.Length > _maxNameLength || - args.Name.Length == 0 || - string.IsNullOrWhiteSpace(args.Name) || - string.IsNullOrEmpty(args.Name)) - { - return; - } - - var name = args.Name.Trim(); - - var metaData = MetaData(uid); - - // don't change the name if the value doesn't actually change - if (metaData.EntityName.Equals(name, StringComparison.InvariantCulture)) - return; - - _adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.Actor):player} set borg \"{ToPrettyString(uid)}\"'s name to: {name}"); - _metaData.SetEntityName(uid, name, metaData); - } - - private void OnRemoveModuleBuiMessage(EntityUid uid, BorgChassisComponent component, BorgRemoveModuleBuiMessage args) - { - var module = GetEntity(args.Module); - - if (!component.ModuleContainer.Contains(module)) - return; - - if (!CanRemoveModule((uid, component), (module, Comp(module)), args.Actor)) - return; - - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Actor):player} removed module {ToPrettyString(module)} from borg {ToPrettyString(uid)}"); - _container.Remove(module, component.ModuleContainer); - _hands.TryPickupAnyHand(args.Actor, module); - - UpdateUI(uid, component); - } - - public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var chargePercent = 0f; - var hasBattery = false; - if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) - { - hasBattery = true; - chargePercent = battery.CurrentCharge / battery.MaxCharge; - } - - var state = new BorgBuiState(chargePercent, hasBattery); - _ui.SetUiState(uid, BorgUiKey.Key, state); - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index 88d484b8467..063514060e9 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -1,38 +1,18 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Actions; -using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.DeviceNetwork.Systems; -using Content.Server.Hands.Systems; -using Content.Server.PowerCell; -using Content.Shared.Alert; -using Content.Shared.Body.Events; -using Content.Shared.Database; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Item.ItemToggle.Components; -using Content.Shared.Mind; -using Content.Shared.Mind.Components; -using Content.Shared.Mobs; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage.Systems; +using Content.Shared.Emag.Systems; using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Systems; -using Content.Shared.Pointing; +using Content.Shared.Popups; +using Content.Shared.Power.EntitySystems; using Content.Shared.PowerCell; -using Content.Shared.PowerCell.Components; using Content.Shared.Roles; using Content.Shared.Silicons.Borgs; -using Content.Shared.Silicons.Borgs.Components; -using Content.Shared.Throwing; using Content.Shared.Trigger.Systems; -using Content.Shared.Whitelist; -using Content.Shared.Wires; -using Robust.Server.GameObjects; -using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Silicons.Borgs; @@ -40,27 +20,19 @@ namespace Content.Server.Silicons.Borgs; /// public sealed partial class BorgSystem : SharedBorgSystem { - [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IBanManager _banManager = default!; - [Dependency] private readonly IConfigurationManager _cfgManager = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ActionsSystem _actions = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TriggerSystem _trigger = default!; - [Dependency] private readonly HandsSystem _hands = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly ThrowingSystem _throwing = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; + [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public static readonly ProtoId BorgJobId = "Borg"; @@ -69,293 +41,21 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnChassisInteractUsing); - SubscribeLocalEvent(OnMindAdded); - SubscribeLocalEvent(OnMindRemoved); - SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnBeingGibbed); - SubscribeLocalEvent(OnPowerCellChanged); - SubscribeLocalEvent(OnPowerCellSlotEmpty); - SubscribeLocalEvent(OnGetDeadIC); - SubscribeLocalEvent(OnGetUnrevivableIC); - SubscribeLocalEvent(OnToggled); - - SubscribeLocalEvent(OnBrainMindAdded); - SubscribeLocalEvent(OnBrainPointAttempt); - - InitializeModules(); - InitializeMMI(); - InitializeUI(); InitializeTransponder(); } - private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args) - { - UpdateBatteryAlert((uid, component)); - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - private void OnChassisInteractUsing(EntityUid uid, BorgChassisComponent component, AfterInteractUsingEvent args) - { - if (!args.CanReach || args.Handled || uid == args.User) - return; - - var used = args.Used; - TryComp(used, out var brain); - TryComp(used, out var module); - - if (TryComp(uid, out var panel) && !panel.Open) - { - if (brain != null || module != null) - { - Popup.PopupEntity(Loc.GetString("borg-panel-not-open"), uid, args.User); - } - return; - } - - if (component.BrainEntity == null && brain != null && - _whitelistSystem.IsWhitelistPassOrNull(component.BrainWhitelist, used)) - { - if (_mind.TryGetMind(used, out _, out var mind) && - _player.TryGetSessionById(mind.UserId, out var session)) - { - if (!CanPlayerBeBorged(session)) - { - Popup.PopupEntity(Loc.GetString("borg-player-not-allowed"), used, args.User); - return; - } - } - - _container.Insert(used, component.BrainContainer); - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.User):player} installed brain {ToPrettyString(used)} into borg {ToPrettyString(uid)}"); - args.Handled = true; - UpdateUI(uid, component); - } - - if (module != null && CanInsertModule(uid, used, component, module, args.User)) - { - InsertModule((uid, component), used); - _adminLog.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(args.User):player} installed module {ToPrettyString(used)} into borg {ToPrettyString(uid)}"); - args.Handled = true; - UpdateUI(uid, component); - } - } - - /// - /// Inserts a new module into a borg, the same as if a player inserted it manually. - /// - /// - /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists. - /// - /// The borg to insert into. - /// The module to insert. - public void InsertModule(Entity ent, EntityUid module) - { - _container.Insert(module, ent.Comp.ModuleContainer); - } - - // todo: consider transferring over the ghost role? managing that might suck. - protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args) - { - base.OnInserted(uid, component, args); - - if (HasComp(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind) && args.Container == component.BrainContainer) - { - _mind.TransferTo(mindId, uid, mind: mind); - } - } - - protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args) - { - base.OnRemoved(uid, component, args); - - if (HasComp(args.Entity) && _mind.TryGetMind(uid, out var mindId, out var mind) && args.Container == component.BrainContainer) - { - _mind.TransferTo(mindId, args.Entity, mind: mind); - } - } - - private void OnMindAdded(EntityUid uid, BorgChassisComponent component, MindAddedMessage args) - { - BorgActivate(uid, component); - } - - private void OnMindRemoved(EntityUid uid, BorgChassisComponent component, MindRemovedMessage args) - { - BorgDeactivate(uid, component); - } - - private void OnMobStateChanged(EntityUid uid, BorgChassisComponent component, MobStateChangedEvent args) - { - if (args.NewMobState == MobState.Alive) - { - if (_mind.TryGetMind(uid, out _, out _)) - _powerCell.SetDrawEnabled(uid, true); - } - else - { - _powerCell.SetDrawEnabled(uid, false); - } - } - - private void OnBeingGibbed(EntityUid uid, BorgChassisComponent component, ref BeingGibbedEvent args) - { - TryEjectPowerCell(uid, component, out var _); - - _container.EmptyContainer(component.BrainContainer); - _container.EmptyContainer(component.ModuleContainer); - } - - private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args) - { - UpdateBatteryAlert((uid, component)); - - // if we aren't drawing and suddenly get enough power to draw again, reeanble. - if (_powerCell.HasDrawCharge(uid)) - { - Toggle.TryActivate(uid); - } - - UpdateUI(uid, component); - } - - private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args) + public override bool CanPlayerBeBorged(ICommonSession session) { - Toggle.TryDeactivate(uid); - UpdateUI(uid, component); - } - - private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args) - { - args.Dead = true; - } - - private void OnGetUnrevivableIC(EntityUid uid, BorgChassisComponent component, ref GetCharacterUnrevivableIcEvent args) - { - args.Unrevivable = true; - } - - private void OnToggled(Entity ent, ref ItemToggledEvent args) - { - var (uid, comp) = ent; - if (args.Activated) - InstallAllModules(uid, comp); - else - DisableAllModules(uid, comp); - - // only enable the powerdraw if there is a player in the chassis - var drawing = _mind.TryGetMind(uid, out _, out _) && _mobState.IsAlive(ent); - _powerCell.SetDrawEnabled(uid, drawing); - - UpdateUI(uid, comp); - - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - private void OnBrainMindAdded(EntityUid uid, BorgBrainComponent component, MindAddedMessage args) - { - if (!Container.TryGetOuterContainer(uid, Transform(uid), out var container)) - return; - - var containerEnt = container.Owner; - - if (!TryComp(containerEnt, out var chassisComponent) || - container.ID != chassisComponent.BrainContainerId) - return; - - if (!_mind.TryGetMind(uid, out var mindId, out var mind) || - !_player.TryGetSessionById(mind.UserId, out var session)) - return; - - if (!CanPlayerBeBorged(session)) - { - Popup.PopupEntity(Loc.GetString("borg-player-not-allowed-eject"), uid); - Container.RemoveEntity(containerEnt, uid); - _throwing.TryThrow(uid, _random.NextVector2() * 5, 5f); - return; - } - - _mind.TransferTo(mindId, containerEnt, mind: mind); - } - - private void OnBrainPointAttempt(EntityUid uid, BorgBrainComponent component, PointAttemptEvent args) - { - args.Cancel(); - } - - private void UpdateBatteryAlert(Entity ent, PowerCellSlotComponent? slotComponent = null) - { - if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent)) - { - _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert); - _alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert); - return; - } - - var chargePercent = (short) MathF.Round(battery.CurrentCharge / battery.MaxCharge * 10f); - - // we make sure 0 only shows if they have absolutely no battery. - // also account for floating point imprecision - if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent)) - { - chargePercent = 1; - } - - _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert); - _alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent); - } - - public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List? ents) - { - ents = null; - - if (!TryComp(uid, out var slotComp) || - !Container.TryGetContainer(uid, slotComp.CellSlotId, out var container) || - !container.ContainedEntities.Any()) + if (_banManager.GetJobBans(session.UserId)?.Contains(BorgJobId) == true) return false; - ents = Container.EmptyContainer(container); - return true; } - /// - /// Activates a borg when a player occupies it - /// - public void BorgActivate(EntityUid uid, BorgChassisComponent component) - { - Popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(uid, EntityManager))), uid); - if (_powerCell.HasDrawCharge(uid)) - { - Toggle.TryActivate(uid); - _powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid)); - } - _appearance.SetData(uid, BorgVisuals.HasPlayer, true); - } - - /// - /// Deactivates a borg when a player leaves it. - /// - public void BorgDeactivate(EntityUid uid, BorgChassisComponent component) + public override void Update(float frameTime) { - Popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(uid, EntityManager))), uid); - Toggle.TryDeactivate(uid); - _powerCell.SetDrawEnabled(uid, false); - _appearance.SetData(uid, BorgVisuals.HasPlayer, false); - } + base.Update(frameTime); - /// - /// Checks that a player has fulfilled the requirements for the borg job. - /// If they don't have enough hours, they cannot be placed into a chassis. - /// - public bool CanPlayerBeBorged(ICommonSession session) - { - if (_banManager.GetJobBans(session.UserId)?.Contains(BorgJobId) == true) - return false; - - return true; + UpdateTransponder(frameTime); } } diff --git a/Content.Server/Silicons/Bots/HugBotSystem.cs b/Content.Server/Silicons/Bots/HugBotSystem.cs new file mode 100644 index 00000000000..4be948f434c --- /dev/null +++ b/Content.Server/Silicons/Bots/HugBotSystem.cs @@ -0,0 +1,65 @@ +using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; +using Content.Shared.Silicons.Bots; +using Robust.Shared.Timing; + +namespace Content.Server.Silicons.Bots; + +/// +/// Beyond what does, this system manages the "lifecycle" of +/// . +/// +public sealed class HugBotSystem : SharedHugBotSystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHtnRaisedEvent); + } + + private void OnHtnRaisedEvent(Entity entity, ref HTNRaisedEvent args) + { + if (args.Args is not HugBotDidHugEvent || + args.Target is not {} target) + return; + + var ev = new HugBotHugEvent(GetNetEntity(entity)); + RaiseLocalEvent(target, ev); + + ApplyHugBotCooldown(entity, target); + } + + /// + /// Applies to based on the configuration of + /// . + /// + public void ApplyHugBotCooldown(Entity hugBot, EntityUid target) + { + var hugged = EnsureComp(target); + hugged.CooldownCompleteAfter = _gameTiming.CurTime + hugBot.Comp.HugCooldown; + } + + public override void Update(float frameTime) + { + // Iterate through all RecentlyHuggedByHugBot entities... + var huggedEntities = AllEntityQuery(); + while (huggedEntities.MoveNext(out var huggedEnt, out var huggedComp)) + { + // ... and if their cooldown is complete... + if (huggedComp.CooldownCompleteAfter <= _gameTiming.CurTime) + { + // ... remove it, allowing them to receive the blessing of hugs once more. + RemCompDeferred(huggedEnt); + } + } + } +} + +/// +/// This event is indirectly raised (by being ) on a HugBot when it hugs (or emaggedly +/// punches) an entity. +/// +[Serializable, DataDefinition] +public sealed partial class HugBotDidHugEvent : EntityEventArgs; diff --git a/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs b/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs new file mode 100644 index 00000000000..c6c7d47e8d5 --- /dev/null +++ b/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Silicons.Bots; + +namespace Content.Server.Silicons.Bots; + +/// +/// This marker component indicates that its entity has been recently hugged by a HugBot and should not be hugged again +/// before a cooldown period in order to prevent hug spam. +/// +/// +[RegisterComponent, AutoGenerateComponentPause] +public sealed partial class RecentlyHuggedByHugBotComponent : Component +{ + [DataField, AutoPausedField] + public TimeSpan CooldownCompleteAfter = TimeSpan.MinValue; +} diff --git a/Content.Server/Silicons/Laws/IonLawLocalizationSystem.cs b/Content.Server/Silicons/Laws/IonLawLocalizationSystem.cs new file mode 100644 index 00000000000..a08e7f0b202 --- /dev/null +++ b/Content.Server/Silicons/Laws/IonLawLocalizationSystem.cs @@ -0,0 +1,59 @@ +namespace Content.Server.Silicons.Laws; + +public sealed class IonLawLocalizationSystem : EntitySystem +{ + [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IonLawSystem _ionLaw = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _logManager.GetSawmill("ion-law"); + + var culture = _loc.DefaultCulture; + + if (culture == null) + { + _sawmill.Error("Culture was null when trying to generate Ion Law"); + return; + } + + _loc.AddFunction(culture, "ION-NUMBER-BASE", _ => GetIonLawValue("ION-NUMBER-BASE")); + _loc.AddFunction(culture, "ION-NUMBER-MOD", _ => GetIonLawValue("ION-NUMBER-MOD")); + _loc.AddFunction(culture, "ION-ADJECTIVE", _ => GetIonLawValue("ION-ADJECTIVE")); + _loc.AddFunction(culture, "ION-SUBJECT", _ => GetIonLawValue("ION-SUBJECT")); + _loc.AddFunction(culture, "ION-WHO", _ => GetIonLawValue("ION-WHO")); + _loc.AddFunction(culture, "ION-MUST", _ => GetIonLawValue("ION-MUST")); + _loc.AddFunction(culture, "ION-THING", _ => GetIonLawValue("ION-THING")); + _loc.AddFunction(culture, "ION-JOB", _ => GetIonLawValue("ION-JOB")); + _loc.AddFunction(culture, "ION-WHO-GENERAL", _ => GetIonLawValue("ION-WHO-GENERAL")); + _loc.AddFunction(culture, "ION-PLURAL", _ => GetIonLawValue("ION-PLURAL")); + _loc.AddFunction(culture, "ION-REQUIRE", _ => GetIonLawValue("ION-REQUIRE")); + _loc.AddFunction(culture, "ION-SEVERITY", _ => GetIonLawValue("ION-SEVERITY")); + _loc.AddFunction(culture, "ION-ALLERGY", _ => GetIonLawValue("ION-ALLERGY")); + _loc.AddFunction(culture, "ION-FEELING", _ => GetIonLawValue("ION-FEELING")); + _loc.AddFunction(culture, "ION-CONCEPT", _ => GetIonLawValue("ION-CONCEPT")); + _loc.AddFunction(culture, "ION-FOOD", _ => GetIonLawValue("ION-FOOD")); + _loc.AddFunction(culture, "ION-DRINK", _ => GetIonLawValue("ION-DRINK")); + _loc.AddFunction(culture, "ION-CHANGE", _ => GetIonLawValue("ION-CHANGE")); + _loc.AddFunction(culture, "ION-WHO-RANDOM", _ => GetIonLawValue("ION-WHO-RANDOM")); + _loc.AddFunction(culture, "ION-AREA", _ => GetIonLawValue("ION-AREA")); + _loc.AddFunction(culture, "ION-PART", _ => GetIonLawValue("ION-PART")); + _loc.AddFunction(culture, "ION-OBJECT", _ => GetIonLawValue("ION-OBJECT")); + _loc.AddFunction(culture, "ION-HARM-PROTECT", _ => GetIonLawValue("ION-HARM-PROTECT")); + _loc.AddFunction(culture, "ION-VERB", _ => GetIonLawValue("ION-VERB")); + } + + /// + /// Returns a localized value for an ion law token. + /// + private ILocValue GetIonLawValue(string s) + { + var val = _ionLaw.GetOrGenerateValue(s); + return new LocValueString(val.ToString() ?? string.Empty); + } +} diff --git a/Content.Server/Silicons/Laws/IonLawSystem.cs b/Content.Server/Silicons/Laws/IonLawSystem.cs new file mode 100644 index 00000000000..b8bc955825a --- /dev/null +++ b/Content.Server/Silicons/Laws/IonLawSystem.cs @@ -0,0 +1,296 @@ +using Content.Server.StationRecords.Systems; +using Content.Shared.Dataset; +using Content.Shared.Silicons.Laws; +using Content.Shared.Station; +using Content.Shared.StationRecords; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.Silicons.Laws; + +/// +/// This handles generating random ion laws. +/// +public sealed class IonLawSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedStationSystem _stationSystem = default!; + [Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; + private readonly Dictionary> _selectors = new(); + private IonLawPrototype? _ionLaw; + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _logManager.GetSawmill("ion-law"); + + SubscribeLocalEvent(OnPrototypesReloaded); + BuildSelectors(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj) + { + if (obj.ByType.ContainsKey(typeof(DatasetPrototype))) + BuildSelectors(); + } + + private void BuildSelectors() + { + _selectors.Clear(); + + + DatasetFill DatasetFill(ProtoId datasetId) => new() { Dataset = datasetId }; + RandomManifestFill RandomManifestFill(ProtoId fallback) => new() { FallbackDataset = fallback }; + ConstantFill ConstantFill(bool val) => new() { BoolValue = val }; + + AddSelector("ION-NUMBER-BASE", DatasetFill("IonStormNumberBase")); + + AddSelector("ION-NUMBER-MOD", DatasetFill("IonStormNumberMod")); + + AddSelector("ION-ADJECTIVE", DatasetFill("IonStormAdjectives")); + + AddSelector("ION-SUBJECT", DatasetFill("IonStormThreats")); + AddSelector("ION-SUBJECT", DatasetFill("IonStormCrew")); + AddSelector("ION-SUBJECT", DatasetFill("IonStormObjects")); + AddSelector("ION-SUBJECT", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-WHO", DatasetFill("IonStormCrew")); + AddSelector("ION-WHO", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-MUST", DatasetFill("IonStormMusts")); + + AddSelector("ION-THING", DatasetFill("IonStormObjects")); + AddSelector("ION-THING", DatasetFill("IonStormConcepts")); + + AddSelector("ION-JOB", DatasetFill("IonStormCrew")); + AddSelector("ION-JOB", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-WHO-GENERAL", DatasetFill("IonStormAreas")); + AddSelector("ION-WHO-GENERAL", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-PLURAL", ConstantFill(true)); + AddSelector("ION-PLURAL", ConstantFill(false)); + + AddSelector("ION-REQUIRE", DatasetFill("IonStormConcepts")); + AddSelector("ION-REQUIRE", DatasetFill("IonStormRequires")); + + AddSelector("ION-SEVERITY", DatasetFill("IonStormAllergySeverities")); + + AddSelector("ION-ALLERGY", DatasetFill("IonStormConcepts")); + AddSelector("ION-ALLERGY", DatasetFill("IonStormAllergies")); + + AddSelector("ION-FEELING", DatasetFill("IonStormFeelings")); + + AddSelector("ION-CONCEPT", DatasetFill("IonStormConcepts")); + + AddSelector("ION-FOOD", DatasetFill("IonStormFoods")); + + AddSelector("ION-DRINK", DatasetFill("IonStormDrinks")); + + AddSelector("ION-CHANGE", DatasetFill("IonStormCrew")); + AddSelector("ION-CHANGE", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-WHO-RANDOM", DatasetFill("IonStormCrew")); + AddSelector("ION-WHO-RANDOM", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-AREA", DatasetFill("IonStormAreas")); + + AddSelector("ION-PART", ConstantFill(true)); + AddSelector("ION-PART", ConstantFill(false)); + + AddSelector("ION-OBJECT", DatasetFill("IonStormObjects")); + + AddSelector("ION-HARM-PROTECT", DatasetFill("IonStormConcepts")); + AddSelector("ION-HARM-PROTECT", DatasetFill("IonStormCrew")); + AddSelector("ION-HARM-PROTECT", RandomManifestFill("IonStormCrew")); + + AddSelector("ION-VERB", DatasetFill("IonStormVerbs")); + } + + /// + /// Adds a selector to the cache. + /// Used for picking datasets to fill the keys in the strings for the Ion Laws. + /// + /// The key in the strings file to fill. + /// The type of dataset to use and the prototype ID for it, if it takes one. + private void AddSelector(string key, IonLawSelector selector) + { + if (!_selectors.ContainsKey(key)) + _selectors[key] = new List(); + + _selectors[key].Add(selector); + } + + + /// + /// Generates a random ion law by picking an ion law prototype and filling its placeholders with random values, from datasets. + /// + /// A formatted string representing the new ion law. + public string GetIonLaw() + { + var laws = _prototypeManager.EnumeratePrototypes().ToList(); + if (laws.Count == 0) + { + _sawmill.Error("No Ion Laws found"); + return Loc.GetString("ion-law-error-no-protos"); + } + + + var totalWeight = laws.Sum(p => p.Weight); + if (totalWeight <= 0) + { + // if all weights are 0, teat them as equal and just pick one at random. + _ionLaw = _random.Pick(laws); + } + else + { + var value = _random.NextFloat() * totalWeight; + IonLawPrototype? ionLaw = null; + foreach (var law in laws) + { + if (law.Weight <= 0) + continue; + + ionLaw = law; + value -= law.Weight; + if (value <= 0) + { + break; + } + } + _ionLaw = ionLaw; + } + + if (_ionLaw == null) + { + _sawmill.Error("Ion Law was null"); + return Loc.GetString("ion-law-error-was-null"); + } + + return Loc.GetString(_ionLaw.LawString, ("ion", 0)); + } + + /// + /// Gets a value for a specific selector and index, generating it if it doesn't exist in the cache. + /// This allows laws to reference the same generated value multiple times using the same index. + /// + /// The key of the selector list to pick from (e.g.: "ION-WHO"). + /// A string or object representing the generated law component. + public object GetOrGenerateValue(string selectorName) + { + if (!_selectors.TryGetValue(selectorName, out var selectors)) + { + _sawmill.Error("No selectors for Ion Laws found"); + return Loc.GetString("ion-law-error-no-selectors"); + } + + var availableSelectors = selectors.ToList(); + while (availableSelectors.Count > 0) + { + var selector = PickSelector(availableSelectors, _ionLaw); + if (selector == null) + break; + + var newValue = GetSelectorValue(selector); + if (newValue is string s && string.IsNullOrWhiteSpace(s)) + { + availableSelectors.Remove(selector); + continue; + } + + return newValue; + } + + _sawmill.Error("No available selectors found for the Ion Law found - this should never happen, selector was: " + selectorName); + return Loc.GetString("ion-law-error-no-available-selectors"); + } + + private IonLawSelector? PickSelector(List selectors, IonLawPrototype? law) + { + var weightedSelectors = new List<(IonLawSelector, float)>(); + foreach (var s in selectors) + { + var weight = s.Weight; + + if (s is DatasetFill df) + { + if (law != null && law.SelectorWeightAdjust.TryGetValue(df.Dataset.Id, out var adjustedWeight)) + weight = adjustedWeight; + } + else if (s is RandomManifestFill) + { + if (law != null && law.SelectorWeightAdjust.TryGetValue("RandomManifestFill", out var adjustedWeight)) + { + weight = adjustedWeight; + } + } + + if (weight > 0) + weightedSelectors.Add((s, weight)); + } + + var totalWeight = weightedSelectors.Sum(s => s.Item2); + if (totalWeight <= 0) + return null; + + var value = _random.NextFloat() * totalWeight; + foreach (var (selector, selectorWeight) in weightedSelectors) + { + value -= selectorWeight; + if (value <= 0) + return selector; + } + + return weightedSelectors.Last().Item1; + } + + private object GetSelectorValue(IonLawSelector selector) + { + switch (selector) + { + case DatasetFill datasetFill: + if (_prototypeManager.TryIndex(datasetFill.Dataset, out var dataset) && dataset.Values.Any()) + { + return _random.Pick(dataset.Values); + } + _sawmill.Error("Selected DataSet (" + selector + ") was empty or not found" ); + return Loc.GetString("ion-law-error-dataset-empty-or-not-found"); + case RandomManifestFill randomManifestFill: + var stations = _stationSystem.GetStations(); + if (stations.Count > 0) + { + var station = _random.Pick(stations); + if (TryComp(station, out StationRecordsComponent? stationRecords) && + _stationRecordsSystem.TryGetRandomRecord((station, stationRecords), out GeneralStationRecord? record)) + { + var upperName = "'" + record.Name.ToUpper() + "'"; + return upperName; + } + } + + // Fallback to dataset if no manifest record found or stations are empty + if (_prototypeManager.TryIndex(randomManifestFill.FallbackDataset, out var fallbackDataset) && fallbackDataset.Values.Any()) + { + return _random.Pick(fallbackDataset.Values); + } + _sawmill.Error("Fallback DataSet (" + selector + ") was empty or not found" ); + return Loc.GetString("ion-law-error-fallback-dataset-empty-or-not-found"); + case ConstantFill constantFill: + if (constantFill.BoolValue.HasValue) + return constantFill.BoolValue.Value; + _sawmill.Error("The selected Constant Fill did not have a value: " + constantFill ); + return Loc.GetString("ion-law-error-no-bool-value"); + default: + { + _sawmill.Error("Selected DataSet (" + selector + ") was not selected" ); + return Loc.GetString("ion-law-error-no-selector-selected"); + } + } + } +} diff --git a/Content.Server/Silicons/Laws/IonStormSystem.cs b/Content.Server/Silicons/Laws/IonStormSystem.cs index 249813edc15..321a83b8029 100644 --- a/Content.Server/Silicons/Laws/IonStormSystem.cs +++ b/Content.Server/Silicons/Laws/IonStormSystem.cs @@ -1,16 +1,13 @@ -using Content.Server.StationEvents.Components; using Content.Shared.Administration.Logs; using Content.Shared.Database; -using Content.Shared.Dataset; using Content.Shared.FixedPoint; -using Content.Shared.GameTicking.Components; -using Content.Shared.Random; -using Content.Shared.Random.Helpers; using Content.Shared.Silicons.Laws; using Content.Shared.Silicons.Laws.Components; -using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; +using Content.Shared.Random; +using Content.Shared.Random.Helpers; +using Robust.Shared.Prototypes; namespace Content.Server.Silicons.Laws; @@ -20,26 +17,7 @@ public sealed class IonStormSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SiliconLawSystem _siliconLaw = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; - - // funny - private static readonly ProtoId Threats = "IonStormThreats"; - private static readonly ProtoId Objects = "IonStormObjects"; - private static readonly ProtoId Crew = "IonStormCrew"; - private static readonly ProtoId Adjectives = "IonStormAdjectives"; - private static readonly ProtoId Verbs = "IonStormVerbs"; - private static readonly ProtoId NumberBase = "IonStormNumberBase"; - private static readonly ProtoId NumberMod = "IonStormNumberMod"; - private static readonly ProtoId Areas = "IonStormAreas"; - private static readonly ProtoId Feelings = "IonStormFeelings"; - private static readonly ProtoId FeelingsPlural = "IonStormFeelingsPlural"; - private static readonly ProtoId Musts = "IonStormMusts"; - private static readonly ProtoId Requires = "IonStormRequires"; - private static readonly ProtoId Actions = "IonStormActions"; - private static readonly ProtoId Allergies = "IonStormAllergies"; - private static readonly ProtoId AllergySeverities = "IonStormAllergySeverities"; - private static readonly ProtoId Concepts = "IonStormConcepts"; - private static readonly ProtoId Drinks = "IonStormDrinks"; - private static readonly ProtoId Foods = "IonStormFoods"; + [Dependency] private readonly IonLawSystem _ionLaw = default!; /// /// Randomly alters the laws of an individual silicon. @@ -93,7 +71,10 @@ public void IonStormTarget(Entity 0 && _robustRandom.Prob(target.ReplaceChance)) @@ -142,121 +123,4 @@ public void IonStormTarget(Entity threats, - 1 => crew1, - 2 => objects, - _ => throw new IndexOutOfRangeException(), - }; - var crewAll = _robustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew"); - var objectsThreats = _robustRandom.Prob(0.5f) ? objects : threats; - var objectsConcept = _robustRandom.Prob(0.5f) ? objects : concept; - // s goes ahead of require, is/are - // i dont think theres a way to do this in fluent - var (who, plural) = _robustRandom.Next(0, 5) switch - { - 0 => (Loc.GetString("ion-storm-you"), true), - 1 => (Loc.GetString("ion-storm-the-station"), false), - 2 => (Loc.GetString("ion-storm-the-crew"), false), - 3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), true), - _ => (area, false) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS - }; - var jobChange = _robustRandom.Next(0, 3) switch - { - 0 => crew1, - 1 => Loc.GetString("ion-storm-clowns"), - _ => Loc.GetString("ion-storm-heads") - }; - var part = Loc.GetString("ion-storm-part", ("part", _robustRandom.Prob(0.5f))); - var harm = _robustRandom.Next(0, 6) switch - { - 0 => concept, - 1 => $"{adjective} {threats}", - 2 => $"{adjective} {objects}", - 3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)), - 4 => crew1, - _ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2)) - }; - - if (plural) feeling = feelingPlural; - - var subjects = _robustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people"); - - // message logic!!! - return _robustRandom.Next(0, 35) switch - { - 0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)), - 1 => Loc.GetString("ion-storm-law-call-shuttle", ("joined", joined), ("subjects", triple)), - 2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)), - 3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)), - 4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)), - 5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", _robustRandom.Prob(0.5f) ? concept : action)), - 6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)), - 7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)), - 8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)), - 9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", _robustRandom.Prob(0.5f) ? concept : require)), - 10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)), - 11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", _robustRandom.Prob(0.5f) ? concept : allergy)), - 12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", _robustRandom.Prob(0.5f) ? objects : crew1)), - 13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)), - 14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)), - 15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)), - 16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)), - 17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)), - 18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)), - 19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", _robustRandom.Prob(0.5f) ? food : triple)), - 20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)), - 21 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)), - 22 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)), - 23 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)), - 24 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)), - 25 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)), - 26 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)), - 27 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)), - 28 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)), - 29 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)), - 30 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)), - 31 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)), - 32 => Loc.GetString("ion-storm-law-harm", ("who", harm)), - 33 => Loc.GetString("ion-storm-law-protect", ("who", harm)), - _ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple)) - }; - } - - /// - /// Picks a random value from an ion storm dataset. - /// All ion storm datasets start with IonStorm. - /// - private string Pick(string name) - { - var dataset = _proto.Index(name); - return _robustRandom.Pick(dataset.Values); - } } diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs index 79b51f22216..691c40be936 100644 --- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs +++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.GameTicking; using Content.Shared.Mind; using Content.Shared.Mind.Components; +using Content.Shared.Overlays; using Content.Shared.Radio.Components; using Content.Shared.Roles; using Content.Shared.Roles.Components; @@ -33,6 +34,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly EmagSystem _emag = default!; + private static readonly ProtoId DefaultCrewLawset = "Crewsimov"; + /// public override void Initialize() { @@ -303,6 +306,11 @@ protected override void OnUpdaterInsert(Entity ent, while (query.MoveNext(out var update)) { + if (TryComp(update, out var crewIconComp)) + { + crewIconComp.UncertainCrewBorder = DefaultCrewLawset != provider.Laws; + Dirty(update, crewIconComp); + } SetLaws(lawset.Laws, update, provider.LawUploadSound); } } diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 73c5670c1ea..b16c98f56b8 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -2,9 +2,10 @@ using Content.Server.Construction; using Content.Server.Destructible; using Content.Server.Ghost; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Server.Roles; using Content.Server.Spawners.Components; using Content.Server.Spawners.EntitySystems; @@ -12,13 +13,16 @@ using Content.Shared.Alert; using Content.Shared.Chat.Prototypes; using Content.Shared.Containers.ItemSlots; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Destructible; using Content.Shared.DeviceNetwork.Components; using Content.Shared.DoAfter; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; +using Content.Shared.Power; +using Content.Shared.Power.EntitySystems; using Content.Shared.Power.Components; using Content.Shared.Rejuvenate; using Content.Shared.Roles; @@ -45,9 +49,10 @@ public sealed class StationAiSystem : SharedStationAiSystem [Dependency] private readonly RoleSystem _roles = default!; [Dependency] private readonly ItemSlotsSystem _slots = default!; [Dependency] private readonly GhostSystem _ghost = default!; + [Dependency] private readonly ToggleableGhostRoleSystem _ghostrole = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DestructibleSystem _destructible = default!; - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedPopupSystem _popups = default!; [Dependency] private readonly StationSystem _station = default!; @@ -62,11 +67,12 @@ public sealed class StationAiSystem : SharedStationAiSystem private readonly ProtoId _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; private readonly ProtoId _aiLosingPowerChatNotificationPrototype = "AiLosingPower"; private readonly ProtoId _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower"; + private readonly ProtoId _aiTakingDamageChatNotificationPrototype = "AiTakingDamage"; private readonly ProtoId _stationAiJob = "StationAi"; private readonly EntProtoId _stationAiBrain = "StationAiBrain"; - private readonly ProtoId _batteryAlert = "BorgBattery"; + private readonly ProtoId _batteryAlert = "AiBattery"; private readonly ProtoId _damageAlert = "BorgHealth"; public override void Initialize() @@ -95,14 +101,30 @@ private void AfterConstructionChangeEntity(Entity ent, r } var brain = container.ContainedEntities[0]; + var hasMind = _mind.TryGetMind(brain, out var mindId, out var mind); - if (_mind.TryGetMind(brain, out var mindId, out var mind)) + if (hasMind || HasComp(brain)) { - // Found an existing mind to transfer into the AI core var aiBrain = Spawn(_stationAiBrain, Transform(ent.Owner).Coordinates); - _roles.MindAddJobRole(mindId, mind, false, _stationAiJob); - _mind.TransferTo(mindId, aiBrain); + if (hasMind) + { + // Found an existing mind to transfer into the AI core + _roles.MindAddJobRole(mindId, mind, false, _stationAiJob); + _mind.TransferTo(mindId, aiBrain); + } + else + { + // If the brain had a ghost role attached, activate the station AI ghost role + _ghostrole.ActivateGhostRole(aiBrain); + + // Set the new AI brain to the 'rebooting' state + if (TryComp(aiBrain, out var customization)) + SetStationAiState((aiBrain, customization), StationAiState.Rebooting); + + } + + // Delete the new AI brain if it cannot be inserted into the core if (!TryComp(ent, out var targetHolder) || !_slots.TryInsert(ent, targetHolder.Slot, aiBrain, null)) { @@ -123,13 +145,10 @@ private void OnContainerSpawn(Entity ent, ref ContainerS // into an AI core that has a full battery and full integrity. if (TryComp(ent, out var battery)) { - _battery.SetCharge(ent, battery.MaxCharge); + _battery.SetCharge((ent, battery), battery.MaxCharge); } - if (TryComp(ent, out var damageable)) - { - _damageable.SetAllDamage(ent, damageable, 0); - } + _damageable.ClearAllDamage(ent.Owner); } protected override void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -217,10 +236,11 @@ private void OnChargeChanged(Entity entity, ref ChargeCh private void OnDamageChanged(Entity entity, ref DamageChangedEvent args) { - UpdateCoreIntegrityAlert(entity); + UpdateCoreIntegrityAlert(entity, args.DamageIncreased); UpdateDamagedAccent(entity); } + // TODO: This should just read the current damage and charge when speaking instead of updating the component all the time even if we don't even use it. private void UpdateDamagedAccent(Entity ent) { if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) @@ -230,10 +250,10 @@ private void UpdateDamagedAccent(Entity ent) return; if (TryComp(ent, out var battery)) - accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge; + accent.OverrideChargeLevel = _battery.GetChargeLevel((ent.Owner, battery)); if (TryComp(ent, out var damageable)) - accent.OverrideTotalDamage = damageable.TotalDamage; + accent.OverrideTotalDamage = _damageable.GetTotalDamage((ent, damageable)); if (TryComp(ent, out var destructible)) accent.DamageAtMaxCorruption = _destructible.DestroyedAt(ent, destructible); @@ -252,7 +272,7 @@ private void UpdateBatteryAlert(Entity ent) if (!_proto.TryIndex(_batteryAlert, out var proto)) return; - var chargePercent = battery.CurrentCharge / battery.MaxCharge; + var chargePercent = _battery.GetChargeLevel((ent.Owner, battery)); var chargeLevel = Math.Round(chargePercent * proto.MaxSeverity); _alerts.ShowAlert(held.Value, _batteryAlert, (short)Math.Clamp(chargeLevel, 0, proto.MaxSeverity)); @@ -266,7 +286,7 @@ private void UpdateBatteryAlert(Entity ent) } } - private void UpdateCoreIntegrityAlert(Entity ent) + private void UpdateCoreIntegrityAlert(Entity ent, bool damageIncreased = false) { if (!TryComp(ent, out var damageable)) return; @@ -280,10 +300,16 @@ private void UpdateCoreIntegrityAlert(Entity ent) if (!_proto.TryIndex(_damageAlert, out var proto)) return; - var damagePercent = damageable.TotalDamage / _destructible.DestroyedAt(ent, destructible); + var damagePercent = _damageable.GetTotalDamage((ent, damageable)) / _destructible.DestroyedAt(ent, destructible); var damageLevel = Math.Round(damagePercent.Float() * proto.MaxSeverity); _alerts.ShowAlert(held.Value, _damageAlert, (short)Math.Clamp(damageLevel, 0, proto.MaxSeverity)); + + if (damageIncreased) + { + var ev = new ChatNotificationEvent(_aiTakingDamageChatNotificationPrototype, ent); + RaiseLocalEvent(held.Value, ref ev); + } } private void OnDoAfterAttempt(Entity ent, ref DoAfterAttemptEvent args) diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs index 5c6d50ffd37..f6ea77d88c6 100644 --- a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs @@ -12,6 +12,7 @@ using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; +using Robust.Shared.Utility; namespace Content.Server.Singularity.EntitySystems; @@ -152,32 +153,62 @@ private void OnComponentRemoved(Entity gener /// /// Deletes the fields and removes the respective connections for the generators. + /// If a predicate is provided, only remove fields and connections if the predicate returns true. /// - private void RemoveConnections(Entity generator) + /// The field generator component + /// An optional predicate that takes in this generator entity and the other generator entity. + /// It should return true if the connection should be removed, and false otherwise. + /// If a predicate isn't provided, all connections will be removed. + private void RemoveConnections( + Entity generator, + Func, Entity, bool>? removePredicate = null) { var (uid, component) = generator; - foreach (var (direction, value) in component.Connections) + var anyFieldsRemoved = false; + + foreach (var (direction, (otherGen, fields)) in component.Connections) { - foreach (var field in value.Item2) + if (removePredicate is not null && !removePredicate(generator, otherGen)) + { + // Do not delete a connection only if the provided predicate says not to. + continue; + } + + anyFieldsRemoved = true; + + foreach (var field in fields) { QueueDel(field); } - value.Item1.Comp.Connections.Remove(direction.GetOpposite()); - if (value.Item1.Comp.Connections.Count == 0) //Change isconnected only if there's no more connections + component.Connections.Remove(direction); + + otherGen.Comp.Connections.Remove(direction.GetOpposite()); + + if (otherGen.Comp.Connections.Count == 0) //Change isconnected only if there's no more connections { - value.Item1.Comp.IsConnected = false; - ChangeOnLightVisualizer(value.Item1); + otherGen.Comp.IsConnected = false; + ChangeOnLightVisualizer(otherGen); } - ChangeFieldVisualizer(value.Item1); + ChangeFieldVisualizer(otherGen); + } + + if (!anyFieldsRemoved) + { + // No fields were removed, so no logging or other updates are necessary. + return; + } + + _popupSystem.PopupEntity(Loc.GetString("comp-containment-disconnected"), uid, PopupType.LargeCaution); + + if (component.Connections.Count == 0) + { + component.IsConnected = false; + ChangeOnLightVisualizer(generator); } - component.Connections.Clear(); - if (component.IsConnected) - _popupSystem.PopupEntity(Loc.GetString("comp-containment-disconnected"), uid, PopupType.LargeCaution); - component.IsConnected = false; - ChangeOnLightVisualizer(generator); ChangeFieldVisualizer(generator); + _adminLogger.Add(LogType.FieldGeneration, LogImpact.Medium, $"{ToPrettyString(uid)} lost field connections"); // Ideally LogImpact would depend on if there is a singulo nearby } @@ -199,7 +230,7 @@ public void ReceivePower(int power, Entity g if (component.PowerBuffer >= component.PowerMinimum) { var directions = Enum.GetValues().Length; - for (int i = 0; i < directions-1; i+=2) + for (int i = 0; i < directions - 1; i += 2) { var dir = (Direction)i; @@ -220,7 +251,9 @@ public void LosePower(Entity generator, int if (component.PowerBuffer < component.PowerMinimum && component.Connections.Count != 0) { - RemoveConnections(generator); + // Only remove connections if the generators on BOTH sides of the field don't have enough power. + // Since we only run this code if we know this gen doesn't have enough power, we only have to check the other gen. + RemoveConnections(generator, (_, otherGen) => otherGen.Comp.PowerBuffer < otherGen.Comp.PowerMinimum); } ChangePowerVisualizer(power, generator); @@ -393,7 +426,7 @@ private void ChangeFieldVisualizer(Entity ge { _visualizer.SetData(generator, ContainmentFieldGeneratorVisuals.FieldLight, generator.Comp.Connections.Count switch { - >1 => FieldLevelVisuals.MultipleFields, + > 1 => FieldLevelVisuals.MultipleFields, 1 => FieldLevelVisuals.OneField, _ => generator.Comp.Enabled ? FieldLevelVisuals.On : FieldLevelVisuals.NoLevel }); @@ -402,6 +435,7 @@ private void ChangeFieldVisualizer(Entity ge private void ChangeOnLightVisualizer(Entity generator) { _visualizer.SetData(generator, ContainmentFieldGeneratorVisuals.OnLight, generator.Comp.IsConnected); + UpdateConnectionLights(generator); } #endregion diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index 2ccdc109343..837ba05df1f 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -4,9 +4,14 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Projectiles; +using Content.Server.Pinpointer; +using Content.Server.Radio.EntitySystems; using Content.Server.Weapons.Ranged.Systems; +using Content.Shared.Construction; using Content.Shared.Database; +using Content.Shared.Destructible; using Content.Shared.DeviceLinking.Events; +using Content.Shared.Emag.Systems; using Content.Shared.Interaction; using Content.Shared.Lock; using Content.Shared.Popups; @@ -33,6 +38,8 @@ public sealed class EmitterSystem : SharedEmitterSystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly ProjectileSystem _projectile = default!; [Dependency] private readonly GunSystem _gun = default!; + [Dependency] private readonly RadioSystem _radio = default!; + [Dependency] private readonly NavMapSystem _navMap = default!; public override void Initialize() { @@ -43,6 +50,9 @@ public override void Initialize() SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnAnchorStateChanged); SubscribeLocalEvent(OnSignalReceived); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent(OnDeconstructed); // you shouldn't be able to deconstruct locked emitters but out of scope to fix + SubscribeLocalEvent(OnLockToggled); } private void OnAnchorStateChanged(EntityUid uid, EmitterComponent component, ref AnchorStateChangedEvent args) @@ -80,9 +90,10 @@ private void OnActivate(EntityUid uid, EmitterComponent component, ActivateInWor ("target", uid)), uid, args.User); } + var stateText = component.IsOn ? "on" : "off"; _adminLogger.Add(LogType.FieldGeneration, component.IsOn ? LogImpact.Medium : LogImpact.High, - $"{ToPrettyString(args.User):player} toggled {ToPrettyString(uid):emitter}"); + $"{ToPrettyString(args.User):player} toggled {ToPrettyString(uid):emitter} to {stateText}"); args.Handled = true; } else @@ -163,6 +174,8 @@ public void PowerOff(EntityUid uid, EmitterComponent component) return; } + AlertRadio((uid, component), component.LocUnpowered); + component.IsPowered = false; // Must be set while emitter powered. @@ -232,7 +245,7 @@ private void Fire(EntityUid uid, EmitterComponent component) var targetPos = new EntityCoordinates(uid, new Vector2(0, -1)); - _gun.Shoot(uid, gunComponent, ent, xform.Coordinates, targetPos, out _); + _gun.Shoot((uid, gunComponent), ent, xform.Coordinates, targetPos, out _); } private void UpdateAppearance(EntityUid uid, EmitterComponent component) @@ -283,5 +296,37 @@ private void OnSignalReceived(EntityUid uid, EmitterComponent component, ref Sig component.BoltType = boltType; } } + + private void OnDestruction(Entity ent, ref DestructionEventArgs args) + { + // Engineering needs to know if an emitter is destroyed so they can replace it before the engine looses. + AlertRadio(ent, ent.Comp.LocDestroyed); + } + + private void OnDeconstructed(Entity ent, ref MachineDeconstructedEvent args) + { + // right now you don't even need to unlock the emitter to deconstruct it. that's almost certainly a bug but even without it it probably still needs an alert + AlertRadio(ent, ent.Comp.LocDeconstructed); + } + + private void AlertRadio(Entity ent, string locString) + { + if (!ent.Comp.AlertRadio || !ent.Comp.IsOn || !ent.Comp.IsPowered) + return; // APEs do not need to scream over engineering radio, and an emitter that is off is probably not going to be alerting radios + + var message = Loc.GetString( + locString, + ("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString(ent.Owner))) + ); + _radio.SendRadioMessage(ent.Owner, message, ent.Comp.RadioChannel, ent.Owner); + } + + private void OnLockToggled(Entity ent, ref LockToggledEvent args) + { + if (args.Locked) + return; + + AlertRadio(ent, ent.Comp.LocUnlocked); + } } } diff --git a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs index 6f2137b0d0a..bf639746bad 100644 --- a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs +++ b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs @@ -73,9 +73,6 @@ public override void Shutdown() /// The time elapsed since the last set of updates. public override void Update(float frameTime) { - if(!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var gravWell, out var xform)) { diff --git a/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs index 4b31d5f8144..099b2d89b06 100644 --- a/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularityAttractorSystem.cs @@ -36,9 +36,6 @@ public override void Initialize() /// The time elapsed since the last set of updates. public override void Update(float frameTime) { - if (!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); var now = _timing.CurTime; while (query.MoveNext(out var uid, out var attractor, out var xform)) diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs index 8e33410561c..e303717e9ae 100644 --- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs @@ -17,7 +17,6 @@ public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSyste [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly MetaDataSystem _metadata = default!; #endregion Dependencies public override void Initialize() @@ -53,6 +52,9 @@ private void OnPassThreshold(EntityUid uid, SingularityGeneratorComponent? comp) return; SetPower(uid, 0, comp); + + // Other particle entities from the same wave could trigger additional teslas to spawn, so we must block the generator + comp.Inert = true; Spawn(comp.SpawnPrototype, Transform(uid).Coordinates); } @@ -112,7 +114,8 @@ private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent co if (!TryComp(args.OtherEntity, out var generatorComp)) return; - if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled) + if (generatorComp.Inert || + _timing.CurTime < generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled) { QueueDel(uid); return; @@ -176,9 +179,10 @@ private bool CheckContainmentField(Direction dir, EntityThe amount of time since the last set of updates. public override void Update(float frameTime) { - if(!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var singularity)) { diff --git a/Content.Server/SmartFridge/SmartFridgeSystem.cs b/Content.Server/SmartFridge/SmartFridgeSystem.cs new file mode 100644 index 00000000000..ec9646028b2 --- /dev/null +++ b/Content.Server/SmartFridge/SmartFridgeSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.SmartFridge; + +namespace Content.Server.SmartFridge; + +public sealed class SmartFridgeSystem : SharedSmartFridgeSystem; diff --git a/Content.Server/Sound/EmitSoundSystem.cs b/Content.Server/Sound/EmitSoundSystem.cs index 1720d67d024..38878b147fc 100644 --- a/Content.Server/Sound/EmitSoundSystem.cs +++ b/Content.Server/Sound/EmitSoundSystem.cs @@ -1,14 +1,12 @@ using Content.Shared.Sound; using Content.Shared.Sound.Components; using Robust.Shared.Timing; -using Robust.Shared.Network; namespace Content.Server.Sound; public sealed class EmitSoundSystem : SharedEmitSoundSystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; public override void Update(float frameTime) { @@ -49,9 +47,6 @@ private void HandleSpamEmitSoundMapInit(Entity entity, r private void SpamEmitSoundReset(Entity entity) { - if (_net.IsClient) - return; - entity.Comp.NextSound = _timing.CurTime + ((entity.Comp.MinInterval < entity.Comp.MaxInterval) ? Random.Next(entity.Comp.MinInterval, entity.Comp.MaxInterval) : entity.Comp.MaxInterval); diff --git a/Content.Server/Spawners/Components/GridSpawnPointWhitelistComponent.cs b/Content.Server/Spawners/Components/GridSpawnPointWhitelistComponent.cs new file mode 100644 index 00000000000..de9f71f0f81 --- /dev/null +++ b/Content.Server/Spawners/Components/GridSpawnPointWhitelistComponent.cs @@ -0,0 +1,22 @@ +using Content.Shared.Whitelist; + +namespace Content.Server.Spawners.Components; + +/// +/// Defines whitelist and blacklist for entities that can spawn at a spawnpoint when they are spawned via the +/// +[RegisterComponent] +public sealed partial class GridSpawnPointWhitelistComponent : Component +{ + /// + /// Whitelist of entities that can be spawned at this SpawnPoint + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Whitelist of entities that can't be spawned at this SpawnPoint + /// + [DataField] + public EntityWhitelist? Blacklist; +} diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs index 96deb70b47b..2a394eba227 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -127,15 +127,16 @@ private void Spawn(Entity ent) return; var coords = Transform(ent).Coordinates; + var offset = ent.Comp.Offset; var spawns = _entityTable.GetSpawns(ent.Comp.Table); foreach (var proto in spawns) { - var xOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset); - var yOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset); + var xOffset = _robustRandom.NextFloat(-offset, offset); + var yOffset = _robustRandom.NextFloat(-offset, offset); var trueCoords = coords.Offset(new Vector2(xOffset, yOffset)); - SpawnAtPosition(proto, trueCoords); + SpawnAttachedTo(proto, trueCoords); } } } diff --git a/Content.Server/Species/Systems/NymphSystem.cs b/Content.Server/Species/Systems/NymphSystem.cs index 11493b65b3e..6d2d56465eb 100644 --- a/Content.Server/Species/Systems/NymphSystem.cs +++ b/Content.Server/Species/Systems/NymphSystem.cs @@ -1,33 +1,28 @@ using Content.Server.Mind; +using Content.Server.Zombies; +using Content.Shared.Body; using Content.Shared.Species.Components; -using Content.Shared.Body.Events; using Content.Shared.Zombies; -using Content.Server.Zombies; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; namespace Content.Server.Species.Systems; public sealed partial class NymphSystem : EntitySystem { - [Dependency] private readonly IPrototypeManager _protoManager= default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ZombieSystem _zombie = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnRemovedFromPart); + SubscribeLocalEvent(OnRemovedFromPart); } - private void OnRemovedFromPart(EntityUid uid, NymphComponent comp, ref OrganRemovedFromBodyEvent args) + private void OnRemovedFromPart(EntityUid uid, NymphComponent comp, ref OrganGotRemovedEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - - if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(args.OldBody)) + if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(args.Target)) return; if (!_protoManager.TryIndex(comp.EntityPrototype, out var entityProto)) @@ -37,11 +32,11 @@ private void OnRemovedFromPart(EntityUid uid, NymphComponent comp, ref OrganRemo var coords = Transform(uid).Coordinates; var nymph = SpawnAtPosition(entityProto.ID, coords); - if (HasComp(args.OldBody)) // Zombify the new nymph if old one is a zombie + if (HasComp(args.Target)) // Zombify the new nymph if old one is a zombie _zombie.ZombifyEntity(nymph); // Move the mind if there is one and it's supposed to be transferred - if (comp.TransferMind == true && _mindSystem.TryGetMind(args.OldBody, out var mindId, out var mind)) + if (comp.TransferMind && _mindSystem.TryGetMind(uid, out var mindId, out var mind)) _mindSystem.TransferTo(mindId, nymph, mind: mind); // Delete the old organ diff --git a/Content.Server/Speech/AccentSystem.cs b/Content.Server/Speech/AccentSystem.cs index e413dd52f8a..6f89a7d14d1 100644 --- a/Content.Server/Speech/AccentSystem.cs +++ b/Content.Server/Speech/AccentSystem.cs @@ -1,5 +1,5 @@ using System.Text.RegularExpressions; -using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; namespace Content.Server.Speech; @@ -15,6 +15,9 @@ public override void Initialize() private void AccentHandler(TransformSpeechEvent args) { + if (args.Cancelled) + return; + var accentEvent = new AccentGetEvent(args.Sender, args.Message); RaiseLocalEvent(args.Sender, accentEvent, true); diff --git a/Content.Server/Speech/Components/EmoteBlockerComponent.cs b/Content.Server/Speech/Components/EmoteBlockerComponent.cs deleted file mode 100644 index 603fd1b2588..00000000000 --- a/Content.Server/Speech/Components/EmoteBlockerComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Chat.Prototypes; -using Robust.Shared.Prototypes; - -namespace Content.Server.Speech.Components; - -/// -/// Suppresses emotes with the given categories or ID. -/// Additionally, if the Scream Emote would be blocked, also blocks the Scream Action. -/// -[RegisterComponent] -public sealed partial class EmoteBlockerComponent : Component -{ - /// - /// Which categories of emotes are blocked by this component. - /// - [DataField] - public HashSet BlocksCategories = []; - - /// - /// IDs of which specific emotes are blocked by this component. - /// - [DataField] - public HashSet> BlocksEmotes = []; -} diff --git a/Content.Server/Speech/EmotesMenuSystem.cs b/Content.Server/Speech/EmotesMenuSystem.cs index 5f91742eb12..6571587b916 100644 --- a/Content.Server/Speech/EmotesMenuSystem.cs +++ b/Content.Server/Speech/EmotesMenuSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.Chat.Systems; -using Content.Shared.Chat; +using Content.Shared.Chat; +using Content.Server.Chat.Systems; using Robust.Shared.Prototypes; namespace Content.Server.Speech; diff --git a/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs b/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs index 927b1259adb..27e42c70edd 100644 --- a/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/BarkAccentSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.StatusEffectNew; using Content.Server.Speech.Components; using Content.Shared.Speech; using Robust.Shared.Random; @@ -23,6 +24,7 @@ public sealed class BarkAccentSystem : EntitySystem public override void Initialize() { SubscribeLocalEvent(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -36,9 +38,14 @@ public string Accentuate(string message) .Replace("l", "r").Replace("L", "R"); } - private void OnAccent(EntityUid uid, BarkAccentComponent component, AccentGetEvent args) + private void OnAccent(Entity entity, ref AccentGetEvent args) { args.Message = Accentuate(args.Message); } + + private void OnAccentRelayed(Entity entity, ref StatusEffectRelayedEvent args) + { + args.Args.Message = Accentuate(args.Args.Message); + } } } diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs index d4a37a07cd6..e197f2a50bf 100644 --- a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs @@ -1,9 +1,11 @@ using System.Text; using Content.Server.Destructible; -using Content.Server.PowerCell; using Content.Shared.Speech.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; +using Content.Shared.Power.EntitySystems; +using Content.Shared.PowerCell; using Content.Shared.Speech; using Robust.Shared.Random; @@ -12,8 +14,10 @@ namespace Content.Server.Speech.EntitySystems; public sealed class DamagedSiliconAccentSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly DestructibleSystem _destructibleSystem = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public override void Initialize() { @@ -34,7 +38,7 @@ private void OnAccent(Entity ent, ref AccentGetEv } else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) { - currentChargeLevel = battery.CurrentCharge / battery.MaxCharge; + currentChargeLevel = _battery.GetChargeLevel(battery.Value.AsNullable()); } currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f); // Corrupt due to low power (drops characters on longer messages) @@ -50,7 +54,7 @@ private void OnAccent(Entity ent, ref AccentGetEv } else if (TryComp(uid, out var damageable)) { - damage = damageable.TotalDamage; + damage = _damageable.GetTotalDamage((uid, damageable)); } // Corrupt due to damage (drop, repeat, replace with symbols) args.Message = CorruptDamage(args.Message, damage, ent); diff --git a/Content.Server/Speech/EntitySystems/FrontalLispSystem.cs b/Content.Server/Speech/EntitySystems/FrontalLispSystem.cs index 20823fd7993..b3f5ff53dc8 100644 --- a/Content.Server/Speech/EntitySystems/FrontalLispSystem.cs +++ b/Content.Server/Speech/EntitySystems/FrontalLispSystem.cs @@ -1,6 +1,5 @@ using System.Text.RegularExpressions; using Content.Server.Speech.Components; -using Robust.Shared.Random; // Corvax-Localization using Content.Shared.Speech; namespace Content.Server.Speech.EntitySystems; @@ -20,8 +19,6 @@ public override void Initialize() SubscribeLocalEvent(OnAccent); } - [Dependency] private readonly IRobustRandom _random = default!; // Corvax-Localization - private void OnAccent(EntityUid uid, FrontalLispComponent component, AccentGetEvent args) { var message = args.Message; @@ -33,24 +30,6 @@ private void OnAccent(EntityUid uid, FrontalLispComponent component, AccentGetEv message = RegexUpperEcks.Replace(message, "EKTH"); message = RegexLowerEcks.Replace(message, "ekth"); - // Corvax-Localization Start - // с - ш - message = Regex.Replace(message, @"с", _random.Prob(0.90f) ? "ш" : "с"); - message = Regex.Replace(message, @"С", _random.Prob(0.90f) ? "Ш" : "С"); - // ч - ш - message = Regex.Replace(message, @"ч", _random.Prob(0.90f) ? "ш" : "ч"); - message = Regex.Replace(message, @"Ч", _random.Prob(0.90f) ? "Ш" : "Ч"); - // ц - ч - message = Regex.Replace(message, @"ц", _random.Prob(0.90f) ? "ч" : "ц"); - message = Regex.Replace(message, @"Ц", _random.Prob(0.90f) ? "Ч" : "Ц"); - // т - ч - message = Regex.Replace(message, @"\B[т](?![АЕЁИОУЫЭЮЯаеёиоуыэюя])", _random.Prob(0.90f) ? "ч" : "т"); - message = Regex.Replace(message, @"\B[Т](?![АЕЁИОУЫЭЮЯаеёиоуыэюя])", _random.Prob(0.90f) ? "Ч" : "Т"); - // з - ж - message = Regex.Replace(message, @"з", _random.Prob(0.90f) ? "ж" : "з"); - message = Regex.Replace(message, @"З", _random.Prob(0.90f) ? "Ж" : "З"); - // Corvax-Localization End - args.Message = message; } } diff --git a/Content.Server/Speech/EntitySystems/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index 17513d80e7a..35cb7f0eb45 100644 --- a/Content.Server/Speech/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/ListeningSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; using Content.Shared.Speech.Components; diff --git a/Content.Server/Speech/EntitySystems/MothAccentSystem.cs b/Content.Server/Speech/EntitySystems/MothAccentSystem.cs index 0a6e28e4fdb..3bf46459ace 100644 --- a/Content.Server/Speech/EntitySystems/MothAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/MothAccentSystem.cs @@ -1,13 +1,11 @@ using System.Text.RegularExpressions; using Content.Server.Speech.Components; -using Robust.Shared.Random; using Content.Shared.Speech; namespace Content.Server.Speech.EntitySystems; public sealed class MothAccentSystem : EntitySystem { - [Dependency] private readonly IRobustRandom _random = default!; // Corvax-Localization private static readonly Regex RegexLowerBuzz = new Regex("z{1,3}"); private static readonly Regex RegexUpperBuzz = new Regex("Z{1,3}"); @@ -26,33 +24,6 @@ private void OnAccent(EntityUid uid, MothAccentComponent component, AccentGetEve // buZZZ message = RegexUpperBuzz.Replace(message, "ZZZ"); - // Corvax-Localization-Start - // ж => жжж - message = Regex.Replace( - message, - "ж+", - _random.Pick(new List() { "жж", "жжж" }) - ); - // Ж => ЖЖЖ - message = Regex.Replace( - message, - "Ж+", - _random.Pick(new List() { "ЖЖ", "ЖЖЖ" }) - ); - // з => ссс - message = Regex.Replace( - message, - "з+", - _random.Pick(new List() { "зз", "ззз" }) - ); - // З => CCC - message = Regex.Replace( - message, - "З+", - _random.Pick(new List() { "ЗЗ", "ЗЗЗ" }) - ); - // Corvax-Localization-End - args.Message = message; } } diff --git a/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs b/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs index 27679c91346..868e5b73c1e 100644 --- a/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Speech.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Speech; using Content.Shared.Speech.Components; diff --git a/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs b/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs index ff930e524ab..804c711ea7f 100644 --- a/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/OwOAccentSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Speech.Components; using Content.Shared.Speech; +using Content.Shared.StatusEffectNew; using Robust.Shared.Random; namespace Content.Server.Speech.EntitySystems @@ -20,6 +21,7 @@ public sealed class OwOAccentSystem : EntitySystem public override void Initialize() { SubscribeLocalEvent(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -34,9 +36,15 @@ public string Accentuate(string message) .Replace("l", "w").Replace("L", "W"); } - private void OnAccent(EntityUid uid, OwOAccentComponent component, AccentGetEvent args) + private void OnAccent(Entity entity, ref AccentGetEvent args) { args.Message = Accentuate(args.Message); } + + private void OnAccentRelayed(Entity entity, ref StatusEffectRelayedEvent args) + { + args.Args.Message = Accentuate(args.Args.Message); + } + } } diff --git a/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs b/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs index c285063d2d9..60b531caa50 100644 --- a/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs +++ b/Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs @@ -128,7 +128,7 @@ public string ApplyReplacements(string message, string accent) var firstLoc = _loc.GetString(first); var replaceLoc = _loc.GetString(replace); - var regex = new Regex($@"(?(OnAccent); + SubscribeLocalEvent>(OnAccentRelayed); } public string Accentuate(string message) @@ -41,9 +43,14 @@ public string Accentuate(string message) return msg; } - private void OnAccent(EntityUid uid, ScrambledAccentComponent component, AccentGetEvent args) + private void OnAccent(Entity entity, ref AccentGetEvent args) { args.Message = Accentuate(args.Message); } + + private void OnAccentRelayed(Entity entity, ref StatusEffectRelayedEvent args) + { + args.Args.Message = Accentuate(args.Args.Message); + } } } diff --git a/Content.Server/Speech/EntitySystems/SlurredSystem.cs b/Content.Server/Speech/EntitySystems/SlurredSystem.cs index 8690079de1d..34cd0504394 100644 --- a/Content.Server/Speech/EntitySystems/SlurredSystem.cs +++ b/Content.Server/Speech/EntitySystems/SlurredSystem.cs @@ -15,6 +15,16 @@ public sealed class SlurredSystem : SharedSlurredSystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IGameTiming _timing = default!; + /// + /// Divisor applied to total seconds used to get the odds of slurred speech occuring. + /// + private const float SlurredModifier = 1100f; + + /// + /// Minimum amount of time on the slurred accent for it to start taking effect. + /// + private const float SlurredThreshold = 80f; + public override void Initialize() { SubscribeLocalEvent(OnAccent); @@ -23,23 +33,18 @@ public override void Initialize() } /// - /// Slur chance scales with "drunkeness", which is just measured using the time remaining on the status effect. + /// Slur chance scales with the time remaining on any status effect with the SlurredAccentComponent. + /// Typically, this is equivalent to "drunkenness" on the DrunkStatusEffect /// private float GetProbabilityScale(EntityUid uid) { - if (!_status.TryGetMaxTime(uid, out var time)) + if (!_status.TryGetMaxTime(uid, out var time)) return 0; // This is a magic number. Why this value? No clue it was made 3 years before I refactored this. - var magic = SharedDrunkSystem.MagicNumber; - - if (time.Item2 != null) - { - var curTime = _timing.CurTime; - magic = (float) (time.Item2 - curTime).Value.TotalSeconds - 80f; - } + var magic = time.Item2 == null ? SlurredModifier : (float) (time.Item2 - _timing.CurTime).Value.TotalSeconds - SlurredThreshold; - return Math.Clamp(magic / SharedDrunkSystem.MagicNumber, 0f, 1f); + return Math.Clamp(magic / SlurredModifier, 0f, 1f); } private void OnAccent(Entity entity, ref AccentGetEvent args) diff --git a/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs b/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs index ba7043e10c5..6b52e69d7af 100644 --- a/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs +++ b/Content.Server/Speech/EntitySystems/SpeakOnActionSystem.cs @@ -1,10 +1,10 @@ using Content.Server.Chat.Systems; -using Content.Shared.Speech.Components; +using Content.Shared.Actions.Events; +using Content.Shared.Chat; using Content.Shared.Speech; +using Content.Shared.Speech.Components; using Content.Shared.Speech.EntitySystems; using Content.Shared.Speech.Muting; -using Content.Shared.Actions.Events; - namespace Content.Server.Speech.EntitySystems; diff --git a/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs b/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs index 35a77c31f64..6d5f165d6c4 100644 --- a/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs +++ b/Content.Server/Speech/EntitySystems/UnblockableSpeechSystem.cs @@ -1,5 +1,5 @@ -using Content.Server.Chat.Systems; using Content.Server.Speech.Components; +using Content.Shared.Chat; namespace Content.Server.Speech.EntitySystems { diff --git a/Content.Server/Speech/EntitySystems/VocalSystem.cs b/Content.Server/Speech/EntitySystems/VocalSystem.cs index 275140ff5b3..bd6eee63341 100644 --- a/Content.Server/Speech/EntitySystems/VocalSystem.cs +++ b/Content.Server/Speech/EntitySystems/VocalSystem.cs @@ -1,12 +1,11 @@ using Content.Server.Actions; using Content.Server.Chat.Systems; -using Content.Server.Speech.Components; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Cloning.Events; using Content.Shared.Humanoid; using Content.Shared.Speech; using Content.Shared.Speech.Components; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -119,7 +118,7 @@ private void LoadSounds(EntityUid uid, VocalComponent component, Sex? sex = null if (component.Sounds == null) return; - sex ??= CompOrNull(uid)?.Sex ?? Sex.Unsexed; + sex ??= CompOrNull(uid)?.Sex ?? Sex.Unsexed; if (!component.Sounds.TryGetValue(sex.Value, out var protoId)) return; diff --git a/Content.Server/Speech/Muting/MutingSystem.cs b/Content.Server/Speech/Muting/MutingSystem.cs index f588e2238d6..9e2a0602a43 100644 --- a/Content.Server/Speech/Muting/MutingSystem.cs +++ b/Content.Server/Speech/Muting/MutingSystem.cs @@ -1,8 +1,7 @@ -using Content.Shared.Abilities.Mime; -using Content.Server.Chat.Systems; using Content.Server.Popups; -using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; +using Content.Shared.Abilities.Mime; +using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Puppet; using Content.Shared.Speech; diff --git a/Content.Server/Speech/SpeechNoiseSystem.cs b/Content.Server/Speech/SpeechNoiseSystem.cs index 5530f3fe57a..cc9d5feb607 100644 --- a/Content.Server/Speech/SpeechNoiseSystem.cs +++ b/Content.Server/Speech/SpeechNoiseSystem.cs @@ -1,12 +1,10 @@ -using Robust.Shared.Audio; -using Content.Server.Chat; -using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Speech; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Server.Speech { diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs index 24ab5e0ea29..1216d9758a8 100644 --- a/Content.Server/SprayPainter/SprayPainterSystem.cs +++ b/Content.Server/SprayPainter/SprayPainterSystem.cs @@ -15,7 +15,8 @@ using Content.Shared.SprayPainter.Components; using Robust.Server.Audio; using Robust.Server.GameObjects; -using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; namespace Content.Server.SprayPainter; @@ -48,7 +49,16 @@ public override void Initialize() /// private void OnFloorAfterInteract(Entity ent, ref AfterInteractEvent args) { - if (args.Handled || !args.CanReach || args.Target != null) + if (args.Handled || args.Target != null) + return; + + if (ent.Comp.ColorPickerEnabled) + { + PickColor(ent, ref args); + return; + } + + if (!args.CanReach) return; // Includes both off and all other don't cares @@ -56,7 +66,7 @@ private void OnFloorAfterInteract(Entity ent, ref AfterIn return; args.Handled = true; - if (TryComp(ent, out LimitedChargesComponent? charges) && charges.LastCharges < ent.Comp.DecalChargeCost) + if (TryComp(ent, out LimitedChargesComponent? charges) && _charges.GetCurrentCharges((ent, charges)) < ent.Comp.DecalChargeCost) { _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-charges"), args.User, args.User); return; @@ -83,7 +93,7 @@ private void OnFloorAfterInteract(Entity ent, ref AfterIn return; } - var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalRemovable); + var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalValid); if (decals.Count <= 0) { _popup.PopupEntity(Loc.GetString("spray-painter-interact-nothing-to-remove"), args.User, args.User); @@ -100,14 +110,13 @@ private void OnFloorAfterInteract(Entity ent, ref AfterIn _charges.TryUseCharges((ent, charges), ent.Comp.DecalChargeCost); - AdminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{EntityManager.ToPrettyString(args.User):user} painted a {ent.Comp.SelectedDecal}"); + AdminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{ToPrettyString(args.User):user} painted a {ent.Comp.SelectedDecal}"); } /// - /// Handles drawing decals when a spray painter is used to interact with the floor. - /// Spray painter must have decal painting enabled and enough charges of paint to paint on the floor. + /// Returns whether is valid to interact with when a spray painter is used to interact with the floor. /// - private bool IsDecalRemovable(Decal decal) + private bool IsDecalValid(Decal decal) { if (!Proto.TryIndex(decal.Id, out var decalProto)) return false; @@ -165,7 +174,7 @@ private void OnPipeInteract(Entity ent, ref InteractUsi return; if (TryComp(args.Used, out var charges) - && charges.LastCharges < painter.PipeChargeCost) + && _charges.GetCurrentCharges((args.Used, charges)) < painter.PipeChargeCost) { var msg = Loc.GetString("spray-painter-interact-no-charges"); _popup.PopupEntity(msg, args.User, args.User); @@ -189,4 +198,26 @@ private void OnPipeInteract(Entity ent, ref InteractUsi args.Handled = DoAfter.TryStartDoAfter(doAfterEventArgs); } + + private void PickColor(Entity ent, ref AfterInteractEvent args) + { + if (!args.ClickLocation.IsValid(EntityManager) || _transform.GetGrid(args.ClickLocation) is not { } grid) + return; + + var clickPos = args.ClickLocation.Position; + var decals = _decals.GetDecalsInRange(grid, clickPos, validDelegate: IsDecalValid); + if (decals.Count == 0) + { + _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-color-pick"), args.User, args.User); + return; + } + + var closestDecal = decals.MinBy(d => Vector2.Distance(d.Decal.Coordinates, clickPos)).Decal; + + _popup.PopupEntity(Loc.GetString("spray-painter-interact-color-picked", ("id", closestDecal.Id)), args.User, args.User); + + ent.Comp.SelectedDecalColor = closestDecal.Color; + ent.Comp.ColorPickerEnabled = false; + Dirty(ent); + } } diff --git a/Content.Server/Spreader/KudzuSystem.cs b/Content.Server/Spreader/KudzuSystem.cs index fbc809c15b7..687ee9dd572 100644 --- a/Content.Server/Spreader/KudzuSystem.cs +++ b/Content.Server/Spreader/KudzuSystem.cs @@ -1,4 +1,5 @@ -using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Spreader; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -29,7 +30,7 @@ private void OnDamageChanged(EntityUid uid, KudzuComponent component, DamageChan { // Every time we take any damage, we reduce growth depending on all damage over the growth impact // So the kudzu gets slower growing the more it is hurt. - var growthDamage = (int) (args.Damageable.TotalDamage / component.GrowthHealth); + var growthDamage = (int) (_damageable.GetTotalDamage((uid, args.Damageable)) / component.GrowthHealth); if (growthDamage > 0) { if (!EnsureComp(uid, out _)) @@ -117,14 +118,15 @@ public override void Update(float frameTime) if (damageableQuery.TryGetComponent(uid, out var damage)) { - if (damage.TotalDamage > 1.0) + var totalDamage = _damageable.GetTotalDamage((uid, damage)); + if (totalDamage > 1.0) { if (kudzu.DamageRecovery != null) { // This kudzu features healing, so Gradually heal _damageable.TryChangeDamage(uid, kudzu.DamageRecovery, true); } - if (damage.TotalDamage >= kudzu.GrowthBlock) + if (totalDamage >= kudzu.GrowthBlock) { // Don't grow when quite damaged if (_robustRandom.Prob(0.95f)) diff --git a/Content.Server/Spreader/SpreaderSystem.cs b/Content.Server/Spreader/SpreaderSystem.cs index 2bc067542d4..7ea41431da5 100644 --- a/Content.Server/Spreader/SpreaderSystem.cs +++ b/Content.Server/Spreader/SpreaderSystem.cs @@ -295,35 +295,38 @@ public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId - public void ActivateSpreadableNeighbors(EntityUid uid, (EntityUid Grid, Vector2i Tile)? position = null) + public void ActivateSpreadableNeighbors(EntityUid origin, (EntityUid Grid, Vector2i Tile)? position = null) { Vector2i tile; - EntityUid ent; - MapGridComponent? grid; + EntityUid gridUid; + MapGridComponent? gridComp; if (position == null) { - var transform = Transform(uid); - if (!TryComp(transform.GridUid, out grid) || TerminatingOrDeleted(transform.GridUid.Value)) + var transform = Transform(origin); + if (!TryComp(transform.GridUid, out gridComp) || TerminatingOrDeleted(transform.GridUid.Value)) return; - tile = _map.TileIndicesFor(transform.GridUid.Value, grid, transform.Coordinates); - ent = transform.GridUid.Value; + tile = _map.TileIndicesFor(transform.GridUid.Value, gridComp, transform.Coordinates); + gridUid = transform.GridUid.Value; } else { - if (!TryComp(position.Value.Grid, out grid)) + if (!TryComp(position.Value.Grid, out gridComp)) return; - (ent, tile) = position.Value; + (gridUid, tile) = position.Value; } - var anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, tile); + var anchored = _map.GetAnchoredEntitiesEnumerator(gridUid, gridComp, tile); while (anchored.MoveNext(out var entity)) { - if (entity == ent) + // Don't re-activate the terminating entity + if (entity == origin) continue; DebugTools.Assert(Transform(entity.Value).Anchored); - if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value)) + + // Activate any edge spreaders that are non-terminating + if (_query.HasComponent(entity) && !TerminatingOrDeleted(entity)) EnsureComp(entity.Value); } @@ -331,12 +334,14 @@ public void ActivateSpreadableNeighbors(EntityUid uid, (EntityUid Grid, Vector2i { var direction = (AtmosDirection) (1 << i); var adjacentTile = SharedMapSystem.GetDirection(tile, direction.ToDirection()); - anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, adjacentTile); + anchored = _map.GetAnchoredEntitiesEnumerator(gridUid, gridComp, adjacentTile); while (anchored.MoveNext(out var entity)) { DebugTools.Assert(Transform(entity.Value).Anchored); - if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value)) + + // Activate any edge spreaders that are non-terminating + if (_query.HasComponent(entity) && !TerminatingOrDeleted(entity)) EnsureComp(entity.Value); } } diff --git a/Content.Server/Stack/StackSystem.cs b/Content.Server/Stack/StackSystem.cs index aac5a239025..a6b75f953c2 100644 --- a/Content.Server/Stack/StackSystem.cs +++ b/Content.Server/Stack/StackSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.Popups; using Content.Shared.Stacks; -using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -8,148 +7,248 @@ namespace Content.Server.Stack { /// - /// Entity system that handles everything relating to stacks. - /// This is a good example for learning how to code in an ECS manner. + /// Entity system that handles everything relating to stacks. + /// This is a good example for learning how to code in an ECS manner. /// [UsedImplicitly] public sealed class StackSystem : SharedStackSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - public override void Initialize() - { - base.Initialize(); - } - - public override void SetCount(EntityUid uid, int amount, StackComponent? component = null) - { - if (!Resolve(uid, ref component, false)) - return; - - base.SetCount(uid, amount, component); - - // Queue delete stack if count reaches zero. - if (component.Count <= 0) - QueueDel(uid); - } + #region Spawning /// - /// Try to split this stack into two. Returns a non-null if successful. + /// Spawns a new entity and moves an amount to it from the stack. + /// Moves nothing if amount is greater than ent's stack count. /// - public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null) + /// Entity to split in a new stack. + /// How much to move to the new entity. + /// Where to spawn the new stack + /// Null if StackComponent doesn't resolve, or amount to move is greater than ent has available. + [PublicAPI] + public EntityUid? Split(Entity ent, int amount, EntityCoordinates spawnPosition) { - if (!Resolve(uid, ref stack)) + if (!Resolve(ent.Owner, ref ent.Comp)) return null; // Try to remove the amount of things we want to split from the original stack... - if (!Use(uid, amount, stack)) + if (!TryUse(ent, amount)) return null; - // Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked... - var prototype = _prototypeManager.TryIndex(stack.StackTypeId, out var stackType) - ? stackType.Spawn.ToString() - : Prototype(uid)?.ID; + if (!_prototypeManager.Resolve(ent.Comp.StackTypeId, out var stackType)) + return null; // Set the output parameter in the event instance to the newly split stack. - var entity = Spawn(prototype, spawnPosition); + var newEntity = SpawnAtPosition(stackType.Spawn, spawnPosition); - if (TryComp(entity, out StackComponent? stackComp)) - { - // Set the split stack's count. - SetCount(entity, amount, stackComp); - // Don't let people dupe unlimited stacks - stackComp.Unlimited = false; - } + // There should always be a StackComponent + var stackComp = Comp(newEntity); - var ev = new StackSplitEvent(entity); - RaiseLocalEvent(uid, ref ev); + SetCount((newEntity, stackComp), amount); + stackComp.Unlimited = false; // Don't let people dupe unlimited stacks + Dirty(newEntity, stackComp); - return entity; - } + var ev = new StackSplitEvent(newEntity); + RaiseLocalEvent(ent, ref ev); - /// - /// Spawns a stack of a certain stack type. See . - /// - public EntityUid Spawn(int amount, ProtoId id, EntityCoordinates spawnPosition) - { - var proto = _prototypeManager.Index(id); - return Spawn(amount, proto, spawnPosition); + return newEntity; } + #region SpawnAtPosition + /// - /// Spawns a stack of a certain stack type. See . + /// Spawns a stack of a certain stack type and sets its count. Won't set the stack over its max. /// - public EntityUid Spawn(int amount, StackPrototype prototype, EntityCoordinates spawnPosition) + /// The amount to set the spawned stack to. + [PublicAPI] + public EntityUid SpawnAtPosition(int count, StackPrototype prototype, EntityCoordinates spawnPosition) { - // Set the output result parameter to the new stack entity... - var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); - var stack = Comp(entity); + var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); // The real SpawnAtPosition - // And finally, set the correct amount! - SetCount(entity, amount, stack); + SetCount((entity, null), count); return entity; } + /// + [PublicAPI] + public EntityUid SpawnAtPosition(int count, ProtoId id, EntityCoordinates spawnPosition) + { + var proto = _prototypeManager.Index(id); + return SpawnAtPosition(count, proto, spawnPosition); + } + /// - /// Say you want to spawn 97 units of something that has a max stack count of 30. - /// This would spawn 3 stacks of 30 and 1 stack of 7. + /// Say you want to spawn 97 units of something that has a max stack count of 30. + /// This would spawn 3 stacks of 30 and 1 stack of 7. /// - public List SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition) + /// The entities spawned. + /// If the entity to spawn doesn't have stack component this will spawn a bunch of single items. + private List SpawnMultipleAtPosition(EntProtoId entityPrototype, + List amounts, + EntityCoordinates spawnPosition) { - if (amount <= 0) + if (amounts.Count <= 0) { Log.Error( - $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}"); + $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}"); return new(); } - var spawns = CalculateSpawns(entityPrototype, amount); - var spawnedEnts = new List(); - foreach (var count in spawns) + foreach (var count in amounts) { - var entity = SpawnAtPosition(entityPrototype, spawnPosition); + var entity = SpawnAtPosition(entityPrototype, spawnPosition); // The real SpawnAtPosition spawnedEnts.Add(entity); - SetCount(entity, count); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); } return spawnedEnts; } - /// - public List SpawnMultiple(string entityPrototype, int amount, EntityUid target) + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntProtoId entityPrototypeId, + int amount, + EntityCoordinates spawnPosition) { - if (amount <= 0) + return SpawnMultipleAtPosition(entityPrototypeId, + CalculateSpawns(entityPrototypeId, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntityPrototype entityProto, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(entityProto.ID, + CalculateSpawns(entityProto, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(StackPrototype stack, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(stack.Spawn, + CalculateSpawns(stack, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(ProtoId stackId, + int amount, + EntityCoordinates spawnPosition) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleAtPosition(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + spawnPosition); + } + + #endregion + #region SpawnNextToOrDrop + + /// + [PublicAPI] + public EntityUid SpawnNextToOrDrop(int amount, StackPrototype prototype, EntityUid source) + { + var entity = SpawnNextToOrDrop(prototype.Spawn, source); // The real SpawnNextToOrDrop + SetCount((entity, null), amount); + return entity; + } + + /// + [PublicAPI] + public EntityUid SpawnNextToOrDrop(int amount, ProtoId id, EntityUid source) + { + var proto = _prototypeManager.Index(id); + return SpawnNextToOrDrop(amount, proto, source); + } + + /// + private List SpawnMultipleNextToOrDrop(EntProtoId entityPrototype, + List amounts, + EntityUid target) + { + if (amounts.Count <= 0) { Log.Error( - $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}"); + $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}"); return new(); } - var spawns = CalculateSpawns(entityPrototype, amount); - var spawnedEnts = new List(); - foreach (var count in spawns) + foreach (var count in amounts) { - var entity = SpawnNextToOrDrop(entityPrototype, target); + var entity = SpawnNextToOrDrop(entityPrototype, target); // The real SpawnNextToOrDrop spawnedEnts.Add(entity); - SetCount(entity, count); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); } return spawnedEnts; } + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntProtoId stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntityPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.ID, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(StackPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.Spawn, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(ProtoId stackId, + int amount, + EntityUid target) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleNextToOrDrop(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + target); + } + + #endregion + #region Calculate + /// /// Calculates how many stacks to spawn that total up to . /// - /// The stack to spawn. - /// The amount of pieces across all stacks. /// The list of stack counts per entity. - private List CalculateSpawns(string entityPrototype, int amount) + private List CalculateSpawns(int maxCountPerStack, int amount) { - var proto = _prototypeManager.Index(entityPrototype); - proto.TryGetComponent(out var stack, EntityManager.ComponentFactory); - var maxCountPerStack = GetMaxCount(stack); var amounts = new List(); while (amount > 0) { @@ -161,28 +260,47 @@ private List CalculateSpawns(string entityPrototype, int amount) return amounts; } - protected override void UserSplit(EntityUid uid, EntityUid userUid, int amount, - StackComponent? stack = null, - TransformComponent? userTransform = null) + /// + private List CalculateSpawns(StackPrototype stackProto, int amount) { - if (!Resolve(uid, ref stack)) - return; + return CalculateSpawns(GetMaxCount(stackProto), amount); + } - if (!Resolve(userUid, ref userTransform)) + /// + private List CalculateSpawns(EntityPrototype entityPrototype, int amount) + { + return CalculateSpawns(GetMaxCount(entityPrototype), amount); + } + + /// + private List CalculateSpawns(EntProtoId entityId, int amount) + { + return CalculateSpawns(GetMaxCount(entityId), amount); + } + + #endregion + #endregion + #region Event Handlers + + /// + protected override void UserSplit(Entity stack, Entity user, int amount) + { + if (!Resolve(user.Owner, ref user.Comp, false)) return; if (amount <= 0) { - Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), userUid, PopupType.Medium); + Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), user.Owner, PopupType.Medium); return; } - if (Split(uid, amount, userTransform.Coordinates, stack) is not {} split) + if (Split(stack.AsNullable(), amount, user.Comp.Coordinates) is not { } split) return; - Hands.PickupOrDrop(userUid, split); + Hands.PickupOrDrop(user.Owner, split); - Popup.PopupCursor(Loc.GetString("comp-stack-split"), userUid); + Popup.PopupCursor(Loc.GetString("comp-stack-split"), user.Owner); } + #endregion } } diff --git a/Content.Server/Station/Commands/BankCommand.cs b/Content.Server/Station/Commands/BankCommand.cs new file mode 100644 index 00000000000..c24cacdffb6 --- /dev/null +++ b/Content.Server/Station/Commands/BankCommand.cs @@ -0,0 +1,93 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.Cargo.Systems; +using Content.Shared.Administration; +using Content.Shared.Cargo.Components; +using Content.Shared.Cargo.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.Station.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.Admin)] +public sealed class BankCommand : ToolshedCommand +{ + private CargoSystem? _cargo; + + [CommandImplementation("accounts")] + public IEnumerable Accounts([PipedArgument] EntityUid station) + { + _cargo ??= GetSys(); + + foreach (var (account, _) in _cargo.GetAccounts(station)) + { + yield return new BankAccount(account.Id, station, _cargo, EntityManager); + } + } + + [CommandImplementation("accounts")] + public IEnumerable Accounts([PipedArgument] IEnumerable stations) + => stations.SelectMany(Accounts); + + [CommandImplementation("account")] + public BankAccount Account([PipedArgument] EntityUid station, ProtoId account) + { + _cargo ??= GetSys(); + + return new BankAccount(account.Id, station, _cargo, EntityManager); + } + + [CommandImplementation("account")] + public IEnumerable Account([PipedArgument] IEnumerable stations, ProtoId account) + => stations.Select(x => Account(x, account)); + + [CommandImplementation("adjust")] + public IEnumerable Adjust([PipedArgument] IEnumerable @ref, int by) + { + _cargo ??= GetSys(); + var bankAccounts = @ref.ToList(); + foreach (var bankAccount in bankAccounts.ToList()) + { + _cargo.TryAdjustBankAccount(bankAccount.Station, bankAccount.Account, by, true); + } + return bankAccounts; + } + + [CommandImplementation("set")] + public IEnumerable Set([PipedArgument] IEnumerable @ref, int by) + { + _cargo ??= GetSys(); + var bankAccounts = @ref.ToList(); + foreach (var bankAccount in bankAccounts.ToList()) + { + _cargo.TrySetBankAccount(bankAccount.Station, bankAccount.Account, by, true); + } + return bankAccounts; + } + + [CommandImplementation("amount")] + public IEnumerable Amount([PipedArgument] IEnumerable @ref) + { + _cargo ??= GetSys(); + return @ref.Select(bankAccount => (success: _cargo.TryGetAccount(bankAccount.Station, bankAccount.Account, out var money), money)) + .Where(result => result.success) + .Select(result => result.money); + } +} + +public readonly record struct BankAccount( + string Account, + Entity Station, + CargoSystem Cargo, + IEntityManager EntityManager) +{ + public override string ToString() + { + if (!Cargo.TryGetAccount(Station, Account, out var money)) + { + return $"{EntityManager.ToPrettyString(Station)} Account {Account} : (not a account)"; + } + + return $"{EntityManager.ToPrettyString(Station)} Account {Account} : {money}"; + } +} diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs index a3ca014edfc..aa1ab4fe5a0 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs @@ -393,9 +393,6 @@ private Dictionary> GetPlayersJobCandidates(int? weight, if (!(roleBans == null || !roleBans.Contains(jobId))) //TODO: Replace with IsRoleBanned continue; - if (sponsors is not null && !sponsors.HavePriorityJoin && job.SponsorsOnly)//Imperial sponsors - continue; - availableJobs ??= new List(profile.JobPriorities.Count); availableJobs.Add(jobId); } diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index eace9535466..c7037c9a22b 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Station.Components; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; +using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Clothing; using Content.Shared.DetailExaminable; @@ -36,7 +37,8 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem [Dependency] private readonly ActorSystem _actors = default!; [Dependency] private readonly IdCardSystem _cardSystem = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; + [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly PdaSystem _pdaSystem = default!; @@ -124,7 +126,7 @@ public EntityUid SpawnPlayerMob( return jobEntity; } - string speciesId = profile != null ? profile.Species : SharedHumanoidAppearanceSystem.DefaultSpecies; + string speciesId = profile != null ? profile.Species : HumanoidCharacterProfile.DefaultSpecies; if (!_prototypeManager.TryIndex(speciesId, out var species)) throw new ArgumentException($"Invalid species prototype was used: {speciesId}"); @@ -133,7 +135,8 @@ public EntityUid SpawnPlayerMob( if (profile != null) { - _humanoidSystem.LoadProfile(entity.Value, profile); + _visualBody.ApplyProfileTo(entity.Value, profile); + _humanoidProfile.ApplyProfileTo(entity.Value, profile); _metaSystem.SetEntityName(entity.Value, profile.Name); if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText)) diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 1832f1b1a46..676bfbbcc49 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -29,7 +29,7 @@ protected override void Started(EntityUid uid, BasicStationEventSchedulerCompone GameRuleStartedEvent args) { // A little starting variance so schedulers dont all proc at once. - component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + 120); + component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + component.MaximumSpanUntilFirstEvent); } protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule, @@ -132,14 +132,15 @@ public sealed class StationEventCommand : ToolshedCommand // sim an event curTime += TimeSpan.FromSeconds(compMinMax.Next(_random)); - var available = _stationEvent.AvailableEvents(false, playerCount, curTime); - if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, available, out var selectedEvents)) + if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, + out var selectedEvents, + currentTime: curTime, + playerCount: playerCount)) { continue; // doesnt break because maybe the time is preventing events being available. } - var ev = _stationEvent.FindEvent(selectedEvents); - if (ev == null) + if (_stationEvent.FindEvent(selectedEvents) is not { } ev) continue; occurrences[ev] += 1; @@ -150,7 +151,7 @@ public sealed class StationEventCommand : ToolshedCommand } [CommandImplementation("lsprob")] - public IEnumerable<(string, float)> LsProb([CommandArgument] EntProtoId eventSchedulerProto) + public IEnumerable<(string, double)> LsProb([CommandArgument] EntProtoId eventSchedulerProto) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -161,20 +162,18 @@ public sealed class StationEventCommand : ToolshedCommand if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) yield break; - var available = _stationEvent.AvailableEvents(); - if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, available, out var events)) - yield break; + var sortedEvents + = _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules) + .OrderBy(x => -x.Item2); - var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct. - // Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting. - foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...! + foreach (var eventProb in sortedEvents) { - yield return (proto.ID, comp.Weight / totalWeight); + yield return eventProb; } } [CommandImplementation("lsprobtheoretical")] - public IEnumerable<(string, float)> LsProbTime([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] int playerCount, [CommandArgument] float time) + public IEnumerable<(EntProtoId, double)> LsProbTime([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] int playerCount, [CommandArgument] float time) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -187,22 +186,21 @@ public sealed class StationEventCommand : ToolshedCommand var timemins = time * 60; var theoryTime = TimeSpan.Zero + TimeSpan.FromSeconds(timemins); - var available = _stationEvent.AvailableEvents(false, playerCount, theoryTime); - if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, available, out var untimedEvents)) - yield break; - var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= timemins).ToList(); + var sortedEvents + = _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules, + currentTime: theoryTime, + playerCount: playerCount) + .OrderBy(x => -x.Item2); - var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. - - foreach (var (proto, comp) in events) + foreach (var eventProb in sortedEvents) { - yield return (proto.ID, comp.Weight / totalWeight); + yield return eventProb; } } [CommandImplementation("prob")] - public float Prob([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] string eventId) + public double Prob([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] string eventId) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -213,18 +211,15 @@ public float Prob([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgu if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) return 0f; - var available = _stationEvent.AvailableEvents(); - if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, available, out var events)) - return 0f; - - var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. - var weight = 0f; - if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair)) + foreach (var (proto, prob) in _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules)) { - weight = pair.Value.Value.Weight; + if (eventSchedulerProto != proto) + continue; + + return prob; } - return weight / totalWeight; + return 0f; } } } diff --git a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs index b777831856b..5490ae514b6 100644 --- a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs +++ b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs @@ -13,6 +13,12 @@ public sealed partial class BasicStationEventSchedulerComponent : Component [DataField] public float MinimumTimeUntilFirstEvent = 200; + /// + /// How much additional time it may take for a GameRule to first start. + /// + [DataField] + public float MaximumSpanUntilFirstEvent = 120; + /// /// The minimum and maximum time between rule starts in seconds. /// diff --git a/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs b/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs index e2658ac740f..9233b27bd99 100644 --- a/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs +++ b/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs @@ -7,7 +7,11 @@ namespace Content.Server.StationEvents.Components; [RegisterComponent, Access(typeof(GasLeakRule))] public sealed partial class GasLeakRuleComponent : Component { - public readonly Gas[] LeakableGases = + /// + /// Gas types that can be selected for the leak event. + /// + [DataField] + public Gas[] LeakableGases = { Gas.Ammonia, Gas.Plasma, @@ -18,31 +22,86 @@ public sealed partial class GasLeakRuleComponent : Component }; /// - /// Running cooldown of how much time until another leak. + /// Time remaining until the next gas addition to the leak tile. /// + [DataField] public float TimeUntilLeak; /// - /// How long between more gas being added to the tile. + /// Fixed interval in seconds between gas additions to the leak tile. /// + [DataField] public float LeakCooldown = 1.0f; - // Event variables + /// + /// The station where the leak is located. + /// + [DataField] public EntityUid TargetStation; + + /// + /// The specific grid where the leak is located. + /// + [DataField] public EntityUid TargetGrid; + + /// + /// The tile coordinates where the leak is located. + /// + [DataField] public Vector2i TargetTile; + + /// + /// The world coordinates of the leak location. + /// + [DataField] public EntityCoordinates TargetCoords; + + /// + /// Whether a suitable tile for leaking has been found. + /// + [DataField] public bool FoundTile; + + /// + /// The specific gas type currently leaking. + /// + [DataField] public Gas LeakGas; + + /// + /// Current leak rate in moles per second. + /// + [DataField] public float MolesPerSecond; - public readonly int MinimumMolesPerSecond = 80; /// - /// Don't want to make it too fast to give people time to flee. + /// Minimum leak rate in moles per second. + /// + [DataField] + public int MinimumMolesPerSecond = 80; + + /// + /// Maximum leak rate in moles per second. Limited to give people time to flee. /// + [DataField] public int MaximumMolesPerSecond = 200; + /// + /// Minimum total amount of gas to leak over the entire event duration. + /// + [DataField] public int MinimumGas = 1000; + + /// + /// Maximum total amount of gas to leak over the entire event duration. + /// + [DataField] public int MaximumGas = 4000; + + /// + /// Chance to create an ignition spark when the event ends. + /// + [DataField] public float SparkChance = 0.05f; } diff --git a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs index 757a4f18b5d..fcf0d483ab3 100644 --- a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs +++ b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs @@ -9,25 +9,59 @@ namespace Content.Server.StationEvents.Components; public sealed partial class PowerGridCheckRuleComponent : Component { /// - /// Default sound of the announcement when power is back on. + /// Default sound for power restoration announcement. /// private static readonly ProtoId DefaultPowerOn = new("PowerOn"); /// - /// Sound of the announcement to play when power is back on. + /// Sound to play when power is restored. /// [DataField] public SoundSpecifier PowerOnSound = new SoundCollectionSpecifier(DefaultPowerOn, AudioParams.Default.WithVolume(-4f)); + /// + /// Token source for cancelling the power restoration announcement. + /// public CancellationTokenSource? AnnounceCancelToken; + /// + /// Station affected by the power grid event. + /// + [DataField] public EntityUid AffectedStation; - public readonly List Powered = new(); - public readonly List Unpowered = new(); + /// + /// List of APC entities that will be sequentially turned off during the event. + /// + [DataField] + public List Powered = new(); + + /// + /// List of APC entities that have been turned off. + /// + [DataField] + public List Unpowered = new(); + + /// + /// Time delay in seconds before starting to turn off APCs. + /// + [DataField] public float SecondsUntilOff = 30.0f; + /// + /// Number of APC toggles to process per second during the shutdown phase. + /// Dynamically calculated based on total APC count and . + /// public int NumberPerSecond = 0; + + /// + /// Computed time interval between APC toggles. + /// public float UpdateRate => 1.0f / NumberPerSecond; + + /// + /// Accumulated frame time to track when to process the next APC toggle. + /// + [DataField] public float FrameTimeAccumulator = 0.0f; } diff --git a/Content.Server/StationEvents/Components/VentHordeRuleComponent.cs b/Content.Server/StationEvents/Components/VentHordeRuleComponent.cs new file mode 100644 index 00000000000..b4a7b49e6c5 --- /dev/null +++ b/Content.Server/StationEvents/Components/VentHordeRuleComponent.cs @@ -0,0 +1,26 @@ +using Content.Server.StationEvents.Events; +using Content.Shared.EntityTable.EntitySelectors; + +namespace Content.Server.StationEvents.Components; + +/// +/// Component used for the vent horde gamerule. +/// Picks a random entity with +/// and spawns entities picked from the on it after a delay. +/// +[RegisterComponent, Access(typeof(VentHordeRule))] +public sealed partial class VentHordeRuleComponent : Component +{ + /// + /// The table of possible mobs to spawn from the vent. + /// + [DataField(required: true)] + public EntityTableSelector Table = default!; + + /// + /// The vent that has been chosen to spawn the entities. + /// Spawning logic is handled by + /// + [DataField] + public EntityUid? ChosenVent; +} diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index b86d30e2e7b..f0324eeb9d3 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -25,29 +25,21 @@ public sealed class EventManagerSystem : EntitySystem public bool EventsEnabled { get; private set; } private void SetEnabled(bool value) => EventsEnabled = value; + public Dictionary? AllEventCache; + public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnPrototypesReloaded); + Subs.CVar(_configurationManager, CCVars.EventsEnabled, SetEnabled, true); } - /// - /// Randomly runs a valid event. - /// - [Obsolete("use overload taking EnityTableSelector instead or risk unexpected results")] - public void RunRandomEvent() + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { - var randomEvent = PickRandomEvent(); - - if (randomEvent == null) - { - var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events"); - Log.Error(errStr); - return; - } - - GameTicker.AddGameRule(randomEvent); + if (args.WasModified()) + AllEventCache = GetAllEvents(); } /// @@ -55,17 +47,16 @@ public void RunRandomEvent() /// public void RunRandomEvent(EntityTableSelector limitedEventsTable) { - var availableEvents = AvailableEvents(); // handles the player counts and individual event restrictions. - // Putting this here only makes any sense in the context of the toolshed commands in BasicStationEventScheduler. Kill me. - - if (!TryBuildLimitedEvents(limitedEventsTable, availableEvents, out var limitedEvents)) + if (!TryBuildLimitedEvents(limitedEventsTable, out var limitedEvents)) { Log.Warning("Provided event table could not build dict!"); return; } - var randomLimitedEvent = FindEvent(limitedEvents); // this picks the event, It might be better to use the GetSpawns to do it, but that will be a major rebalancing fuck. - if (randomLimitedEvent == null) + // This picks the event. Arguably we should be doing this with GetSpawns but that would be a massive amount of YAML slop. + // Or you'd need a new table prototype which inherits from EntityTables with its own logic for events. + // It's a ton of effort that only results in Events being able to use GroupSelectors so not worth it unless you're insane. + if (FindEvent(limitedEvents) is not { } randomLimitedEvent) { Log.Warning("The selected random event is null!"); return; @@ -81,26 +72,94 @@ public void RunRandomEvent(EntityTableSelector limitedEventsTable) } /// - /// Returns true if the provided EntityTableSelector gives at least one prototype with a StationEvent comp. + /// Builds a list of all possible events and their probabilities. /// + public IEnumerable<(EntProtoId, double)> ListLimitedEvents( + EntityTableSelector limitedEventsTable, + TimeSpan? currentTime = null, + int? playerCount = null) + { + var selectedEvents = _entityTable.ListSpawns(limitedEventsTable); + + return ListLimitedEvents(selectedEvents, currentTime, playerCount); + } + + /// public bool TryBuildLimitedEvents( EntityTableSelector limitedEventsTable, - Dictionary availableEvents, - out Dictionary limitedEvents - ) + out Dictionary limitedEvents, + TimeSpan? currentTime = null, + int? playerCount = null) { - limitedEvents = new Dictionary(); + var selectedEvents = _entityTable.GetSpawns(limitedEventsTable); - if (availableEvents.Count == 0) + return TryBuildLimitedEvents(selectedEvents, out limitedEvents, currentTime, playerCount); + } + + public IEnumerable<(EntProtoId, double)> ListLimitedEvents( + IEnumerable<(EntProtoId, double)> selectedEvents, + TimeSpan? currentTime = null, + int? playerCount = null) + { + var limitedEvents = new List<(EntProtoId, double)>(); + + playerCount ??= _playerManager.PlayerCount; + + // playerCount does a lock so we'll just keep the variable here + currentTime ??= GameTicker.RoundDuration(); + + var totalWeight = 0f; + + foreach (var (eventId, prob) in selectedEvents) { - Log.Warning("No events were available to run!"); - return false; + if (!_prototype.Resolve(eventId, out var eventproto)) + continue; + + if (eventproto.Abstract) + continue; + + if (!eventproto.TryGetComponent(out var stationEvent, EntityManager.ComponentFactory)) + continue; + + if (!CanRun(eventproto, stationEvent, playerCount.Value, currentTime.Value)) + continue; + + limitedEvents.Add((eventproto, prob * stationEvent.Weight)); + totalWeight += stationEvent.Weight; } - var selectedEvents = _entityTable.GetSpawns(limitedEventsTable); + if (!limitedEvents.Any() || totalWeight <= 0) + yield break; - if (selectedEvents.Any() != true) // This is here so if you fuck up the table it wont die. - return false; + for (var i = 0; i < limitedEvents.Count; i++) + { + var eventWeight = limitedEvents[i]; + eventWeight.Item2 /= totalWeight; + yield return eventWeight; + } + } + + /// + /// Builds a dictionary of valid event prototypes from a list of . + /// Dictionary output consists of the valid prototype as the key, and the as the value. + /// + /// List of events we're selecting from. + /// Dictionary we're outputting. + /// Optional override for station time. + /// Optional override for playerCount. + /// Returns true if the provided EntProtoId list has at least one prototype with a StationEventComp that can successfully run! + public bool TryBuildLimitedEvents( + IEnumerable selectedEvents, + out Dictionary limitedEvents, + TimeSpan? currentTime = null, + int? playerCount = null) + { + limitedEvents = new Dictionary(); + + playerCount ??= _playerManager.PlayerCount; + + // playerCount does a lock so we'll just keep the variable here + currentTime ??= GameTicker.RoundDuration(); foreach (var eventid in selectedEvents) { @@ -119,7 +178,7 @@ out Dictionary limitedEvents if (!eventproto.TryGetComponent(out var stationEvent, EntityManager.ComponentFactory)) continue; - if (!availableEvents.ContainsKey(eventproto)) + if (!CanRun(eventproto, stationEvent, playerCount.Value, currentTime.Value)) continue; limitedEvents.Add(eventproto, stationEvent); @@ -183,16 +242,13 @@ out Dictionary limitedEvents /// Override for round time, if using this to simulate events rather than in an actual round. /// public Dictionary AvailableEvents( - bool ignoreEarliestStart = false, int? playerCountOverride = null, TimeSpan? currentTimeOverride = null) { var playerCount = playerCountOverride ?? _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - var currentTime = currentTimeOverride ?? (!ignoreEarliestStart - ? GameTicker.RoundDuration() - : TimeSpan.Zero); + var currentTime = currentTimeOverride ?? GameTicker.RoundDuration(); var result = new Dictionary(); @@ -207,7 +263,19 @@ public Dictionary AvailableEvents( return result; } + /// + /// Returns all events prototypes which exist. Prioritizes the cache. + /// + /// All event prototypes, and their event component. public Dictionary AllEvents() + { + return AllEventCache ?? GetAllEvents(); + } + + /// + /// Gets all event prototypes that exist. Private because you should be using the cache! + /// + private Dictionary GetAllEvents() { var allEvents = new Dictionary(); foreach (var prototype in _prototype.EnumeratePrototypes()) diff --git a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs index 5e0ce6eb676..1fe8f1f0389 100644 --- a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs @@ -6,6 +6,7 @@ using Content.Shared.Station.Components; using Content.Shared.Storage.Components; using Content.Shared.GameTicking.Components; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/StationEvents/Events/BreakerFlipRule.cs b/Content.Server/StationEvents/Events/BreakerFlipRule.cs index 8a25d40abbc..745db7a0cde 100644 --- a/Content.Server/StationEvents/Events/BreakerFlipRule.cs +++ b/Content.Server/StationEvents/Events/BreakerFlipRule.cs @@ -4,6 +4,7 @@ using Content.Shared.GameTicking.Components; using Content.Shared.Station.Components; using JetBrains.Annotations; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; @@ -17,7 +18,7 @@ protected override void Added(EntityUid uid, BreakerFlipRuleComponent component, if (!TryComp(uid, out var stationEvent)) return; - var str = Loc.GetString("station-event-breaker-flip-announcement", ("data", Loc.GetString(Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}")))); + var str = Loc.GetString("station-event-breaker-flip-announcement", ("data", Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}"))); stationEvent.StartAnnouncement = str; base.Added(uid, component, gameRule, args); diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index d38de1e0d80..b83fdc702bb 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -64,9 +64,7 @@ protected override void ActiveTick(EntityUid uid, CargoGiftsRuleComponent compon if (!_cargoSystem.AddAndApproveOrder( station!.Value, - product.Product, - product.Name, - product.Cost, + product, qty, Loc.GetString(component.Sender), Loc.GetString(component.Description), diff --git a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs index b03231b5bcd..ef1bd696e91 100644 --- a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs +++ b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs @@ -17,7 +17,7 @@ protected override void Started(EntityUid uid, MassHallucinationsRuleComponent c { base.Started(uid, component, gameRule, args); - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var ent, out _, out _)) { if (!EnsureComp(ent, out var paracusia)) diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs index 3a86f694ab8..b0cc0bc9932 100644 --- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs +++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs @@ -9,6 +9,7 @@ using Robust.Shared.Player; using Robust.Shared.Utility; using Timer = Robust.Shared.Timing.Timer; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events { diff --git a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs index 08e43b4c68d..9453477bd62 100644 --- a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs +++ b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs @@ -3,6 +3,7 @@ using Content.Shared.GameTicking.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index aaa48a482ec..e0ec1328bea 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs index 695381227be..d93c67ef92d 100644 --- a/Content.Server/StationEvents/Events/VentClogRule.cs +++ b/Content.Server/StationEvents/Events/VentClogRule.cs @@ -27,7 +27,7 @@ protected override void Started(EntityUid uid, VentClogRuleComponent component, // TODO: "safe random" for chems. Right now this includes admin chemicals. var allReagents = PrototypeManager.EnumeratePrototypes() - .Where(x => !x.Abstract && !component.BlacklistedReagents.Contains(x)) + .Where(x => !x.Abstract) .Select(x => new ProtoId(x.ID)).ToList(); foreach (var (_, transform) in EntityQuery()) diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs index 5ca5a9d215c..c8113c3098d 100644 --- a/Content.Server/StationEvents/Events/VentCrittersRule.cs +++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs @@ -27,6 +27,9 @@ protected override void Started(EntityUid uid, VentCrittersRuleComponent compone var validLocations = new List(); while (locations.MoveNext(out _, out _, out var transform)) { + if (!transform.Anchored) + continue; + if (CompOrNull(transform.GridUid)?.Station == station) { validLocations.Add(transform.Coordinates); diff --git a/Content.Server/StationEvents/Events/VentHordeRule.cs b/Content.Server/StationEvents/Events/VentHordeRule.cs new file mode 100644 index 00000000000..230b93c5485 --- /dev/null +++ b/Content.Server/StationEvents/Events/VentHordeRule.cs @@ -0,0 +1,122 @@ +using System.Linq; +using Content.Server.Pinpointer; +using Content.Server.StationEvents.Components; +using Content.Server.VentHorde.Components; +using Content.Server.VentHorde.Systems; +using Content.Shared.EntityTable; +using Content.Shared.GameTicking.Components; +using Content.Shared.Station.Components; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.StationEvents.Events; + +/// +/// Variant of that selects a single vent and spawns all entities there. +/// +public sealed class VentHordeRule : StationEventSystem +{ + /* + * DO NOT COPY PASTE THIS TO MAKE YOUR MOB EVENT. + * USE THE PROTOTYPE. + */ + + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly NavMapSystem _navMap = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityTableSystem _table = default!; + [Dependency] private readonly VentHordeSystem _horde = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + protected override void Added(EntityUid uid, VentHordeRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + // Choose location and make sure it's not null + component.ChosenVent = ChooseVent(); + + if (component.ChosenVent is not { } vent) + { + Log.Warning($"Unable to find a valid vent for {args.RuleId}!"); + ForceEndSelf(uid, gameRule); + return; + } + + // Get the event component so we can format the announcement + if (TryComp(uid, out var stationEventComp) && stationEventComp.StartAnnouncement != null) + { + // Get the nearest beacon + var mapLocation = _transform.ToMapCoordinates(Transform(vent).Coordinates); + var nearestBeacon = _navMap.GetNearestBeaconString(mapLocation, onlyName: true); + + // Format the announcement with the location, if the string doesn't have them it'll still work fine + // time is not said on purpose to keep the players on their toes. + // also because we cannot tell the end time inside of Added(). + stationEventComp.StartAnnouncement = + Loc.GetString(stationEventComp.StartAnnouncement, + ("location", nearestBeacon)); + } + + base.Added(uid, component, gameRule, args); + } + + protected override void Started(EntityUid uid, VentHordeRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (!Exists(component.ChosenVent)) + { + Log.Warning($"Chosen vent for {args.RuleId} does not exist!"); + ForceEndSelf(uid, gameRule); + return; + } + + if (!TryComp(uid, out var stationEventComp)) + return; + + // We grab when the gamerule is expected to end and subtract the current time from it to get the duration. + var duration = (stationEventComp.EndTime - _timing.CurTime) ?? TimeSpan.Zero; + + var spawns = _table.GetSpawns(component.Table); + + if (component.ChosenVent == null) + return; + + // And start the spawn at the chosen vent. + // The duration is the same as the time until expected gamerule end time, but that is only for convenience. + // The spawn can happen early in certain circumstances anyway. + _horde.StartHordeSpawn(component.ChosenVent.Value, spawns.ToList(), duration); + } + + private EntityUid? ChooseVent() + { + // Get a station + if (!TryGetRandomStation(out var station)) + { + return null; + } + + // Query the possible locations + var locations = EntityQueryEnumerator(); + var validLocations = new List(); + + // Filter to things on the same station + while (locations.MoveNext(out var uid, out _, out var transform)) + { + if (!transform.Anchored) + continue; + + if (HasComp(uid)) + continue; + + if (CompOrNull(transform.GridUid)?.Station == station) + { + validLocations.Add(uid); + } + } + + // Pick one at random + if (validLocations.Count != 0) + return _random.Pick(validLocations); + + return null; + } +} diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs index 10eedd7562d..2e6e7fcb96c 100644 --- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs +++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; -using Content.Server.Forensics; using Content.Shared.Access.Components; using Content.Shared.Forensics.Components; using Content.Shared.GameTicking; diff --git a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs index c38a3e9b175..8fb898dfa84 100644 --- a/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/EntityStorageSystem.cs @@ -1,22 +1,11 @@ -using System.Diagnostics.CodeAnalysis; using Content.Server.Atmos.EntitySystems; using Content.Server.Body.Systems; using Content.Server.Construction; using Content.Server.Construction.Components; -using Content.Server.Storage.Components; -using Content.Shared.Destructible; -using Content.Shared.Explosion; -using Content.Shared.Foldable; -using Content.Shared.Interaction; -using Content.Shared.Lock; -using Content.Shared.Movement.Events; +using Content.Shared.Atmos; using Content.Shared.Storage.Components; using Content.Shared.Storage.EntitySystems; -using Content.Shared.Tools.Systems; -using Content.Shared.Verbs; using Robust.Server.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; using Robust.Shared.Map; namespace Content.Server.Storage.EntitySystems; @@ -32,30 +21,11 @@ public override void Initialize() { base.Initialize(); - /* CompRef things */ - SubscribeLocalEvent(OnEntityUnpausedEvent); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnComponentStartup); - SubscribeLocalEvent(OnInteract, after: new[] { typeof(LockSystem) }); - SubscribeLocalEvent(OnLockToggleAttempt); - SubscribeLocalEvent(OnDestruction); - SubscribeLocalEvent>(AddToggleOpenVerb); - SubscribeLocalEvent(OnRelayMovement); - SubscribeLocalEvent(OnFoldAttempt); - - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); - /* CompRef things */ - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnWeldableAttempt); - SubscribeLocalEvent(OnExploded); SubscribeLocalEvent(OnInsideInhale); SubscribeLocalEvent(OnInsideExhale); SubscribeLocalEvent(OnInsideExposed); - - SubscribeLocalEvent(OnRemoved); } private void OnMapInit(EntityUid uid, EntityStorageComponent component, MapInitEvent args) @@ -76,64 +46,30 @@ protected override void OnComponentInit(EntityUid uid, EntityStorageComponent co _construction.AddContainer(uid, ContainerName, construction); } - public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component) - { - if (component != null) - return true; - - TryComp(uid, out var storage); - component = storage; - return component != null; - } - - private void OnWeldableAttempt(EntityUid uid, EntityStorageComponent component, WeldableAttemptEvent args) - { - if (component.Open) - { - args.Cancel(); - return; - } - - if (component.Contents.Contains(args.User)) - { - var msg = Loc.GetString("entity-storage-component-already-contains-user-message"); - Popup.PopupEntity(msg, args.User, args.User); - args.Cancel(); - } - } - - private void OnExploded(Entity ent, ref BeforeExplodeEvent args) - { - args.Contents.AddRange(ent.Comp.Contents.ContainedEntities); - } - protected override void TakeGas(EntityUid uid, EntityStorageComponent component) { if (!component.Airtight) return; - var serverComp = (EntityStorageComponent) component; - var tile = GetOffsetTileRef(uid, serverComp); + var tile = GetOffsetTileRef(uid, component); - if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is {} environment) + if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is { } environment) { - _atmos.Merge(serverComp.Air, environment.RemoveVolume(serverComp.Air.Volume)); + _atmos.Merge(component.Air, environment.RemoveVolume(component.Air.Volume)); } } public override void ReleaseGas(EntityUid uid, EntityStorageComponent component) { - var serverComp = (EntityStorageComponent) component; - - if (!serverComp.Airtight) + if (!component.Airtight) return; - var tile = GetOffsetTileRef(uid, serverComp); + var tile = GetOffsetTileRef(uid, component); - if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is {} environment) + if (tile != null && _atmos.GetTileMixture(tile.Value.GridUid, null, tile.Value.GridIndices, true) is { } environment) { - _atmos.Merge(environment, serverComp.Air); - serverComp.Air.Clear(); + _atmos.Merge(environment, component.Air); + component.Air.Clear(); } } @@ -149,13 +85,6 @@ public override void ReleaseGas(EntityUid uid, EntityStorageComponent component) return null; } - private void OnRemoved(EntityUid uid, InsideEntityStorageComponent component, EntGotRemovedFromContainerMessage args) - { - if (args.Container.Owner != component.Storage) - return; - RemComp(uid, component); - } - #region Gas mix event handlers private void OnInsideInhale(EntityUid uid, InsideEntityStorageComponent component, InhaleLocationEvent args) diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 4b5dd7290c0..cdbac12d804 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -1,19 +1,11 @@ -using Content.Server.Administration.Managers; -using Content.Shared.Administration; using Content.Shared.Explosion; -using Content.Shared.Ghost; using Content.Shared.Hands; -using Content.Shared.Lock; using Content.Shared.Storage; using Content.Shared.Storage.Components; using Content.Shared.Storage.EntitySystems; -using Content.Shared.Verbs; -using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - namespace Content.Server.Storage.EntitySystems; public sealed partial class StorageSystem : SharedStorageSystem @@ -24,7 +16,6 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnExploded); - SubscribeLocalEvent(OnStorageFillMapInit); } diff --git a/Content.Server/Storage/StorageCommand.cs b/Content.Server/Storage/StorageCommand.cs index b7aee26aa97..d5855388fa1 100644 --- a/Content.Server/Storage/StorageCommand.cs +++ b/Content.Server/Storage/StorageCommand.cs @@ -20,7 +20,7 @@ public sealed class StorageCommand : ToolshedCommand public IEnumerable StorageInsert([PipedArgument] IEnumerable entsToInsert, EntityUid targetEnt) => entsToInsert.Where(x => StorageInsert(x, targetEnt) != null); - public EntityUid? StorageInsert(EntityUid entToInsert, EntityUid targetEnt) + private EntityUid? StorageInsert(EntityUid entToInsert, EntityUid targetEnt) { _storage ??= GetSys(); @@ -32,7 +32,6 @@ public IEnumerable StorageInsert([PipedArgument] IEnumerable StorageFastTake([PipedArgument] IEnumerable storageEnts) => storageEnts.Select(StorageFastTake).OfType(); @@ -53,6 +52,29 @@ public IEnumerable StorageFastTake([PipedArgument] IEnumerable StorageQuery([PipedArgument] IEnumerable storageEnts, bool recursive) => + storageEnts.SelectMany(x => StorageQueryRecursiveBase(x, recursive)); + + public IEnumerable StorageQueryRecursiveBase(EntityUid storageEnt, bool recursive) + { + _storage ??= GetSys(); + _container ??= GetSys(); + + if (!EntityManager.TryGetComponent(storageEnt, out var storage)) + return []; + + IEnumerable containedEntities = storage.Container.ContainedEntities; + if (recursive) + { + foreach (var ent in containedEntities) + { + containedEntities = containedEntities.Concat(StorageQueryRecursiveBase(ent, true)); + } + } + + return containedEntities; + } } diff --git a/Content.Server/Store/Conditions/BuyerObjectiveWhitelistCondition.cs b/Content.Server/Store/Conditions/BuyerObjectiveWhitelistCondition.cs new file mode 100644 index 00000000000..b6e7496ce91 --- /dev/null +++ b/Content.Server/Store/Conditions/BuyerObjectiveWhitelistCondition.cs @@ -0,0 +1,46 @@ +using Content.Shared.Mind; +using Content.Shared.Store; +using Content.Shared.Whitelist; + +namespace Content.Server.Store.Conditions; + +/// +/// Filters out an entry based on the objectives of an entity. +/// +public sealed partial class BuyerObjectiveWhitelistCondition : ListingCondition +{ + /// + /// A whitelist of objective types. + /// If there is no whitelist, the object will appear by default. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// A blacklist of objective types. + /// If an objective is both whitelisted and blacklisted the blacklist will take priority and the entry will not appear. + /// + [DataField] + public EntityWhitelist? Blacklist; + + public override bool Condition(ListingConditionArgs args) + { + var ent = args.EntityManager; + var whitelistSystem = ent.System(); + + if (!args.EntityManager.TryGetComponent(args.Buyer, out var mindComp)) + return true; // inanimate objects don't have minds + + var whitelisted = false; + + foreach (var objective in mindComp.Objectives) + { + if (whitelistSystem.IsWhitelistPass(Blacklist, objective)) + return false; + if (whitelistSystem.IsWhitelistPass(Whitelist, objective)) + whitelisted = true; + } + + return Whitelist == null || whitelisted; + } +} diff --git a/Content.Server/Store/Conditions/BuyerSpeciesCondition.cs b/Content.Server/Store/Conditions/BuyerSpeciesCondition.cs index a4f1227eccf..f7595bc836a 100644 --- a/Content.Server/Store/Conditions/BuyerSpeciesCondition.cs +++ b/Content.Server/Store/Conditions/BuyerSpeciesCondition.cs @@ -31,18 +31,18 @@ public override bool Condition(ListingConditionArgs args) if (!ent.TryGetComponent(args.Buyer, out var mind)) return true; // needed to obtain body entityuid to check for humanoid appearance - if (!ent.TryGetComponent(mind.OwnedEntity, out var appearance)) + if (!ent.TryGetComponent(mind.OwnedEntity, out var humanoid)) return true; // inanimate or non-humanoid entities should be handled elsewhere, main example being surplus crates if (Blacklist != null) { - if (Blacklist.Contains(appearance.Species)) + if (Blacklist.Contains(humanoid.Species)) return false; } if (Whitelist != null) { - if (!Whitelist.Contains(appearance.Species)) + if (!Whitelist.Contains(humanoid.Species)) return false; } diff --git a/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs index ff4a9a19cdd..28b27878409 100644 --- a/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs +++ b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs @@ -26,7 +26,7 @@ public override bool Condition(ListingConditionArgs args) var whitelistSystem = ent.System(); if (whitelistSystem.IsWhitelistFail(Whitelist, args.Buyer) || - whitelistSystem.IsBlacklistPass(Blacklist, args.Buyer)) + whitelistSystem.IsWhitelistPass(Blacklist, args.Buyer)) return false; return true; diff --git a/Content.Server/Store/Conditions/StoreWhitelistCondition.cs b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs index ced4dfa9c0b..e27d79af756 100644 --- a/Content.Server/Store/Conditions/StoreWhitelistCondition.cs +++ b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs @@ -29,7 +29,7 @@ public override bool Condition(ListingConditionArgs args) var whitelistSystem = ent.System(); if (whitelistSystem.IsWhitelistFail(Whitelist, args.StoreEntity.Value) || - whitelistSystem.IsBlacklistPass(Blacklist, args.StoreEntity.Value)) + whitelistSystem.IsWhitelistPass(Blacklist, args.StoreEntity.Value)) return false; return true; diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs deleted file mode 100644 index 412114ce03b..00000000000 --- a/Content.Server/Store/Systems/StoreSystem.Listings.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Mind; -using Content.Shared.Store; -using Content.Shared.Store.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.Store.Systems; - -public sealed partial class StoreSystem -{ - /// - /// Refreshes all listings on a store. - /// Do not use if you don't know what you're doing. - /// - /// The store to refresh - public void RefreshAllListings(StoreComponent component) - { - var previousState = component.FullListingsCatalog; - var newState = GetAllListings(); - // if we refresh list with existing cost modifiers - they will be removed, - // need to restore them - if (previousState.Count != 0) - { - foreach (var previousStateListingItem in previousState) - { - if (!previousStateListingItem.IsCostModified - || !TryGetListing(newState, previousStateListingItem.ID, out var found)) - { - continue; - } - - foreach (var (modifierSourceId, costModifier) in previousStateListingItem.CostModifiersBySourceId) - { - found.AddCostModifier(modifierSourceId, costModifier); - } - } - } - - component.FullListingsCatalog = newState; - } - - /// - /// Gets all listings from a prototype. - /// - /// All the listings - public HashSet GetAllListings() - { - var clones = new HashSet(); - foreach (var prototype in _proto.EnumeratePrototypes()) - { - clones.Add(new ListingDataWithCostModifiers(prototype)); - } - - return clones; - } - - /// - /// Adds a listing from an Id to a store - /// - /// The store to add the listing to - /// The id of the listing - /// Whether or not the listing was added successfully - public bool TryAddListing(StoreComponent component, string listingId) - { - if (!_proto.TryIndex(listingId, out var proto)) - { - Log.Error("Attempted to add invalid listing."); - return false; - } - - return TryAddListing(component, proto); - } - - /// - /// Adds a listing to a store - /// - /// The store to add the listing to - /// The listing - /// Whether or not the listing was add successfully - public bool TryAddListing(StoreComponent component, ListingPrototype listing) - { - return component.FullListingsCatalog.Add(new ListingDataWithCostModifiers(listing)); - } - - /// - /// Gets the available listings for a store - /// - /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) - /// - /// The store the listings are coming from. - /// The available listings. - public IEnumerable GetAvailableListings(EntityUid buyer, EntityUid store, StoreComponent component) - { - return GetAvailableListings(buyer, component.FullListingsCatalog, component.Categories, store); - } - - /// - /// Gets the available listings for a user given an overall set of listings and categories to filter by. - /// - /// Either the account owner, user, or an inanimate object (e.g., surplus bundle) - /// All of the listings that are available. If null, will just get all listings from the prototypes. - /// What categories to filter by. - /// The physial entity of the store. Can be null. - /// The available listings. - public IEnumerable GetAvailableListings( - EntityUid buyer, - IReadOnlyCollection? listings, - HashSet> categories, - EntityUid? storeEntity = null - ) - { - listings ??= GetAllListings(); - - foreach (var listing in listings) - { - if (!ListingHasCategory(listing, categories)) - continue; - - if (listing.Conditions != null) - { - var args = new ListingConditionArgs(GetBuyerMind(buyer), storeEntity, listing, EntityManager); - var conditionsMet = true; - - foreach (var condition in listing.Conditions) - { - if (!condition.Condition(args)) - { - conditionsMet = false; - break; - } - } - - if (!conditionsMet) - continue; - } - - yield return listing; - } - } - - /// - /// Returns the entity's mind entity, if it has one, to be used for listing conditions. - /// If it doesn't have one, or is a mind entity already, it returns itself. - /// - /// The buying entity. - public EntityUid GetBuyerMind(EntityUid buyer) - { - if (!HasComp(buyer) && _mind.TryGetMind(buyer, out var buyerMind, out var _)) - return buyerMind; - - return buyer; - } - - /// - /// Checks if a listing appears in a list of given categories - /// - /// The listing itself. - /// The categories to check through. - /// If the listing was present in one of the categories. - public bool ListingHasCategory(ListingData listing, HashSet> categories) - { - foreach (var cat in categories) - { - if (listing.Categories.Contains(cat)) - return true; - } - return false; - } - - private bool TryGetListing(IReadOnlyCollection collection, string listingId, [MaybeNullWhen(false)] out ListingDataWithCostModifiers found) - { - foreach(var current in collection) - { - if (current.ID == listingId) - { - found = current; - return true; - } - } - - found = null!; - return false; - } -} diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs index cb92a42c10a..f4ecd870407 100644 --- a/Content.Server/Store/Systems/StoreSystem.Refund.cs +++ b/Content.Server/Store/Systems/StoreSystem.Refund.cs @@ -4,11 +4,14 @@ using Content.Shared.Store.Components; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Containers; +using Robust.Shared.Timing; namespace Content.Server.Store.Systems; public sealed partial class StoreSystem { + [Dependency] private readonly IGameTiming _timing = default!; + private void InitializeRefund() { SubscribeLocalEvent(OnStoreTerminating); diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 742434ff23a..713146856b1 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -7,14 +7,12 @@ using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Hands.EntitySystems; -using Content.Shared.Mind; -using Content.Shared.PDA.Ringer; +using Content.Shared.Mindshield.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Store; using Content.Shared.Store.Components; using Content.Shared.UserInterface; -using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Server.Store.Systems; @@ -22,14 +20,14 @@ namespace Content.Server.Store.Systems; public sealed partial class StoreSystem { [Dependency] private readonly IAdminLogManager _admin = default!; - [Dependency] private readonly SharedHandsSystem _hands = default!; - [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly ActionUpgradeSystem _actionUpgrade = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly StackSystem _stack = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; private void InitializeUi() { @@ -38,6 +36,16 @@ private void InitializeUi() SubscribeLocalEvent(OnRequestWithdraw); SubscribeLocalEvent(OnRequestRefund); SubscribeLocalEvent(OnRefundEntityDeleted); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); + SubscribeLocalEvent((e, c, ev) => + RemoteStoreRelay((e, c), ev)); } private void OnRefundEntityDeleted(Entity ent, ref RefundEntityDeletedEvent args) @@ -45,73 +53,12 @@ private void OnRefundEntityDeleted(Entity ent, ref RefundEntityD ent.Comp.BoughtEntities.Remove(args.Uid); } - /// - /// Toggles the store Ui open and closed - /// - /// the person doing the toggling - /// the store being toggled - /// - public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null) - { - if (!Resolve(storeEnt, ref component)) - return; - - if (!TryComp(user, out var actor)) - return; - - if (!_ui.TryToggleUi(storeEnt, StoreUiKey.Key, actor.PlayerSession)) - return; - - UpdateUserInterface(user, storeEnt, component); - } - - /// - /// Closes the store UI for everyone, if it's open - /// - public void CloseUi(EntityUid uid, StoreComponent? component = null) + private void RemoteStoreRelay(Entity entity, object ev) { - if (!Resolve(uid, ref component)) + if (entity.Comp.Store == null || !TryComp(entity.Comp.Store, out var store)) return; - _ui.CloseUi(uid, StoreUiKey.Key); - } - - /// - /// Updates the user interface for a store and refreshes the listings - /// - /// The person who if opening the store ui. Listings are filtered based on this. - /// The store entity itself - /// The store component being refreshed. - public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null) - { - if (!Resolve(store, ref component)) - return; - - //this is the person who will be passed into logic for all listing filtering. - if (user != null) //if we have no "buyer" for this update, then don't update the listings - { - component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component) - .ToHashSet(); - } - - //dictionary for all currencies, including 0 values for currencies on the whitelist - Dictionary, FixedPoint2> allCurrency = new(); - foreach (var supported in component.CurrencyWhitelist) - { - allCurrency.Add(supported, FixedPoint2.Zero); - - if (component.Balance.TryGetValue(supported, out var value)) - allCurrency[supported] = value; - } - - // TODO: if multiple users are supposed to be able to interact with a single BUI & see different - // stores/listings, this needs to use session specific BUI states. - - // only tell operatives to lock their uplink if it can be locked - var showFooter = HasComp(store); - - var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed); - _ui.SetUiState(store, StoreUiKey.Key, state); + RaiseLocalEvent(entity.Comp.Store.Value, ev); } private void OnRequestUpdate(EntityUid uid, StoreComponent component, StoreRequestUpdateInterfaceMessage args) @@ -176,6 +123,13 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi component.BalanceSpent[currency] += amount; } + //apply components + if (listing.ProductComponents != null) + { + if (_proto.Resolve(listing.ProductComponents, out var productComponentsEntity)) + EntityManager.AddComponents(buyer, productComponentsEntity.Components); + } + //spawn entity if (listing.ProductEntity != null) { @@ -202,7 +156,7 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi EntityUid? actionId; // I guess we just allow duplicate actions? // Allow duplicate actions and just have a single list buy for the buy-once ones. - if (!_mind.TryGetMind(buyer, out var mind, out _)) + if (listing.ApplyToMob || !Mind.TryGetMind(buyer, out var mind, out _)) actionId = _actions.AddAction(buyer, listing.ProductAction); else actionId = _actionContainer.AddAction(mind, listing.ProductAction); @@ -262,12 +216,27 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi } //log dat shit. + var logImpact = LogImpact.Low; + var logExtraInfo = ""; + if (component.ExpectedFaction?.Count > 0 && !_npcFaction.IsMemberOfAny(buyer, component.ExpectedFaction)) + { + logImpact = LogImpact.High; + logExtraInfo = ", but was not from an expected faction"; + + if (HasComp(buyer)) + { + logImpact = LogImpact.Extreme; + logExtraInfo += " while also possessing a mindshield"; + } + } + _admin.Add(LogType.StorePurchase, - LogImpact.Low, - $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _proto)}\" from {ToPrettyString(uid)}"); + logImpact, + $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, Proto)}\" from {ToPrettyString(uid)}{logExtraInfo}."); listing.PurchaseAmount++; //track how many times something has been purchased - _audio.PlayEntity(component.BuySuccessSound, msg.Actor, uid); //cha-ching! + if (msg.SoundSource != null && GetEntity(msg.SoundSource) != null) + _audio.PlayEntity(component.BuySuccessSound, msg.Actor, GetEntity(msg.SoundSource.Value)); //cha-ching! var buyFinished = new StoreBuyFinishedEvent { @@ -296,7 +265,7 @@ private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreReq return; //make sure a malicious client didn't send us random shit - if (!_proto.TryIndex(msg.Currency, out var proto)) + if (!Proto.TryIndex(msg.Currency, out var proto)) return; //we need an actually valid entity to spawn. This check has been done earlier, but just in case. @@ -313,7 +282,7 @@ private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreReq { var cashId = proto.Cash[value]; var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value)); - var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates); + var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates); if (ents.FirstOrDefault() is {} ent) _hands.PickupOrDrop(buyer, ent); amountRemaining -= value * amountToSpawn; diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 0625ced0876..b8e92f10bf6 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -1,42 +1,25 @@ -using Content.Server.Store.Components; -using Content.Shared.UserInterface; -using Content.Shared.FixedPoint; using Content.Shared.Implants.Components; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Stacks; +using Content.Shared.Store; using Content.Shared.Store.Components; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; +using Content.Shared.UserInterface; using Robust.Shared.Utility; -using System.Linq; -using Robust.Shared.Timing; -using Content.Shared.Mind; namespace Content.Server.Store.Systems; -/// -/// Manages general interactions with a store and different entities, -/// getting listings for stores, and interfacing with the store UI. -/// -public sealed partial class StoreSystem : EntitySystem +public sealed partial class StoreSystem : SharedStoreSystem { - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly IGameTiming _timing = default!; - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStoreOpenAttempt); - SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(BeforeActivatableUiOpen); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnImplantActivate); + + SubscribeLocalEvent(OnImplantActivate); InitializeUi(); InitializeCommand(); @@ -47,6 +30,10 @@ private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent arg { RefreshAllListings(component); component.StartingMap = Transform(uid).MapUid; + + // Add the bui key if it does not exist already (the check is needed to make sure that we don't overwrite existing InterfaceData). + if (!UI.HasUi(uid, StoreUiKey.Key)) + UI.SetUi(uid, StoreUiKey.Key, new InterfaceData("StoreBoundUserInterface")); } private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args) @@ -72,7 +59,7 @@ private void OnStoreOpenAttempt(EntityUid uid, StoreComponent component, Activat if (!component.OwnerOnly) return; - if (!_mind.TryGetMind(args.User, out var mind, out _)) + if (!Mind.TryGetMind(args.User, out var mind, out _)) return; component.AccountOwner ??= mind; @@ -81,126 +68,17 @@ private void OnStoreOpenAttempt(EntityUid uid, StoreComponent component, Activat if (component.AccountOwner == mind) return; - _popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User); + if (!args.Silent) + Popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User); + args.Cancel(); } - private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) + private void OnImplantActivate(Entity entity, ref OpenUplinkImplantEvent args) { - if (args.Handled || !args.CanReach) - return; - - if (!TryComp(args.Target, out var store)) - return; - - var ev = new CurrencyInsertAttemptEvent(args.User, args.Target.Value, args.Used, store); - RaiseLocalEvent(args.Target.Value, ev); - if (ev.Cancelled) - return; - - if (!TryAddCurrency((uid, component), (args.Target.Value, store))) + if (GetRemoteStore(entity.AsNullable()) is not { } store) return; - args.Handled = true; - var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target)); - _popup.PopupEntity(msg, args.Target.Value, args.User); - } - - private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args) - { - ToggleUi(args.Performer, uid, component); - } - - /// - /// Gets the value from an entity's currency component. - /// Scales with stacks. - /// - /// - /// If this result is intended to be used with , - /// consider using instead to ensure that the currency is consumed in the process. - /// - /// - /// - /// The value of the currency - public Dictionary GetCurrencyValue(EntityUid uid, CurrencyComponent component) - { - var amount = EntityManager.GetComponentOrNull(uid)?.Count ?? 1; - return component.Price.ToDictionary(v => v.Key, p => p.Value * amount); - } - - /// - /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process. - /// - public bool TryAddCurrency(Entity currency, Entity store) - { - if (!Resolve(currency.Owner, ref currency.Comp)) - return false; - - if (!Resolve(store.Owner, ref store.Comp)) - return false; - - var value = currency.Comp.Price; - if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1) - { - value = currency.Comp.Price - .ToDictionary(v => v.Key, p => p.Value * stack.Count); - } - - if (!TryAddCurrency(value, store, store.Comp)) - return false; - - // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the - // same tick - currency.Comp.Price.Clear(); - if (stack != null) - _stack.SetCount(currency.Owner, 0, stack); - - QueueDel(currency); - return true; - } - - /// - /// Tries to add a currency to a store's balance - /// - /// The value to add to the store - /// - /// The store to add it to - /// Whether or not the currency was succesfully added - public bool TryAddCurrency(Dictionary currency, EntityUid uid, StoreComponent? store = null) - { - if (!Resolve(uid, ref store)) - return false; - - //verify these before values are modified - foreach (var type in currency) - { - if (!store.CurrencyWhitelist.Contains(type.Key)) - return false; - } - - foreach (var type in currency) - { - if (!store.Balance.TryAdd(type.Key, type.Value)) - store.Balance[type.Key] += type.Value; - } - - UpdateUserInterface(null, uid, store); - return true; - } -} - -public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs -{ - public readonly EntityUid User; - public readonly EntityUid Target; - public readonly EntityUid Used; - public readonly StoreComponent Store; - - public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store) - { - User = user; - Target = target; - Used = used; - Store = store; + ToggleUi(args.Performer, store, store.Comp, entity, entity.Comp); } } diff --git a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs index c1757b1c2d3..09e42966c7b 100644 --- a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs +++ b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs @@ -27,6 +27,7 @@ private void TryDoCollideStun(Entity ent, EntityUid targ if (ent.Comp.Refresh) { _stunSystem.TryUpdateStunDuration(target, ent.Comp.StunAmount); + _movementMod.TryUpdateMovementSpeedModDuration( target, MovementModStatusSystem.TaserSlowdown, diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs index e8ae5db3001..90d2dc75888 100644 --- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs +++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs @@ -1,12 +1,15 @@ using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Server.Power.Events; +using Content.Server.Power.EntitySystems; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage.Events; using Content.Shared.Examine; using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Popups; +using Content.Shared.Power; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Stunnable; namespace Content.Server.Stunnable.Systems @@ -15,7 +18,7 @@ public sealed class StunbatonSystem : SharedStunbatonSystem { [Dependency] private readonly RiggableSystem _riggableSystem = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedBatterySystem _battery = default!; [Dependency] private readonly ItemToggleSystem _itemToggle = default!; public override void Initialize() @@ -24,14 +27,14 @@ public override void Initialize() SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnSolutionChange); - SubscribeLocalEvent(OnStaminaHitAttempt); + SubscribeLocalEvent(OnStaminaHitAttempt); SubscribeLocalEvent(OnChargeChanged); } - private void OnStaminaHitAttempt(Entity entity, ref BeforeStaminaDamageOnTriggerEvent args) + private void OnStaminaHitAttempt(Entity entity, ref StaminaDamageOnHitAttemptEvent args) { if (!_itemToggle.IsActivated(entity.Owner) || - !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge(entity.Owner, entity.Comp.EnergyPerUse, battery)) + !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse)) { args.Cancelled = true; } @@ -46,7 +49,7 @@ private void OnExamined(Entity entity, ref ExaminedEvent arg if (TryComp(entity.Owner, out var battery)) { - var count = (int) (battery.CurrentCharge / entity.Comp.EnergyPerUse); + var count = _battery.GetRemainingUses((entity.Owner, battery), entity.Comp.EnergyPerUse); args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count))); } } @@ -55,7 +58,7 @@ protected override void TryTurnOn(Entity entity, ref ItemTog { base.TryTurnOn(entity, ref args); - if (!TryComp(entity, out var battery) || battery.CurrentCharge < entity.Comp.EnergyPerUse) + if (!TryComp(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse) { args.Cancelled = true; if (args.User != null) @@ -67,7 +70,7 @@ protected override void TryTurnOn(Entity entity, ref ItemTog if (TryComp(entity, out var rig) && rig.IsRigged) { - _riggableSystem.Explode(entity.Owner, battery, args.User); + _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)), args.User); } } @@ -80,9 +83,10 @@ private void OnSolutionChange(Entity entity, ref SolutionCon return; if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged) - _riggableSystem.Explode(entity.Owner, battery); + _riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery))); } + // TODO: Not used anywhere? private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used) { RaiseLocalEvent(target, new PowerPulseEvent() @@ -95,7 +99,7 @@ private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used) private void OnChargeChanged(Entity entity, ref ChargeChangedEvent args) { if (TryComp(entity.Owner, out var battery) && - battery.CurrentCharge < entity.Comp.EnergyPerUse) + _battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse) { _itemToggle.TryDeactivate(entity.Owner, predicted: false); } diff --git a/Content.Server/SurveillanceCamera/CameraMapVisibilityWireAction.cs b/Content.Server/SurveillanceCamera/CameraMapVisibilityWireAction.cs new file mode 100644 index 00000000000..a673cf383f5 --- /dev/null +++ b/Content.Server/SurveillanceCamera/CameraMapVisibilityWireAction.cs @@ -0,0 +1,38 @@ +using Content.Server.Wires; +using Content.Shared.SurveillanceCamera.Components; +using Content.Shared.Wires; + +namespace Content.Server.SurveillanceCamera; + +public sealed partial class CameraMapVisibilityWireAction : ComponentWireAction +{ + private SurveillanceCameraMapSystem _cameraMapSystem => EntityManager.System(); + + public override string Name { get; set; } = "wire-name-camera-map"; + public override Color Color { get; set; } = Color.Teal; + public override object StatusKey => "OnMapVisibility"; + + public override StatusLightState? GetLightState(Wire wire, SurveillanceCameraComponent component) + { + return _cameraMapSystem.IsCameraVisible(wire.Owner) + ? StatusLightState.On + : StatusLightState.Off; + } + + public override bool Cut(EntityUid user, Wire wire, SurveillanceCameraComponent component) + { + _cameraMapSystem.SetCameraVisibility(wire.Owner, false); + return true; + } + + public override bool Mend(EntityUid user, Wire wire, SurveillanceCameraComponent component) + { + _cameraMapSystem.SetCameraVisibility(wire.Owner, true); + return true; + } + + public override void Pulse(EntityUid user, Wire wire, SurveillanceCameraComponent component) + { + + } +} diff --git a/Content.Server/SurveillanceCamera/Components/SurveillanceCameraComponent.cs b/Content.Server/SurveillanceCamera/Components/SurveillanceCameraComponent.cs deleted file mode 100644 index b0d41e52c74..00000000000 --- a/Content.Server/SurveillanceCamera/Components/SurveillanceCameraComponent.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Shared.DeviceNetwork; -using Robust.Shared.Prototypes; - -namespace Content.Server.SurveillanceCamera; - -[RegisterComponent] -[Access(typeof(SurveillanceCameraSystem))] -public sealed partial class SurveillanceCameraComponent : Component -{ - // List of active viewers. This is for bookkeeping purposes, - // so that when a camera shuts down, any entity viewing it - // will immediately have their subscription revoked. - [ViewVariables] - public HashSet ActiveViewers { get; } = new(); - - // Monitors != Viewers, as viewers are entities that are tied - // to a player session that's viewing from this camera - // - // Monitors are grouped sets of viewers, and may be - // completely different monitor types (e.g., monitor console, - // AI, etc.) - [ViewVariables] - public HashSet ActiveMonitors { get; } = new(); - - // If this camera is active or not. Deactivating a camera - // will not allow it to obtain any new viewers. - [ViewVariables] - public bool Active { get; set; } = true; - - // This one isn't easy to deal with. Will require a UI - // to change/set this so mapping these in isn't - // the most terrible thing possible. - [ViewVariables(VVAccess.ReadWrite)] - [DataField("id")] - public string CameraId { get; set; } = "camera"; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("nameSet")] - public bool NameSet { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("networkSet")] - public bool NetworkSet { get; set; } - - // This has to be device network frequency prototypes. - [DataField("setupAvailableNetworks")] - public List> AvailableNetworks { get; private set; } = new(); -} diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMapSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMapSystem.cs new file mode 100644 index 00000000000..efc9e2569fc --- /dev/null +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMapSystem.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using Content.Server.Power.Components; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.SurveillanceCamera.Components; + +namespace Content.Server.SurveillanceCamera; + +public sealed class SurveillanceCameraMapSystem : EntitySystem +{ + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnCameraMoved); + SubscribeLocalEvent(OnCameraUnpaused); + + SubscribeNetworkEvent(OnRequestCameraMarkerUpdate); + } + + private void OnCameraUnpaused(EntityUid uid, SurveillanceCameraComponent comp, ref EntityUnpausedEvent args) + { + if (Terminating(uid)) + return; + + UpdateCameraMarker((uid, comp)); + } + + private void OnCameraMoved(EntityUid uid, SurveillanceCameraComponent comp, ref MoveEvent args) + { + if (Terminating(uid)) + return; + + var oldGridUid = _transform.GetGrid(args.OldPosition); + var newGridUid = _transform.GetGrid(args.NewPosition); + + if (oldGridUid != newGridUid && oldGridUid is not null && !Terminating(oldGridUid.Value)) + { + if (TryComp(oldGridUid, out var oldMapComp)) + { + var netEntity = GetNetEntity(uid); + if (oldMapComp.Cameras.Remove(netEntity)) + Dirty(oldGridUid.Value, oldMapComp); + } + } + + if (newGridUid is not null && !Terminating(newGridUid.Value)) + UpdateCameraMarker((uid, comp)); + } + + private void OnRequestCameraMarkerUpdate(RequestCameraMarkerUpdateMessage args) + { + var cameraEntity = GetEntity(args.CameraEntity); + + if (TryComp(cameraEntity, out var comp) + && HasComp(cameraEntity)) + UpdateCameraMarker((cameraEntity, comp)); + } + + /// + /// Updates camera data in the SurveillanceCameraMapComponent for the specified camera entity. + /// + public void UpdateCameraMarker(Entity camera) + { + var (uid, comp) = camera; + + if (Terminating(uid)) + return; + + if (!TryComp(uid, out TransformComponent? xform) || !TryComp(uid, out DeviceNetworkComponent? deviceNet)) + return; + + var gridUid = xform.GridUid ?? xform.MapUid; + if (gridUid is null) + return; + + var netEntity = GetNetEntity(uid); + + var mapComp = EnsureComp(gridUid.Value); + var worldPos = _transform.GetWorldPosition(xform); + var gridMatrix = _transform.GetInvWorldMatrix(Transform(gridUid.Value)); + var localPos = Vector2.Transform(worldPos, gridMatrix); + + var address = deviceNet.Address; + var subnet = deviceNet.ReceiveFrequencyId ?? string.Empty; + var powered = CompOrNull(uid)?.Powered ?? true; + var active = comp.Active && powered; + + bool exists = mapComp.Cameras.TryGetValue(netEntity, out var existing); + + if (exists && + existing.Position.Equals(localPos) && + existing.Active == active && + existing.Address == address && + existing.Subnet == subnet) + { + return; + } + + var visible = exists ? existing.Visible : true; + + mapComp.Cameras[netEntity] = new CameraMarker + { + Position = localPos, + Active = active, + Address = address, + Subnet = subnet, + Visible = visible + }; + Dirty(gridUid.Value, mapComp); + } + + /// + /// Sets the visibility state of a camera on the camera map. + /// + public void SetCameraVisibility(EntityUid cameraUid, bool visible) + { + if (!TryComp(cameraUid, out TransformComponent? xform)) + return; + + var gridUid = xform.GridUid ?? xform.MapUid; + if (gridUid == null || !TryComp(gridUid.Value, out var mapComp)) + return; + + var netEntity = GetNetEntity(cameraUid); + if (mapComp.Cameras.TryGetValue(netEntity, out var marker)) + { + marker.Visible = visible; + mapComp.Cameras[netEntity] = marker; + Dirty(gridUid.Value, mapComp); + } + } + + /// + /// Checks if a camera is currently visible on the camera map. + /// + public bool IsCameraVisible(EntityUid cameraUid) + { + if (!TryComp(cameraUid, out TransformComponent? xform)) + return false; + + var gridUid = xform.GridUid ?? xform.MapUid; + if (gridUid == null || !TryComp(gridUid, out var mapComp)) + return false; + + var netEntity = GetNetEntity(cameraUid); + return mapComp.Cameras.TryGetValue(netEntity, out var marker) && marker.Visible; + } +} diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs index 4029488159a..723ddbf0ffb 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Chat.Systems; using Content.Shared.Speech; using Content.Shared.Speech.Components; +using Content.Shared.SurveillanceCamera.Components; using Content.Shared.Whitelist; using Robust.Shared.Player; using static Content.Server.Chat.Systems.ChatSystem; @@ -29,7 +30,7 @@ private void OnExpandRecipients(ExpandICChatRecipientsEvent ev) // This function ensures that chat popups appear on camera views that have connected microphones. foreach (var (_, __, camera, xform) in EntityQuery()) { - if (camera.ActiveViewers.Count == 0) + if (camera.ActivePvsViewers.Count == 0) continue; // get range to camera. This way wispers will still appear as obfuscated if they are too far from the camera's microphone @@ -40,7 +41,7 @@ private void OnExpandRecipients(ExpandICChatRecipientsEvent ev) if (range < 0 || range > ev.VoiceRange) continue; - foreach (var viewer in camera.ActiveViewers) + foreach (var viewer in camera.ActivePvsViewers) { // if the player has not already received the chat message, send it to them but don't log it to the chat // window. This is simply so that it appears in camera. @@ -61,7 +62,7 @@ private void OnInit(EntityUid uid, SurveillanceCameraMicrophoneComponent compone public void CanListen(EntityUid uid, SurveillanceCameraMicrophoneComponent microphone, ListenAttemptEvent args) { // TODO maybe just make this a part of ActiveListenerComponent? - if (_whitelistSystem.IsBlacklistPass(microphone.Blacklist, args.Source)) + if (_whitelistSystem.IsWhitelistPass(microphone.Blacklist, args.Source)) args.Cancel(); } diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs index 4f59654bb96..8a68e74855b 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs @@ -185,7 +185,7 @@ private void OnSwitchMessage(EntityUid uid, SurveillanceCameraMonitorComponent c // there would be a null check here, but honestly // whichever one is the "latest" switch message gets to // do the switch - TrySwitchCameraByAddress(uid, message.Address, component); + TrySwitchCameraByAddress(uid, message.Address, message.CameraSubnet, component); } private void OnPowerChanged(EntityUid uid, SurveillanceCameraMonitorComponent component, ref PowerChangedEvent args) @@ -426,15 +426,18 @@ private void SwitchCamera(EntityUid uid, EntityUid camera, SurveillanceCameraMon UpdateUserInterface(uid, monitor); } - private void TrySwitchCameraByAddress(EntityUid uid, string address, - SurveillanceCameraMonitorComponent? monitor = null) + private void TrySwitchCameraByAddress(EntityUid uid, string address, string? cameraSubnet = null, SurveillanceCameraMonitorComponent? monitor = null) { - if (!Resolve(uid, ref monitor) - || string.IsNullOrEmpty(monitor.ActiveSubnet) - || !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress)) - { + if (!Resolve(uid, ref monitor)) + return; + + if (cameraSubnet != null && cameraSubnet != monitor.ActiveSubnet) + SetActiveSubnet(uid, cameraSubnet, monitor); + + var activeSubnet = monitor.ActiveSubnet; + + if (string.IsNullOrEmpty(activeSubnet) || !monitor.KnownSubnets.TryGetValue(activeSubnet, out var subnetAddress)) return; - } var payload = new NetworkPayload() { diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs new file mode 100644 index 00000000000..4015a65cc99 --- /dev/null +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.Collide.cs @@ -0,0 +1,101 @@ +using Content.Shared.Power.EntitySystems; +using Content.Shared.SurveillanceCamera; +using Content.Shared.SurveillanceCamera.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; + +namespace Content.Server.SurveillanceCamera; + +public partial class SurveillanceCameraSystem +{ + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + [Dependency] private readonly EntityQuery _cameraQuery = default!; + + public void InitializeCollide() + { + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnStart); + SubscribeLocalEvent(OnEnd); + + SubscribeLocalEvent(OnCollideShutdown); + SubscribeLocalEvent(OnOverrideState); + } + + private void OnCollideShutdown(Entity ent, ref ComponentShutdown args) + { + // TODO: Check this on the event. + if (TerminatingOrDeleted(ent.Owner)) + return; + + // Regenerate contacts for everything we were colliding with. + var contacts = _physics.GetContacts(ent.Owner); + + while (contacts.MoveNext(out var contact)) + { + if (!contact.IsTouching) + continue; + + var other = contact.OtherEnt(ent.Owner); + + if (_cameraQuery.HasComp(other)) + { + _physics.RegenerateContacts(other); + } + } + } + + // You may be wondering what de fok this is doing here. + // At the moment there's no easy way to do collision whitelists based on components. + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (!_cameraQuery.HasComp(args.OtherEntity)) + { + args.Cancelled = true; + } + } + + private void OnEnd(Entity ent, ref EndCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + // TODO: Engine bug IsTouching box2d yay. + var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1; + + if (contacts > 0) + return; + + cameraCollider.Enabled = false; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnStart(Entity ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureId) + return; + + if (!_cameraQuery.TryComp(args.OtherEntity, out var cameraCollider)) + return; + + cameraCollider.Enabled = true; + Dirty(args.OtherEntity, cameraCollider); + UpdateVisuals(args.OtherEntity); + } + + private void OnOverrideState(Entity ent, ref SurveillanceCameraGetIsViewedExternallyEvent args) + { + if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner)) + return; + + if (!ent.Comp.Enabled) + return; + + args.Viewed = true; + } +} + diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs index 7f7dbc6c97c..09c2b39447f 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs @@ -1,13 +1,11 @@ using Content.Server.Administration.Logs; using Content.Server.DeviceNetwork.Systems; -using Content.Server.Emp; -using Content.Shared.ActionBlocker; using Content.Shared.Database; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Power; using Content.Shared.SurveillanceCamera; -using Content.Shared.Verbs; +using Content.Shared.SurveillanceCamera.Components; using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -15,16 +13,15 @@ namespace Content.Server.SurveillanceCamera; -public sealed class SurveillanceCameraSystem : EntitySystem +public sealed partial class SurveillanceCameraSystem : SharedSurveillanceCameraSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; - + [Dependency] private readonly SurveillanceCameraMapSystem _cameraMapSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; // Pings a surveillance camera subnet. All cameras will always respond // with a data message if they are on the same subnet. @@ -57,15 +54,15 @@ public sealed class SurveillanceCameraSystem : EntitySystem public override void Initialize() { + base.Initialize(); + SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnPacketReceived); SubscribeLocalEvent(OnSetName); SubscribeLocalEvent(OnSetNetwork); - SubscribeLocalEvent>(AddVerbs); - SubscribeLocalEvent(OnEmpPulse); - SubscribeLocalEvent(OnEmpDisabledRemoved); + InitializeCollide(); } private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args) @@ -86,7 +83,7 @@ private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent compone { { DeviceNetworkConstants.Command, string.Empty }, { CameraAddressData, deviceNet.Address }, - { CameraNameData, component.CameraId }, + { CameraNameData, component.UseEntityNameAsCameraId ? MetaData(uid).EntityName : component.CameraId }, { CameraSubnetData, string.Empty } }; @@ -131,26 +128,6 @@ private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent compone } } - private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent verbs) - { - if (!_actionBlocker.CanInteract(verbs.User, uid) || !_actionBlocker.CanComplexInteract(verbs.User)) - { - return; - } - - if (component.NameSet && component.NetworkSet) - { - return; - } - - AlternativeVerb verb = new(); - verb.Text = Loc.GetString("surveillance-camera-setup"); - verb.Act = () => OpenSetupInterface(uid, verbs.User, component); - verbs.Verbs.Add(verb); - } - - - private void OnPowerChanged(EntityUid camera, SurveillanceCameraComponent component, ref PowerChangedEvent args) { SetActive(camera, args.Powered, component); @@ -173,6 +150,7 @@ private void OnSetName(EntityUid uid, SurveillanceCameraComponent component, Sur component.CameraId = args.Name; component.NameSet = true; + Dirty(uid, component); UpdateSetupInterface(uid, component); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Actor)} set the name of {ToPrettyString(uid)} to \"{args.Name}.\""); } @@ -198,10 +176,11 @@ private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component, _deviceNetworkSystem.SetReceiveFrequency(uid, frequency.Frequency); component.NetworkSet = true; + Dirty(uid, component); UpdateSetupInterface(uid, component); } - private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null) + protected override void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null) { if (!Resolve(uid, ref camera)) return; @@ -238,7 +217,8 @@ private void UpdateSetupInterface(EntityUid uid, SurveillanceCameraComponent? ca } } - var state = new SurveillanceCameraSetupBoundUiState(camera.CameraId, deviceNet.ReceiveFrequency ?? 0, + var name = camera.UseEntityNameAsCameraId ? MetaData(uid).EntityName : camera.CameraId; + var state = new SurveillanceCameraSetupBoundUiState(name, deviceNet.ReceiveFrequency ?? 0, camera.AvailableNetworks, camera.NameSet, camera.NetworkSet); _userInterface.SetUiState(uid, SurveillanceCameraSetupUiKey.Camera, state); } @@ -254,7 +234,7 @@ private void Deactivate(EntityUid camera, SurveillanceCameraComponent? component var ev = new SurveillanceCameraDeactivateEvent(camera); - RemoveActiveViewers(camera, new(component.ActiveViewers), null, component); + RemoveActiveViewers(camera, new(component.ActivePvsViewers), null, component); component.Active = false; // Send a targetted event to all monitors. @@ -271,7 +251,26 @@ private void Deactivate(EntityUid camera, SurveillanceCameraComponent? component UpdateVisuals(camera, component); } - public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) + /// + /// Checks whether the camera is being viewed through by anyone at all. + /// + /// The camera to check + /// True if the camera is looked through, otherwise False. + public bool IsGettingViewed(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.ActivePvsViewers.Count > 0 || ent.Comp.ActiveMonitors.Count > 0) + return true; + + var ev = new SurveillanceCameraGetIsViewedExternallyEvent(); + RaiseLocalEvent(ent, ref ev); + + return ev.Viewed; + } + + public override void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null) { if (!Resolve(camera, ref component)) { @@ -292,6 +291,8 @@ public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponen } UpdateVisuals(camera, component); + + _cameraMapSystem.UpdateCameraMarker((camera, component)); } public void AddActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null) @@ -304,7 +305,8 @@ public void AddActiveViewer(EntityUid camera, EntityUid player, EntityUid? monit } _viewSubscriberSystem.AddViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Add(player); + + component.ActivePvsViewers.Add(player); if (monitor != null) { @@ -366,7 +368,7 @@ public void RemoveActiveViewer(EntityUid camera, EntityUid player, EntityUid? mo if (Resolve(player, ref actor)) _viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession); - component.ActiveViewers.Remove(player); + component.ActivePvsViewers.Remove(player); if (monitor != null) { @@ -411,28 +413,13 @@ private void UpdateVisuals(EntityUid uid, SurveillanceCameraComponent? component key = SurveillanceCameraVisuals.Active; } - if (component.ActiveViewers.Count > 0 || component.ActiveMonitors.Count > 0) + if (IsGettingViewed((uid, component))) { key = SurveillanceCameraVisuals.InUse; } _appearance.SetData(uid, SurveillanceCameraVisualsKey.Key, key, appearance); } - - private void OnEmpPulse(EntityUid uid, SurveillanceCameraComponent component, ref EmpPulseEvent args) - { - if (component.Active) - { - args.Affected = true; - args.Disabled = true; - SetActive(uid, false); - } - } - - private void OnEmpDisabledRemoved(EntityUid uid, SurveillanceCameraComponent component, ref EmpDisabledRemoved args) - { - SetActive(uid, true); - } } public sealed class OnSurveillanceCameraViewerAddEvent : EntityEventArgs diff --git a/Content.Server/Teleportation/TeleportLocationsSystem.cs b/Content.Server/Teleportation/TeleportLocationsSystem.cs index 4c8ee765d35..0493f7fb4db 100644 --- a/Content.Server/Teleportation/TeleportLocationsSystem.cs +++ b/Content.Server/Teleportation/TeleportLocationsSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Teleportation; using Content.Shared.Teleportation.Components; using Content.Shared.Teleportation.Systems; @@ -61,7 +62,7 @@ private void UpdateTeleportPoints(Entity ent) while (allEnts.MoveNext(out var warpEnt, out var warpPointComp)) { - if (_whitelist.IsBlacklistPass(warpPointComp.Blacklist, warpEnt) || string.IsNullOrWhiteSpace(warpPointComp.Location)) + if (_whitelist.IsWhitelistPass(warpPointComp.Blacklist, warpEnt) || string.IsNullOrWhiteSpace(warpPointComp.Location)) continue; ent.Comp.AvailableWarps.Add(new TeleportPoint(warpPointComp.Location, GetNetEntity(warpEnt))); diff --git a/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs new file mode 100644 index 00000000000..7c7b1e81e83 --- /dev/null +++ b/Content.Server/Temperature/Components/ContainerTemperatureComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Temperature.Components; + +[RegisterComponent] +public sealed partial class ContainerTemperatureComponent : Component +{ + [DataField] + public float? HeatDamageThreshold; + + [DataField] + public float? ColdDamageThreshold; +} diff --git a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs deleted file mode 100644 index 024b8a013b3..00000000000 --- a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Content.Server.Temperature.Components; - -[RegisterComponent] -public sealed partial class ContainerTemperatureDamageThresholdsComponent: Component -{ - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float? HeatDamageThreshold; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float? ColdDamageThreshold; -} diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs deleted file mode 100644 index 4669df60f70..00000000000 --- a/Content.Server/Temperature/Components/TemperatureComponent.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Content.Shared.Alert; -using Content.Shared.Atmos; -using Content.Shared.Damage; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Server.Temperature.Components; - -/// -/// Handles changing temperature, -/// informing others of the current temperature, -/// and taking fire damage from high temperature. -/// -[RegisterComponent] -public sealed partial class TemperatureComponent : Component -{ - /// - /// Surface temperature which is modified by the environment. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float CurrentTemperature = Atmospherics.T20C; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float HeatDamageThreshold = 360f; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float ColdDamageThreshold = 160f; - - /// - /// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float? ParentHeatDamageThreshold; - - /// - /// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float? ParentColdDamageThreshold; - - /// - /// Heat capacity per kg of mass. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float SpecificHeat = 50f; - - /// - /// How well does the air surrounding you merge into your body temperature? - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float AtmosTemperatureTransferEfficiency = 0.1f; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier ColdDamage = new(); - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier HeatDamage = new(); - - /// - /// Temperature won't do more than this amount of damage per second. - /// - /// - /// Okay it genuinely reaches this basically immediately for a plasma fire. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 DamageCap = FixedPoint2.New(8); - - /// - /// Used to keep track of when damage starts/stops. Useful for logs. - /// - [DataField] - public bool TakingDamage; - - [DataField] - public ProtoId HotAlert = "Hot"; - - [DataField] - public ProtoId ColdAlert = "Cold"; -} diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs b/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs new file mode 100644 index 00000000000..d436977daf3 --- /dev/null +++ b/Content.Server/Temperature/Systems/TemperatureSystem.Damage.cs @@ -0,0 +1,279 @@ +using Content.Server.Administration.Logs; +using Content.Server.Body.Components; +using Content.Server.Temperature.Components; +using Content.Shared.Alert; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Database; +using Content.Shared.Rounding; +using Content.Shared.Temperature; +using Content.Shared.Temperature.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Temperature.Systems; + +/// +/// Handles entities taking damage from being too hot or too cold. +/// Also handles alerts relevant to the same. +/// +public sealed partial class TemperatureSystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private EntityQuery _tempDamageQuery; + private EntityQuery _containerTemperatureQuery; + private EntityQuery _thermalRegulatorQuery; + + /// + /// All the components that will have their damage updated at the end of the tick. + /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning + /// that we need some mechanism to ensure it doesn't double-dip on damage for both calls. + /// + public HashSet> ShouldUpdateDamage = new(); + + /// + /// Alert prototype for Temperature. + /// + public static readonly ProtoId TemperatureAlertCategory = "Temperature"; + + /// + /// The maximum severity applicable to temperature alerts. + /// + public static readonly short MaxTemperatureAlertSeverity = 3; + + /// + /// On a scale of 0. to 1. where 0. is the ideal temperature and 1. is a temperature damage threshold this is the point where the component starts raising temperature alerts. + /// + public static readonly float MinAlertTemperatureScale = 0.33f; + + private void InitializeDamage() + { + SubscribeLocalEvent(ServerAlert); + + SubscribeLocalEvent(EnqueueDamage); + SubscribeLocalEvent(OnUnpaused); + + // Allows overriding thresholds based on the parent's thresholds. + SubscribeLocalEvent(OnParentChange); + SubscribeLocalEvent(OnParentThresholdStartup); + SubscribeLocalEvent(OnParentThresholdShutdown); + + _tempDamageQuery = GetEntityQuery(); + _containerTemperatureQuery = GetEntityQuery(); + _thermalRegulatorQuery = GetEntityQuery(); + } + + private void UpdateDamage() + { + foreach (var entity in ShouldUpdateDamage) + { + if (Deleted(entity) || Paused(entity)) + continue; + + var deltaTime = _gameTiming.CurTime - entity.Comp.LastUpdate; + if (entity.Comp.TakingDamage && deltaTime < entity.Comp.UpdateInterval) + continue; + + ChangeDamage(entity, deltaTime); + } + + ShouldUpdateDamage.Clear(); + } + + private void ChangeDamage(Entity entity, TimeSpan deltaTime) + { + entity.Comp.LastUpdate = _gameTiming.CurTime; + + if (!HasComp(entity) || !TemperatureQuery.TryComp(entity, out var temperature)) + return; + + // See this link for where the scaling func comes from: + // https://www.desmos.com/calculator/0vknqtdvq9 + // Based on a logistic curve, which caps out at MaxDamage + var heatK = 0.005; + var a = 1; + var y = entity.Comp.DamageCap; + var c = y * 2; + + var heatDamageThreshold = entity.Comp.ParentHeatDamageThreshold ?? entity.Comp.HeatDamageThreshold; + var coldDamageThreshold = entity.Comp.ParentColdDamageThreshold ?? entity.Comp.ColdDamageThreshold; + + if (temperature.CurrentTemperature >= heatDamageThreshold) + { + if (!entity.Comp.TakingDamage) + { + _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking high temperature damage"); + entity.Comp.TakingDamage = true; + } + + var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold); + var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y; + _damageable.TryChangeDamage(entity.Owner, entity.Comp.HeatDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false); + } + else if (temperature.CurrentTemperature <= coldDamageThreshold) + { + if (!entity.Comp.TakingDamage) + { + _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking low temperature damage"); + entity.Comp.TakingDamage = true; + } + + var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold); + var tempDamage = + Math.Sqrt(diff * (Math.Pow(entity.Comp.DamageCap.Double(), 2) / coldDamageThreshold)); + _damageable.TryChangeDamage(entity.Owner, entity.Comp.ColdDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false); + } + else if (entity.Comp.TakingDamage) + { + _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} stopped taking temperature damage"); + entity.Comp.TakingDamage = false; + } + } + + private void ServerAlert(Entity entity, ref OnTemperatureChangeEvent args) + { + ProtoId type; + float threshold; + float idealTemp; + + if (!_tempDamageQuery.TryComp(entity, out var thresholds)) + { + _alerts.ClearAlertCategory(entity.Owner, TemperatureAlertCategory); + return; + } + + if (_thermalRegulatorQuery.TryComp(entity, out var regulator) && + regulator.NormalBodyTemperature > thresholds.ColdDamageThreshold && + regulator.NormalBodyTemperature < thresholds.HeatDamageThreshold) + { + idealTemp = regulator.NormalBodyTemperature; + } + else + { + idealTemp = (thresholds.ColdDamageThreshold + thresholds.HeatDamageThreshold) / 2; + } + + if (args.CurrentTemperature <= idealTemp) + { + type = thresholds.ColdAlert; + threshold = thresholds.ColdDamageThreshold; + } + else + { + type = thresholds.HotAlert; + threshold = thresholds.HeatDamageThreshold; + } + + // Calculates a scale where 0.0 is the ideal temperature and 1.0 is where temperature damage begins + // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds + var tempScale = (args.CurrentTemperature - idealTemp) / (threshold - idealTemp); + var alertLevel = (short)ContentHelpers.RoundToLevels(tempScale - MinAlertTemperatureScale, 1.00f - MinAlertTemperatureScale, MaxTemperatureAlertSeverity + 1); + + if (alertLevel > 0) + _alerts.ShowAlert(entity.AsNullable(), type, alertLevel); + else + _alerts.ClearAlertCategory(entity.AsNullable(), TemperatureAlertCategory); + } + + private void EnqueueDamage(Entity ent, ref OnTemperatureChangeEvent args) + { + if (ShouldUpdateDamage.Add(ent) && !ent.Comp.TakingDamage) + ent.Comp.LastUpdate = _gameTiming.CurTime; + } + + private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + ent.Comp.LastUpdate += args.PausedTime; + } + + private void OnParentChange(Entity entity, ref EntParentChangedMessage args) + { + // We only need to update thresholds if the thresholds changed for the entity's ancestors. + var oldThresholds = args.OldParent != null + ? RecalculateParentThresholds(args.OldParent.Value) + : (null, null); + var xform = Transform(entity.Owner); + var newThresholds = RecalculateParentThresholds(xform.ParentUid); + + if (oldThresholds != newThresholds) + RecursiveThresholdUpdate((entity, entity.Comp, xform)); + } + + private void OnParentThresholdStartup(Entity entity, ref ComponentStartup args) + { + RecursiveThresholdUpdate(entity.Owner); + } + + private void OnParentThresholdShutdown(Entity entity, ref ComponentShutdown args) + { + RecursiveThresholdUpdate(entity.Owner); + } + + /// + /// Recalculate and apply parent thresholds for the root entity and all its children. + /// + /// The root entity we're currently updating + private void RecursiveThresholdUpdate(Entity root) + { + RecalculateAndApplyParentThresholds(root); + + var xform = root.Comp2 ?? Transform(root); + var enumerator = xform.ChildEnumerator; + while (enumerator.MoveNext(out var child)) + { + RecursiveThresholdUpdate(child); + } + } + + /// + /// Recalculate parent thresholds and apply them on the uid temperature component. + /// + /// The entity whose temperature damage thresholds we're updating + private void RecalculateAndApplyParentThresholds(Entity entity) + { + if (!_tempDamageQuery.Resolve(entity, ref entity.Comp, logMissing: false)) + return; + + var newThresholds = RecalculateParentThresholds(Transform(entity).ParentUid); + entity.Comp.ParentHeatDamageThreshold = newThresholds.Item1; + entity.Comp.ParentColdDamageThreshold = newThresholds.Item2; + } + + /// + /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the + /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each). + /// + /// parent we start with + private (float?, float?) RecalculateParentThresholds(EntityUid initialParentUid) + { + // Recursively check parents for the best threshold available + var parentUid = initialParentUid; + float? newHeatThreshold = null; + float? newColdThreshold = null; + while (parentUid.IsValid()) + { + if (_containerTemperatureQuery.TryComp(parentUid, out var newThresholds)) + { + if (newThresholds.HeatDamageThreshold != null) + { + newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value, + newHeatThreshold ?? 0); + } + + if (newThresholds.ColdDamageThreshold != null) + { + newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value, + newColdThreshold ?? float.MaxValue); + } + } + + parentUid = Transform(parentUid).ParentUid; + } + + return (newHeatThreshold, newColdThreshold); + } +} diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index ff1f300e79a..484912c780e 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -1,61 +1,32 @@ -using System.Linq; -using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; using Content.Server.Temperature.Components; -using Content.Shared.Alert; using Content.Shared.Atmos; -using Content.Shared.Damage; -using Content.Shared.Database; using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Temperature; -using Robust.Shared.Physics.Components; -using Robust.Shared.Prototypes; -using Robust.Shared.Physics.Events; using Content.Shared.Projectiles; +using Content.Shared.Temperature.Components; +using Content.Shared.Temperature.Systems; namespace Content.Server.Temperature.Systems; -public sealed class TemperatureSystem : EntitySystem +public sealed partial class TemperatureSystem : SharedTemperatureSystem { - [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly TemperatureSystem _temperature = default!; - - /// - /// All the components that will have their damage updated at the end of the tick. - /// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning - /// that we need some mechanism to ensure it doesn't double dip on damage for both calls. - /// - public HashSet> ShouldUpdateDamage = new(); - - public float UpdateInterval = 1.0f; - - private float _accumulatedFrametime; - - public static readonly ProtoId TemperatureAlertCategory = "Temperature"; public override void Initialize() { - SubscribeLocalEvent(EnqueueDamage); + base.Initialize(); + SubscribeLocalEvent(OnAtmosExposedUpdate); SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(ServerAlert); Subs.SubscribeWithRelay(OnTemperatureChangeAttempt, held: false); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(ChangeTemperatureOnCollide); - // Allows overriding thresholds based on the parent's thresholds. - SubscribeLocalEvent(OnParentChange); - SubscribeLocalEvent( - OnParentThresholdStartup); - SubscribeLocalEvent( - OnParentThresholdShutdown); + InitializeDamage(); } public override void Update(float frameTime) @@ -85,50 +56,23 @@ public override void Update(float frameTime) ForceChangeTemperature(uid, temp.CurrentTemperature - degrees, temp); } - UpdateDamage(frameTime); - } - - private void UpdateDamage(float frameTime) - { - _accumulatedFrametime += frameTime; - - if (_accumulatedFrametime < UpdateInterval) - return; - _accumulatedFrametime -= UpdateInterval; - - if (!ShouldUpdateDamage.Any()) - return; - - foreach (var comp in ShouldUpdateDamage) - { - MetaDataComponent? metaData = null; - - var uid = comp.Owner; - if (Deleted(uid, metaData) || Paused(uid, metaData)) - continue; - - ChangeDamage(uid, comp); - } - - ShouldUpdateDamage.Clear(); + UpdateDamage(); } public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null) { - if (!Resolve(uid, ref temperature)) + if (!TemperatureQuery.Resolve(uid, ref temperature)) return; - float lastTemp = temperature.CurrentTemperature; - float delta = temperature.CurrentTemperature - temp; + var lastTemp = temperature.CurrentTemperature; + var delta = temperature.CurrentTemperature - temp; temperature.CurrentTemperature = temp; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), - true); + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true); } - public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, - TemperatureComponent? temperature = null) + public override void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null) { - if (!Resolve(uid, ref temperature, false)) + if (!TemperatureQuery.Resolve(uid, ref temperature, false)) return; if (!ignoreHeatResistance) @@ -142,11 +86,10 @@ public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistanc temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature); float delta = temperature.CurrentTemperature - lastTemp; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true); + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true); } - private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, - ref AtmosExposedUpdateEvent args) + private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, ref AtmosExposedUpdateEvent args) { var transform = args.Transform; @@ -156,27 +99,18 @@ private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperatur var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature; var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false); var heatCapacity = GetHeatCapacity(uid, temperature); + // TODO ATMOS: This heat transfer formula is really really wrong, it needs to be pulled out. Pending on HeatContainers. var heat = temperatureDelta * (airHeatCapacity * heatCapacity / (airHeatCapacity + heatCapacity)); ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); } - public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null) - { - if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0) - { - return Atmospherics.MinimumHeatCapacity; - } - - return comp.SpecificHeat * physics.FixturesMass; - } - - private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args) + private void OnInit(Entity entity, ref MapInitEvent args) { - if (!TryComp(uid, out var temp)) + if (!TemperatureQuery.TryComp(entity, out var temp)) return; - comp.Temperature = temp.CurrentTemperature; + entity.Comp.Temperature = temp.CurrentTemperature; } private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args) @@ -184,116 +118,6 @@ private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEv ForceChangeTemperature(uid, Atmospherics.T20C, comp); } - private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args) - { - ProtoId type; - float threshold; - float idealTemp; - - if (!TryComp(uid, out var temperature)) - { - _alerts.ClearAlertCategory(uid, TemperatureAlertCategory); - return; - } - - if (TryComp(uid, out var regulator) && - regulator.NormalBodyTemperature > temperature.ColdDamageThreshold && - regulator.NormalBodyTemperature < temperature.HeatDamageThreshold) - { - idealTemp = regulator.NormalBodyTemperature; - } - else - { - idealTemp = (temperature.ColdDamageThreshold + temperature.HeatDamageThreshold) / 2; - } - - if (args.CurrentTemperature <= idealTemp) - { - type = temperature.ColdAlert; - threshold = temperature.ColdDamageThreshold; - } - else - { - type = temperature.HotAlert; - threshold = temperature.HeatDamageThreshold; - } - - // Calculates a scale where 1.0 is the ideal temperature and 0.0 is where temperature damage begins - // The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds - var tempScale = (args.CurrentTemperature - threshold) / (idealTemp - threshold); - switch (tempScale) - { - case <= 0f: - _alerts.ShowAlert(uid, type, 3); - break; - - case <= 0.4f: - _alerts.ShowAlert(uid, type, 2); - break; - - case <= 0.66f: - _alerts.ShowAlert(uid, type, 1); - break; - - case > 0.66f: - _alerts.ClearAlertCategory(uid, TemperatureAlertCategory); - break; - } - } - - private void EnqueueDamage(Entity temperature, ref OnTemperatureChangeEvent args) - { - ShouldUpdateDamage.Add(temperature); - } - - private void ChangeDamage(EntityUid uid, TemperatureComponent temperature) - { - if (!HasComp(uid)) - return; - - // See this link for where the scaling func comes from: - // https://www.desmos.com/calculator/0vknqtdvq9 - // Based on a logistic curve, which caps out at MaxDamage - var heatK = 0.005; - var a = 1; - var y = temperature.DamageCap; - var c = y * 2; - - var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold; - var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold; - - if (temperature.CurrentTemperature >= heatDamageThreshold) - { - if (!temperature.TakingDamage) - { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking high temperature damage"); - temperature.TakingDamage = true; - } - - var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold); - var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y; - _damageable.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false); - } - else if (temperature.CurrentTemperature <= coldDamageThreshold) - { - if (!temperature.TakingDamage) - { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking low temperature damage"); - temperature.TakingDamage = true; - } - - var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold); - var tempDamage = - Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold)); - //_damageable.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false); // imperial medieval ships no cold damage - } - else if (temperature.TakingDamage) - { - _adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} stopped taking temperature damage"); - temperature.TakingDamage = false; - } - } - private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component, ModifyChangedTemperatureEvent args) { var coefficient = args.TemperatureDelta < 0 @@ -308,118 +132,6 @@ private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComp private void ChangeTemperatureOnCollide(Entity ent, ref ProjectileHitEvent args) { - _temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature - } - - private void OnParentChange(EntityUid uid, TemperatureComponent component, - ref EntParentChangedMessage args) - { - var temperatureQuery = GetEntityQuery(); - var transformQuery = GetEntityQuery(); - var thresholdsQuery = GetEntityQuery(); - // We only need to update thresholds if the thresholds changed for the entity's ancestors. - var oldThresholds = args.OldParent != null - ? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery) - : (null, null); - var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery); - - if (oldThresholds != newThresholds) - { - RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery); - } - } - - private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, - ComponentStartup args) - { - RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), - GetEntityQuery()); - } - - private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, - ComponentShutdown args) - { - RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), - GetEntityQuery()); - } - - /// - /// Recalculate and apply parent thresholds for the root entity and all its descendant. - /// - /// - /// - /// - /// - private void RecursiveThresholdUpdate(EntityUid root, EntityQuery temperatureQuery, - EntityQuery transformQuery, - EntityQuery tempThresholdsQuery) - { - RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery); - - var enumerator = Transform(root).ChildEnumerator; - while (enumerator.MoveNext(out var child)) - { - RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery); - } - } - - /// - /// Recalculate parent thresholds and apply them on the uid temperature component. - /// - /// - /// - /// - /// - private void RecalculateAndApplyParentThresholds(EntityUid uid, - EntityQuery temperatureQuery, EntityQuery transformQuery, - EntityQuery tempThresholdsQuery) - { - if (!temperatureQuery.TryGetComponent(uid, out var temperature)) - { - return; - } - - var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery); - temperature.ParentHeatDamageThreshold = newThresholds.Item1; - temperature.ParentColdDamageThreshold = newThresholds.Item2; - } - - /// - /// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the - /// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each). - /// - /// - /// - /// - private (float?, float?) RecalculateParentThresholds( - EntityUid initialParentUid, - EntityQuery transformQuery, - EntityQuery tempThresholdsQuery) - { - // Recursively check parents for the best threshold available - var parentUid = initialParentUid; - float? newHeatThreshold = null; - float? newColdThreshold = null; - while (parentUid.IsValid()) - { - if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds)) - { - if (newThresholds.HeatDamageThreshold != null) - { - newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value, - newHeatThreshold ?? 0); - } - - if (newThresholds.ColdDamageThreshold != null) - { - newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value, - newColdThreshold ?? float.MaxValue); - } - } - - parentUid = transformQuery.GetComponent(parentUid).ParentUid; - } - - return (newHeatThreshold, newColdThreshold); + ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature } } diff --git a/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs b/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs index 4fd2f9b6ed0..d418ea5a18e 100644 --- a/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs +++ b/Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs @@ -1,7 +1,7 @@ -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Tesla.Components; using Content.Server.Lightning; +using Content.Shared.Power.Components; namespace Content.Server.Tesla.EntitySystems; @@ -24,7 +24,7 @@ private void OnHitByLightning(Entity coil, ref HitByLightnin { if (TryComp(coil, out var batteryComponent)) { - _battery.SetCharge(coil, batteryComponent.CurrentCharge + coil.Comp.ChargeFromLightning); + _battery.ChangeCharge((coil, batteryComponent), coil.Comp.ChargeFromLightning); } } } diff --git a/Content.Server/Thief/Components/ThiefBeaconComponent.cs b/Content.Server/Thief/Components/ThiefBeaconComponent.cs deleted file mode 100644 index 65db79f8617..00000000000 --- a/Content.Server/Thief/Components/ThiefBeaconComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.Thief.Systems; -using Robust.Shared.Audio; - -namespace Content.Server.Thief.Components; - -/// -/// working together with StealAreaComponent, allows the thief to count objects near the beacon as stolen when setting up. -/// -[RegisterComponent, Access(typeof(ThiefBeaconSystem))] -public sealed partial class ThiefBeaconComponent : Component -{ - [DataField] - public SoundSpecifier LinkSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg"); - - [DataField] - public SoundSpecifier UnlinkSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg"); -} diff --git a/Content.Server/Thief/Components/ThiefUndeterminedBackpackComponent.cs b/Content.Server/Thief/Components/ThiefUndeterminedBackpackComponent.cs index 9080caa2456..e4fc1310d38 100644 --- a/Content.Server/Thief/Components/ThiefUndeterminedBackpackComponent.cs +++ b/Content.Server/Thief/Components/ThiefUndeterminedBackpackComponent.cs @@ -30,6 +30,18 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component [DataField] public int MaxSelectedSets = 2; + /// + /// Title field for undetermined equipment ui. + /// + [DataField] + public LocId ToolName = "thief-backpack-window-title"; + + /// + /// Description field for undetermined equipment ui. + /// + [DataField] + public LocId ToolDesc = "thief-backpack-window-description"; + /// /// What entity all the spawned items will appear inside of /// If null, will instead drop on the ground. diff --git a/Content.Server/Thief/Systems/ThiefBeaconSystem.cs b/Content.Server/Thief/Systems/ThiefBeaconSystem.cs deleted file mode 100644 index 069966a8a43..00000000000 --- a/Content.Server/Thief/Systems/ThiefBeaconSystem.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Content.Server.Mind; -using Content.Server.Objectives.Components; -using Content.Server.Thief.Components; -using Content.Shared.Examine; -using Content.Shared.Foldable; -using Content.Shared.Popups; -using Content.Shared.Verbs; -using Content.Shared.Roles; -using Content.Shared.Roles.Components; -using Robust.Shared.Audio.Systems; - -namespace Content.Server.Thief.Systems; - -/// -/// -/// -public sealed class ThiefBeaconSystem : EntitySystem -{ - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly MindSystem _mind = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnGetInteractionVerbs); - SubscribeLocalEvent(OnFolded); - SubscribeLocalEvent(OnExamined); - } - - private void OnGetInteractionVerbs(Entity beacon, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands is null) - return; - - if (TryComp(beacon, out var foldable) && foldable.IsFolded) - return; - - var mind = _mind.GetMind(args.User); - if (mind == null || !_roles.MindHasRole(mind.Value)) - return; - - var user = args.User; - args.Verbs.Add(new() - { - Act = () => - { - SetCoordinate(beacon, mind.Value); - }, - Message = Loc.GetString("thief-fulton-verb-message"), - Text = Loc.GetString("thief-fulton-verb-text"), - }); - } - - private void OnFolded(Entity beacon, ref FoldedEvent args) - { - if (args.IsFolded) - ClearCoordinate(beacon); - } - - private void OnExamined(Entity beacon, ref ExaminedEvent args) - { - if (!TryComp(beacon, out var area)) - return; - - args.PushText(Loc.GetString(area.Owners.Count == 0 - ? "thief-fulton-examined-unset" - : "thief-fulton-examined-set")); - } - - private void SetCoordinate(Entity beacon, EntityUid mind) - { - if (!TryComp(beacon, out var area)) - return; - - _audio.PlayPvs(beacon.Comp.LinkSound, beacon); - _popup.PopupEntity(Loc.GetString("thief-fulton-set"), beacon); - area.Owners.Clear(); //We only reconfigure the beacon for ourselves, we don't need multiple thieves to steal from the same beacon. - area.Owners.Add(mind); - } - - private void ClearCoordinate(Entity beacon) - { - if (!TryComp(beacon, out var area)) - return; - - if (area.Owners.Count == 0) - return; - - _audio.PlayPvs(beacon.Comp.UnlinkSound, beacon); - _popup.PopupEntity(Loc.GetString("thief-fulton-clear"), beacon); - area.Owners.Clear(); - } -} diff --git a/Content.Server/Thief/Systems/ThiefUndeterminedBackpackSystem.cs b/Content.Server/Thief/Systems/ThiefUndeterminedBackpackSystem.cs index 23f845a2e73..65d8c5d0505 100644 --- a/Content.Server/Thief/Systems/ThiefUndeterminedBackpackSystem.cs +++ b/Content.Server/Thief/Systems/ThiefUndeterminedBackpackSystem.cs @@ -96,6 +96,6 @@ private void UpdateUI(EntityUid uid, ThiefUndeterminedBackpackComponent? compone data.Add(i, info); } - _ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, component.MaxSelectedSets)); + _ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, component.MaxSelectedSets, component.ToolName, component.ToolDesc)); } } diff --git a/Content.Server/Tiles/TileEntityEffectComponent.cs b/Content.Server/Tiles/TileEntityEffectComponent.cs index 4201af47f9e..70f11bb0605 100644 --- a/Content.Server/Tiles/TileEntityEffectComponent.cs +++ b/Content.Server/Tiles/TileEntityEffectComponent.cs @@ -14,6 +14,6 @@ public sealed partial class TileEntityEffectComponent : Component /// /// List of effects that should be applied. /// - [ViewVariables(VVAccess.ReadWrite), DataField] + [DataField] public List Effects = default!; } diff --git a/Content.Server/Tiles/TileEntityEffectSystem.cs b/Content.Server/Tiles/TileEntityEffectSystem.cs index 4d866cb2548..1123781e7b4 100644 --- a/Content.Server/Tiles/TileEntityEffectSystem.cs +++ b/Content.Server/Tiles/TileEntityEffectSystem.cs @@ -1,13 +1,11 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; using Content.Shared.StepTrigger.Systems; -using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects; namespace Content.Server.Tiles; public sealed class TileEntityEffectSystem : EntitySystem { + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; public override void Initialize() { @@ -23,11 +21,7 @@ private void OnTileStepTriggerAttempt(Entity ent, ref private void OnTileStepTriggered(Entity ent, ref StepTriggeredOffEvent args) { var otherUid = args.Tripper; - var effectArgs = new EntityEffectBaseArgs(otherUid, EntityManager); - foreach (var effect in ent.Comp.Effects) - { - effect.Effect(effectArgs); - } + _entityEffects.ApplyEffects(otherUid, ent.Comp.Effects.ToArray(), user: otherUid); } } diff --git a/Content.Server/Tips/TipsSystem.cs b/Content.Server/Tips/TipsSystem.cs index cd7f7b52f54..406ee76ca6b 100644 --- a/Content.Server/Tips/TipsSystem.cs +++ b/Content.Server/Tips/TipsSystem.cs @@ -4,10 +4,7 @@ using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Tips; -using Robust.Server.GameObjects; -using Robust.Server.Player; using Robust.Shared.Configuration; -using Robust.Shared.Console; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -15,10 +12,7 @@ namespace Content.Server.Tips; -/// -/// Handles periodically displaying gameplay tips to all players ingame. -/// -public sealed class TipsSystem : EntitySystem +public sealed class TipsSystem : SharedTipsSystem { [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; @@ -26,8 +20,6 @@ public sealed class TipsSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly GameTicker _ticker = default!; - [Dependency] private readonly IConsoleHost _conHost = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; private bool _tipsEnabled; private float _tipTimeOutOfRound; @@ -35,16 +27,6 @@ public sealed class TipsSystem : EntitySystem private string _tipsDataset = ""; private float _tipTippyChance; - /// - /// Always adds this time to a speech message. This is so really short message stay around for a bit. - /// - private const float SpeechBuffer = 3f; - - /// - /// Expected reading speed. - /// - private const float Wpm = 180f; - [ViewVariables(VVAccess.ReadWrite)] private TimeSpan _nextTipTime = TimeSpan.Zero; @@ -53,110 +35,45 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnGameRunLevelChanged); - Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, SetOutOfRound, true); - Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true); Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true); - Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true); - Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true); + Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, value => _tipTimeOutOfRound = value, true); + Subs.CVar(_cfg, CCVars.TipFrequencyInRound, value => _tipTimeInRound = value, true); + Subs.CVar(_cfg, CCVars.TipsDataset, value => _tipsDataset = value, true); + Subs.CVar(_cfg, CCVars.TipsTippyChance, value => _tipTippyChance = value, true); RecalculateNextTipTime(); - _conHost.RegisterCommand("tippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendTippy, SendTippyHelper); - _conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip); } - private CompletionResult SendTippyHelper(IConsoleShell shell, string[] args) + private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev) { - return args.Length switch + // reset for lobby -> inround + // reset for inround -> post but not post -> lobby + if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound) { - 1 => CompletionResult.FromHintOptions( - CompletionHelper.SessionNames(players: _playerManager), - Loc.GetString("cmd-tippy-auto-1")), - 2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")), - 3 => CompletionResult.FromHintOptions( - CompletionHelper.PrototypeIdsLimited(args[2], _prototype), - Loc.GetString("cmd-tippy-auto-3")), - 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")), - 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), - 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), - _ => CompletionResult.Empty - }; + RecalculateNextTipTime(); + } } - private void SendTip(IConsoleShell shell, string argstr, string[] args) + private void SetEnabled(bool value) { - AnnounceRandomTip(); - RecalculateNextTipTime(); + _tipsEnabled = value; + + if (_nextTipTime != TimeSpan.Zero) + RecalculateNextTipTime(); } - private void SendTippy(IConsoleShell shell, string argstr, string[] args) + public override void RecalculateNextTipTime() { - if (args.Length < 2) - { - shell.WriteLine(Loc.GetString("cmd-tippy-help")); - return; - } - - ActorComponent? actor = null; - if (args[0] != "all") + if (_ticker.RunLevel == GameRunLevel.InRound) { - ICommonSession? session; - if (args.Length > 0) - { - // Get player entity - if (!_playerManager.TryGetSessionByUsername(args[0], out session)) - { - shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); - return; - } - } - else - { - session = shell.Player; - } - - if (session?.AttachedEntity is not { } user) - { - shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); - return; - } - - if (!TryComp(user, out actor)) - { - shell.WriteError(Loc.GetString("cmd-tippy-error-no-user")); - return; - } + _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound); } - - var ev = new TippyEvent(args[1]); - - if (args.Length > 2) + else { - ev.Proto = args[2]; - if (!_prototype.HasIndex(args[2])) - { - shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2]))); - return; - } + _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound); } - - if (args.Length > 3) - ev.SpeakTime = float.Parse(args[3]); - else - ev.SpeakTime = GetSpeechTime(ev.Msg); - - if (args.Length > 4) - ev.SlideTime = float.Parse(args[4]); - - if (args.Length > 5) - ev.WaddleInterval = float.Parse(args[5]); - - if (actor != null) - RaiseNetworkEvent(ev, actor.PlayerSession); - else - RaiseNetworkEvent(ev); } - public override void Update(float frameTime) { base.Update(frameTime); @@ -171,41 +88,30 @@ public override void Update(float frameTime) } } - private void SetOutOfRound(float value) + public override void SendTippy( + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) { - _tipTimeOutOfRound = value; + var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval); + RaiseNetworkEvent(ev); } - private void SetInRound(float value) + public override void SendTippy( + ICommonSession session, + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) { - _tipTimeInRound = value; + var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval); + RaiseNetworkEvent(ev, session); } - private void SetEnabled(bool value) - { - _tipsEnabled = value; - - if (_nextTipTime != TimeSpan.Zero) - RecalculateNextTipTime(); - } - - private void SetDataset(string value) - { - _tipsDataset = value; - } - - private void SetTippyChance(float value) - { - _tipTippyChance = value; - } - - public static float GetSpeechTime(string text) - { - var wordCount = (float)text.Split().Length; - return SpeechBuffer + wordCount * (60f / Wpm); - } - - private void AnnounceRandomTip() + public override void AnnounceRandomTip() { if (!_prototype.TryIndex(_tipsDataset, out var tips)) return; @@ -215,35 +121,20 @@ private void AnnounceRandomTip() if (_random.Prob(_tipTippyChance)) { - var ev = new TippyEvent(msg); - ev.SpeakTime = GetSpeechTime(msg); - RaiseNetworkEvent(ev); - } else - { - _chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg, - EntityUid.Invalid, false, false, Color.MediumPurple); - } - } - - private void RecalculateNextTipTime() - { - if (_ticker.RunLevel == GameRunLevel.InRound) - { - _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound); + var speakTime = GetSpeechTime(msg); + SendTippy(msg, speakTime: speakTime); } else { - _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound); - } - } - - private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev) - { - // reset for lobby -> inround - // reset for inround -> post but not post -> lobby - if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound) - { - RecalculateNextTipTime(); + _chat.ChatMessageToManyFiltered( + Filter.Broadcast(), + ChatChannel.OOC, + tip, + msg, + EntityUid.Invalid, + false, + false, + Color.MediumPurple); } } } diff --git a/Content.Server/Tools/Innate/InnateToolSystem.cs b/Content.Server/Tools/Innate/InnateToolSystem.cs index 6afe8863033..7b7cc2a8aa7 100644 --- a/Content.Server/Tools/Innate/InnateToolSystem.cs +++ b/Content.Server/Tools/Innate/InnateToolSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Shared.Body.Part; using Content.Shared.Destructible; using Content.Shared.Hands; using Content.Shared.Hands.Components; diff --git a/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs b/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs new file mode 100644 index 00000000000..8092381a172 --- /dev/null +++ b/Content.Server/Toolshed/Commands/Misc/CloneCommand.cs @@ -0,0 +1,83 @@ +using Content.Server.Administration; +using Content.Server.Humanoid; +using Content.Shared.Administration; +using Content.Shared.Body; +using Content.Shared.Cloning; +using Content.Shared.Inventory; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.Cloning.Commands; + +[ToolshedCommand, AdminCommand(AdminFlags.Fun)] +public sealed class CloneCommand : ToolshedCommand +{ + private SharedVisualBodySystem? _visualBody; + private CloningSystem? _cloning; + private MetaDataSystem? _metadata; + + [CommandImplementation("humanoidappearance")] + public IEnumerable HumanoidAppearance([PipedArgument] IEnumerable targets, EntityUid source, bool rename) + { + _visualBody ??= GetSys(); + _metadata ??= GetSys(); + + foreach (var ent in targets) + { + _visualBody.CopyAppearanceFrom(source, ent); + + if (rename) + _metadata.SetEntityName(ent, MetaData(source).EntityName, raiseEvents: true); + + yield return ent; + } + } + + [CommandImplementation("comps")] + public IEnumerable Comps([PipedArgument] IEnumerable targets, EntityUid source, ProtoId settings) + { + _cloning ??= GetSys(); + + foreach (var ent in targets) + { + _cloning.CloneComponents(source, ent, settings); + yield return ent; + } + } + + [CommandImplementation("equipment")] + public IEnumerable Equipment([PipedArgument] IEnumerable targets, EntityUid source, SlotFlags flags) + { + _cloning ??= GetSys(); + + foreach (var ent in targets) + { + _cloning.CopyEquipment(source, ent, flags); + yield return ent; + } + } + + [CommandImplementation("implants")] + public IEnumerable Implants([PipedArgument] IEnumerable targets, EntityUid source, bool copyStorage) + { + _cloning ??= GetSys(); + + foreach (var ent in targets) + { + _cloning.CopyImplants(source, ent, copyStorage); + yield return ent; + } + } + + [CommandImplementation("storage")] + public IEnumerable InternalStorage([PipedArgument] IEnumerable targets, EntityUid source) + { + _cloning ??= GetSys(); + + foreach (var ent in targets) + { + _cloning.CopyStorage(source, ent); + yield return ent; + } + } +} diff --git a/Content.Server/Toolshed/Commands/Misc/MsgCommand.cs b/Content.Server/Toolshed/Commands/Misc/MsgCommand.cs new file mode 100644 index 00000000000..4708ae82d5b --- /dev/null +++ b/Content.Server/Toolshed/Commands/Misc/MsgCommand.cs @@ -0,0 +1,106 @@ +using Content.Server.Administration; +using Content.Server.Chat.Managers; +using Content.Server.Popups; +using Content.Server.Prayer; +using Content.Server.Tips; +using Content.Shared.Administration; +using Content.Shared.Chat; +using Content.Shared.Popups; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.Toolshed.Commands.Misc; + +[ToolshedCommand, AdminCommand(AdminFlags.Fun)] +public sealed class MsgCommand : ToolshedCommand +{ + [Dependency] private IChatManager _chatManager = default!; + + private PrayerSystem? _prayer; + private PopupSystem? _popup; + private TipsSystem? _tips; + + [CommandImplementation("subtle")] + public IEnumerable Subtle(IInvocationContext ctx, [PipedArgument] IEnumerable targets, string popup, string message) + { + _prayer ??= GetSys(); + + foreach (var ent in targets) + { + if (!TryComp(ent, out var actor)) + continue; + + _prayer.SendSubtleMessage(actor.PlayerSession, ctx.Session, message, popup); + + yield return ent; + } + } + + [CommandImplementation("chat")] + public IEnumerable Chat([PipedArgument] IEnumerable targets, string message) + { + foreach (var ent in targets) + { + if (!TryComp(ent, out var actor)) + continue; + + _chatManager.ChatMessageToOne(ChatChannel.Local, message, message, EntityUid.Invalid, false, actor.PlayerSession.Channel); + yield return ent; + } + } + + [CommandImplementation("chat")] + public IEnumerable Chat([PipedArgument] IEnumerable targets, string message) + { + foreach (var session in targets) + { + _chatManager.ChatMessageToOne(ChatChannel.Local, message, message, EntityUid.Invalid, false, session.Channel); + yield return session; + } + } + + [CommandImplementation("popup")] + public IEnumerable Popup([PipedArgument] IEnumerable targets, string popup, PopupType type, bool recipientOnly) + { + _popup ??= GetSys(); + + foreach (var ent in targets) + { + if (recipientOnly) + _popup.PopupEntity(popup, ent, ent, type); // If recipientOnly, show the popup only to the recipient. + else + _popup.PopupEntity(popup, ent, type); // Otherwise, show it to everyone within PVS of the target entity. + yield return ent; + } + } + + [CommandImplementation("tippy")] + public IEnumerable Tippy([PipedArgument] IEnumerable targets, string message, EntProtoId prototype, float speakTime, float slideTime, float waddleInterval) + { + _tips ??= GetSys(); + + foreach (var ent in targets) + { + if (!TryComp(ent, out var actor)) + continue; + + _tips.SendTippy(actor.PlayerSession, message, prototype, speakTime, slideTime, waddleInterval); + + yield return ent; + } + } + + [CommandImplementation("tippy")] + public IEnumerable Tippy([PipedArgument] IEnumerable targets, string message, EntProtoId prototype, float speakTime, float slideTime, float waddleInterval) + { + _tips ??= GetSys(); + + foreach (var session in targets) + { + _tips.SendTippy(session, message, prototype, speakTime, slideTime, waddleInterval); + + yield return session; + } + } +} diff --git a/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs b/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs new file mode 100644 index 00000000000..4182bac0817 --- /dev/null +++ b/Content.Server/Toolshed/Commands/StatusEffects/StatusEffectCommand.cs @@ -0,0 +1,141 @@ +using Content.Server.Administration; +using Content.Server.Toolshed.TypeParsers.StatusEffects; +using Content.Shared.Administration; +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; + +namespace Content.Server.Toolshed.Commands.StatusEffects; + +[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)] +public sealed class StatusEffectCommand : ToolshedCommand +{ + private StatusEffectsSystem? _statusEffectsSystem; + + [CommandImplementation("add")] + public EntityUid? Add([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + _statusEffectsSystem.TryAddStatusEffectDuration( + input, + status, + TimeSpan.FromSeconds(time), + ZeroAsNull(delay)); + + return input; + } + + [CommandImplementation("add")] + public IEnumerable Add([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + foreach (var ent in input) + { + _statusEffectsSystem.TryAddStatusEffectDuration( + ent, + status, + TimeSpan.FromSeconds(time), + ZeroAsNull(delay)); + + yield return ent; + } + } + + [CommandImplementation("update")] + public EntityUid? Update([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + _statusEffectsSystem.TryUpdateStatusEffectDuration( + input, + status, + ZeroAsNull(time), + ZeroAsNull(delay)); + + return input; + } + + [CommandImplementation("update")] + public IEnumerable Update([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + foreach (var ent in input) + { + _statusEffectsSystem.TryUpdateStatusEffectDuration( + ent, + status, + ZeroAsNull(time), + ZeroAsNull(delay)); + + yield return ent; + } + } + + [CommandImplementation("set")] + public EntityUid? Set([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + _statusEffectsSystem.TrySetStatusEffectDuration( + input, + status, + ZeroAsNull(time), + ZeroAsNull(delay)); + + return input; + } + + [CommandImplementation("set")] + public IEnumerable Set([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0, float delay = 0) + { + _statusEffectsSystem ??= GetSys(); + + foreach (var ent in input) + { + _statusEffectsSystem.TrySetStatusEffectDuration( + ent, + status, + ZeroAsNull(time), + ZeroAsNull(delay)); + + yield return ent; + } + } + + [CommandImplementation("remove")] + public EntityUid? Remove([PipedArgument] EntityUid input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0) + { + _statusEffectsSystem ??= GetSys(); + + _statusEffectsSystem.TryRemoveTime( + input, + status, + ZeroAsNull(time)); + + return input; + } + + [CommandImplementation("remove")] + public IEnumerable Remove([PipedArgument] IEnumerable input, [CommandArgument(typeof(StatusEffectCompletionParser))] EntProtoId status, float time = 0) + { + _statusEffectsSystem ??= GetSys(); + + foreach (var ent in input) + { + _statusEffectsSystem.TryRemoveTime( + ent, + status, + ZeroAsNull(time)); + + yield return ent; + } + } + + private static TimeSpan? ZeroAsNull(float delay) + { + return delay == 0 ? null : TimeSpan.FromSeconds(delay); + } +} diff --git a/Content.Server/Toolshed/TypeParsers/StatusEffects/StatusEffectCompletionParser.cs b/Content.Server/Toolshed/TypeParsers/StatusEffects/StatusEffectCompletionParser.cs new file mode 100644 index 00000000000..839f7fa9ca6 --- /dev/null +++ b/Content.Server/Toolshed/TypeParsers/StatusEffects/StatusEffectCompletionParser.cs @@ -0,0 +1,16 @@ +using Content.Shared.StatusEffectNew; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Content.Server.Toolshed.TypeParsers.StatusEffects; + +public sealed class StatusEffectCompletionParser : CustomCompletionParser +{ + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) + { + return CompletionResult.FromHintOptions(IoCManager.Resolve().System().StatusEffectPrototypes, GetArgHint(arg)); + } +} diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs index 8350c01f7aa..bf00f5ebd84 100644 --- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs +++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs @@ -73,7 +73,13 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) } // Finally add uplink - if (!_uplinkSystem.AddUplink(user, 20, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted)) + var result = _uplinkSystem.AddUplink(user, 20, out var code, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted); + + if (code != null && result == AddUplinkResult.Pda) + shell.WriteLine(Loc.GetString("add-uplink-command-success-pda", ("code", string.Join("-", code).Replace("sharp", "#")))); + else if (result == AddUplinkResult.Implant) + shell.WriteLine(Loc.GetString("add-uplink-command-success-implant")); + else if (result == AddUplinkResult.Failure) shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); } diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index e4aa7b5ec17..1daee849ddc 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.PDA.Ringer; using Content.Server.Store.Systems; using Content.Server.StoreDiscount.Systems; using Content.Shared.FixedPoint; @@ -9,6 +10,7 @@ using Content.Shared.PDA; using Content.Shared.Store; using Content.Shared.Store.Components; +using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Uplink; @@ -21,38 +23,110 @@ public sealed class UplinkSystem : EntitySystem [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!; [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly RingerSystem _ringer = default!; + public static readonly EntProtoId TraitorUplinkStore = "StorePresetRemoteUplink"; public static readonly ProtoId TelecrystalCurrencyPrototype = "Telecrystal"; private static readonly EntProtoId FallbackUplinkImplant = "UplinkImplant"; private static readonly ProtoId FallbackUplinkCatalog = "UplinkUplinkImplanter"; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRemoteStoreImplanted); + } + + private void OnRemoteStoreImplanted(Entity entity, ref ImplantImplantedEvent args) + { + if (_mind.GetMind(args.Implanted) is not { } mind ) + return; + + var storeEnumerator = EntityQueryEnumerator(); + while (storeEnumerator.MoveNext(out var uid, out _, out var store)) + { + if (store.AccountOwner != mind) + continue; + + entity.Comp.Store = uid; + return; + } + + // If we didn't have an uplink, make an empty one. + entity.Comp.Store = Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); + SetUplink(args.Implanted, entity.Comp.Store.Value, 0, false); + Log.Error($"{ToPrettyString(args.Implanted)} did not have an uplink when they were implanted."); + } + /// /// Adds an uplink to the target /// /// The person who is getting the uplink /// The amount of currency on the uplink. If null, will just use the amount specified in the preset. + /// The code which was generated, if any. /// The entity that will actually have the uplink functionality. Defaults to the PDA if null. /// Marker that enables discounts for uplink items. - /// Whether or not the uplink was added successfully - public bool AddUplink( + /// Binds the uplink to the specific uplink entity. + /// Whether the uplink was added successfully to a PDA, implant or not at all. + public AddUplinkResult AddUplink( EntityUid user, FixedPoint2 balance, + out Note[]? code, EntityUid? uplinkEntity = null, - bool giveDiscounts = false) + bool giveDiscounts = false, + bool bindToPda = false) { - // Try to find target item if none passed + code = null; + + var storeEntity = Spawn(TraitorUplinkStore, MapCoordinates.Nullspace); + if (TryAddEntityUplink(user, balance, out var generatedCode, uplinkEntity, storeEntity, giveDiscounts, bindToPda)) + { + code = generatedCode; + return AddUplinkResult.Pda; + } + + if (TryImplantUplink(user, storeEntity, balance, giveDiscounts)) + { + return AddUplinkResult.Implant; + } + + Del(storeEntity); + return AddUplinkResult.Failure; + } + public bool TryAddEntityUplink( + EntityUid user, + FixedPoint2 balance, + out Note[]? code, + EntityUid? uplinkEntity, + EntityUid storeEntity, + bool giveDiscounts = false, + bool bindToPda = false) + { + code = null; uplinkEntity ??= FindUplinkTarget(user); if (uplinkEntity == null) - return ImplantUplink(user, balance, giveDiscounts); + return false; - EnsureComp(uplinkEntity.Value); + var ev = new GenerateUplinkCodeEvent(); + RaiseLocalEvent(storeEntity, ref ev); - SetUplink(user, uplinkEntity.Value, balance, giveDiscounts); + if (ev.Code == null) + { + QueueDel(storeEntity); + return false; + } - // TODO add BUI. Currently can't be done outside of yaml -_- - // ^ What does this even mean? + code = ev.Code; + + if (bindToPda) + { + var accessComp = EnsureComp(storeEntity); + _ringer.SetBoundUplinkEntity((storeEntity, accessComp), uplinkEntity.Value); + } + + SetUplink(user, storeEntity, balance, giveDiscounts); return true; } @@ -60,25 +134,25 @@ public bool AddUplink( /// /// Configure TC for the uplink /// - private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) + private void SetUplink(EntityUid user, EntityUid store, FixedPoint2 balance, bool giveDiscounts) { if (!_mind.TryGetMind(user, out var mind, out _)) return; - var store = EnsureComp(uplink); + var storeComp = EnsureComp(store); - store.AccountOwner = mind; + storeComp.AccountOwner = mind; - store.Balance.Clear(); + storeComp.Balance.Clear(); _store.TryAddCurrency(new Dictionary { { TelecrystalCurrencyPrototype, balance } }, - uplink, - store); + store, + storeComp); var uplinkInitializedEvent = new StoreInitializedEvent( TargetUser: mind, - Store: uplink, + Store: store, UseDiscounts: giveDiscounts, - Listings: _store.GetAvailableListings(mind, uplink, store) + Listings: _store.GetAvailableListings(mind, store, storeComp) .ToArray()); RaiseLocalEvent(ref uplinkInitializedEvent); } @@ -86,9 +160,9 @@ private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bo /// /// Implant an uplink as a fallback measure if the traitor had no PDA /// - private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) + public bool TryImplantUplink(EntityUid user, EntityUid storeEntity, FixedPoint2 balance, bool giveDiscounts) { - if (!_proto.Resolve(FallbackUplinkCatalog, out var catalog)) + if (!_proto.Resolve(FallbackUplinkCatalog, out var catalog)) return false; if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost)) @@ -99,12 +173,15 @@ private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscoun else balance = balance - cost; + SetUplink(user, storeEntity, balance, giveDiscounts); var implant = _subdermalImplant.AddImplant(user, FallbackUplinkImplant); - if (!HasComp(implant)) + if (!HasComp(implant)) + { + Log.Error($"Implant does not have the store component {implant}"); return false; + } - SetUplink(user, implant.Value, balance, giveDiscounts); return true; } @@ -117,23 +194,29 @@ private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscoun // Try to find PDA in inventory if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) { - while (containerSlotEnumerator.MoveNext(out var pdaUid)) + while (containerSlotEnumerator.MoveNext(out var containerSlot)) { - if (!pdaUid.ContainedEntity.HasValue) - continue; + var pdaUid = containerSlot.ContainedEntity; - if (HasComp(pdaUid.ContainedEntity.Value) || HasComp(pdaUid.ContainedEntity.Value)) - return pdaUid.ContainedEntity.Value; + if (HasComp(pdaUid) && HasComp(pdaUid)) + return pdaUid.Value; } } // Also check hands foreach (var item in _handsSystem.EnumerateHeld(user)) { - if (HasComp(item) || HasComp(item)) + if (HasComp(item) && HasComp(item)) return item; } return null; } } + +public enum AddUplinkResult +{ + Pda, + Implant, + Failure, +} diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs index d6035f887b1..0956bcb4019 100644 --- a/Content.Server/Traits/TraitSystem.cs +++ b/Content.Server/Traits/TraitSystem.cs @@ -41,11 +41,18 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) } if (_whitelistSystem.IsWhitelistFail(traitPrototype.Whitelist, args.Mob) || - _whitelistSystem.IsBlacklistPass(traitPrototype.Blacklist, args.Mob)) + _whitelistSystem.IsWhitelistPass(traitPrototype.Blacklist, args.Mob)) continue; // Add all components required by the prototype - EntityManager.AddComponents(args.Mob, traitPrototype.Components, false); + if (traitPrototype.Components.Count > 0) + EntityManager.AddComponents(args.Mob, traitPrototype.Components, false); + + // Add all JobSpecials required by the prototype + foreach (var special in traitPrototype.Specials) + { + special.AfterEquip(args.Mob); + } // Add item required by the trait if (traitPrototype.TraitGear == null) diff --git a/Content.Server/Trigger/Systems/FireStackOnTriggerSystem.cs b/Content.Server/Trigger/Systems/FireStackOnTriggerSystem.cs index af3298b865e..b1d89bda051 100644 --- a/Content.Server/Trigger/Systems/FireStackOnTriggerSystem.cs +++ b/Content.Server/Trigger/Systems/FireStackOnTriggerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; using Content.Shared.Trigger; using Content.Shared.Trigger.Components.Effects; @@ -31,7 +32,10 @@ private void OnTriggerFlame(Entity ent, ref Trigger if (target == null) return; - _flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite); + if (!TryComp(target.Value, out var flammable)) + return; + + _flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite, flammable: flammable); args.Handled = true; } @@ -46,7 +50,10 @@ private void OnTriggerExtinguish(Entity ent, ref T if (target == null) return; - _flame.Extinguish(target.Value); + if (!TryComp(target.Value, out var flammable)) + return; + + _flame.Extinguish(target.Value, flammable: flammable); args.Handled = true; } diff --git a/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs b/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs index 1e08587ce79..48ead111618 100644 --- a/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs +++ b/Content.Server/Trigger/Systems/SpeakOnTriggerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Trigger; using Content.Shared.Trigger.Components.Effects; using Robust.Shared.Prototypes; diff --git a/Content.Server/TurretController/DeployableTurretControllerSystem.cs b/Content.Server/TurretController/DeployableTurretControllerSystem.cs index f0b68814311..fbb802b6796 100644 --- a/Content.Server/TurretController/DeployableTurretControllerSystem.cs +++ b/Content.Server/TurretController/DeployableTurretControllerSystem.cs @@ -9,6 +9,8 @@ using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using System.Linq; +using Content.Server.Administration.Logs; +using Content.Shared.Database; namespace Content.Server.TurretController; @@ -17,6 +19,7 @@ public sealed partial class DeployableTurretControllerSystem : SharedDeployableT { [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; /// Keys for the device network. See for further examples. public const string CmdSetArmamemtState = "set_armament_state"; @@ -109,6 +112,8 @@ protected override void ChangeArmamentSetting(Entity ent, ref DeviceN args.Data.TryGetValue(command, out int? armamentState)) { if (TryComp(ent, out var batteryWeaponFireModes)) - _fireModes.TrySetFireMode(ent, batteryWeaponFireModes, armamentState.Value); + _fireModes.TrySetFireMode((ent, batteryWeaponFireModes), armamentState.Value); TrySetState(ent, armamentState.Value >= 0); return; diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs index 2cf5539a269..1c283783937 100644 --- a/Content.Server/VendingMachines/VendingMachineSystem.cs +++ b/Content.Server/VendingMachines/VendingMachineSystem.cs @@ -1,23 +1,18 @@ using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; -using Content.Server.Emp; using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Server.Vocalization.Systems; using Content.Shared.Cargo; using Content.Shared.Damage; -using Content.Shared.Destructible; +using Content.Shared.Damage.Systems; using Content.Shared.Emp; using Content.Shared.Power; using Content.Shared.Throwing; -using Content.Shared.UserInterface; using Content.Shared.VendingMachines; using Content.Shared.Wall; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; -using Robust.Shared.Map; namespace Content.Server.VendingMachines { @@ -34,13 +29,10 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnBreak); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnVendingPrice); - SubscribeLocalEvent(OnEmpPulse); SubscribeLocalEvent(OnTryVocalize); - SubscribeLocalEvent(OnActivatableUIOpenAttempt); SubscribeLocalEvent(OnSelfDispense); SubscribeLocalEvent(OnPriceCalculation); @@ -74,28 +66,17 @@ protected override void OnMapInit(EntityUid uid, VendingMachineComponent compone } } - private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args) - { - if (component.Broken) - args.Cancel(); - } - private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args) { TryUpdateVisualState((uid, component)); } - private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs) - { - vendComponent.Broken = true; - TryUpdateVisualState((uid, vendComponent)); - } - private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args) { if (!args.DamageIncreased && component.Broken) { component.Broken = false; + Dirty(uid, component); TryUpdateVisualState((uid, component)); return; } @@ -165,6 +146,7 @@ public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, return; var item = _random.Pick(availableItems); + if (forceEject) { vendComponent.NextItemToEject = item.ID; @@ -210,23 +192,9 @@ protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendCo if (vendComponent.ThrowNextItem) { - //Imperial Space Vending Machine; Start - if (vendComponent.TargetDirection == null) - { - //Imperial Space Vending Machine; End - var range = vendComponent.NonLimitedEjectRange; - var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range)); - _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce); - //Imperial Space Vending Machine; Start - } - else - { - - var direction = vendComponent.TargetDirection ?? new EntityCoordinates(); - vendComponent.TargetDirection = null; - _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce); - } - //Imperial Space Vending Machine; End + var range = vendComponent.NonLimitedEjectRange; + var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range)); + _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce); } vendComponent.NextItemToEject = null; @@ -272,16 +240,6 @@ private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent co args.Price += priceSets.Max(); } - private void OnEmpPulse(EntityUid uid, VendingMachineComponent component, ref EmpPulseEvent args) - { - if (!component.Broken && this.IsPowered(uid, EntityManager)) - { - args.Affected = true; - args.Disabled = true; - component.NextEmpEject = Timing.CurTime; - } - } - private void OnTryVocalize(Entity ent, ref TryVocalizeEvent args) { args.Cancelled |= ent.Comp.Broken; diff --git a/Content.Server/VentHorde/Components/VentHordeSpawnerComponent.cs b/Content.Server/VentHorde/Components/VentHordeSpawnerComponent.cs new file mode 100644 index 00000000000..ab1d68031ca --- /dev/null +++ b/Content.Server/VentHorde/Components/VentHordeSpawnerComponent.cs @@ -0,0 +1,77 @@ +using Content.Server.VentHorde.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.VentHorde.Components; + +/// +/// Marks an entity as selected by the . +/// Will spawn all entities contained within on its location at . +/// +[RegisterComponent, Access(typeof(VentHordeSystem))] +[AutoGenerateComponentPause] +public sealed partial class VentHordeSpawnerComponent : Component +{ + /// + /// The mobs to spawn from the vent. + /// + [DataField(required: true)] + public List Entities = new (); + + /// + /// Maximum speed at which the entities will be thrown out of the vent. + /// + [DataField] + public float MaxThrowSpeed = 1.5f; + + /// + /// Minimum speed at which the entities will be thrown out of the vent. + /// + [DataField] + public float MinThrowSpeed = 0.5f; + + /// + /// Maximum distance which travel when thrown out of the vent. + /// + [DataField] + public float MaxThrowDistance = 4f; + + /// + /// Minimum distance which travel when thrown out of the vent. + /// + [DataField] + public float MinThrowDistance = 2f; + + /// + /// The time at which the entities will spawn. + /// + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? SpawnTime; + + /// + /// Plays on loop when a vent is selected as a spawner. + /// + [DataField] + public SoundSpecifier PassiveSound = new SoundPathSpecifier("/Audio/Machines/airlock_creaking.ogg") + { + Params = AudioParams.Default.WithVolume(-3f), + }; + + /// + /// Plays when the entities are thrown out of the vent. + /// + [DataField] + public SoundSpecifier EndSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/grenade_launcher.ogg") + { + Params = AudioParams.Default.WithVolume(-3f), + }; + + /// + /// The PassiveSound entity, used to cancel the audio. + /// + [DataField] + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? AudioStream; +} diff --git a/Content.Server/VentHorde/Systems/VentHordeSystem.cs b/Content.Server/VentHorde/Systems/VentHordeSystem.cs new file mode 100644 index 00000000000..a2834e01459 --- /dev/null +++ b/Content.Server/VentHorde/Systems/VentHordeSystem.cs @@ -0,0 +1,119 @@ +using Content.Server.VentHorde.Components; +using Content.Shared.Destructible; +using Content.Shared.Jittering; +using Content.Shared.Throwing; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.VentHorde.Systems; + +public sealed class VentHordeSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedJitteringSystem _jitter = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSpawnerInit); + SubscribeLocalEvent(OnSpawnerShutdown); + + SubscribeLocalEvent(OnSpawnerBreakage); + SubscribeLocalEvent(OnSpawnerAnchored); + } + + private void OnSpawnerInit(Entity entity, ref MapInitEvent args) + { + _jitter.AddJitter(entity); + } + + private void OnSpawnerShutdown(Entity entity, ref ComponentShutdown args) + { + _audio.Stop(entity.Comp.AudioStream); + RemCompDeferred(entity); + } + + private void OnSpawnerBreakage(Entity entity, ref BreakageEventArgs args) + { + // There is no escape. + EndHordeSpawn(entity); + } + + private void OnSpawnerAnchored(Entity entity, ref AnchorStateChangedEvent args) + { + // Anchor state changes when the entity is broken, to avoid double spawning we check if the entity is gonna be deleted. + if (TerminatingOrDeleted(entity)) + return; + + // There is no escape. + EndHordeSpawn(entity); + } + + /// + /// Starts a horde spawn at an entity. + /// + /// The entity to spawn the horde at. + /// List of entities to spawn. + /// Time after which to spawn the entities. + /// If an already active spawner is selected, will add entities to its list. Otherwise, will fail. + public void StartHordeSpawn(EntityUid uid, List spawns, TimeSpan spawnDelay, bool append = true) + { + if (TryComp(uid, out var hordeSpawner)) + { + if (append) + { + hordeSpawner.Entities.AddRange(spawns); + } + + return; + } + + hordeSpawner = EnsureComp(uid); + + hordeSpawner.AudioStream = _audio.PlayPvs(hordeSpawner.PassiveSound, uid, hordeSpawner.PassiveSound.Params.WithLoop(true))?.Entity; + + hordeSpawner.Entities = spawns; + hordeSpawner.SpawnTime = _timing.CurTime + spawnDelay; + } + + /// + /// Ends a horde spawn, causing all entities to spawn at once. + /// + /// The horde spawner entity. + public void EndHordeSpawn(Entity entity) + { + entity.Comp.AudioStream = _audio.Stop(entity.Comp.AudioStream); + + _audio.PlayPvs(entity.Comp.EndSound, entity); + + foreach (var spawn in entity.Comp.Entities) + { + var spawned = Spawn(spawn, Transform(entity).Coordinates); + var direction = _random.NextVector2() * _random.NextFloat(entity.Comp.MinThrowDistance, entity.Comp.MaxThrowDistance); + var throwSpeed = _random.NextFloat(entity.Comp.MinThrowSpeed, entity.Comp.MaxThrowSpeed); + _throwing.TryThrow(spawned, direction, throwSpeed); + } + + RemCompDeferred(entity); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.SpawnTime != null && _timing.CurTime > comp.SpawnTime) + { + EndHordeSpawn((uid, comp)); + } + } + } +} diff --git a/Content.Server/VoiceMask/VoiceMaskComponent.cs b/Content.Server/VoiceMask/VoiceMaskComponent.cs deleted file mode 100644 index d3116f94db1..00000000000 --- a/Content.Server/VoiceMask/VoiceMaskComponent.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Content.Shared.Speech; -using Robust.Shared.Prototypes; - -namespace Content.Server.VoiceMask; - -/// -/// This component is for voice mask items! Adding this component to clothing will give the the voice mask UI -/// and allow the wearer to change their voice and verb at will. -/// -/// -/// DO NOT use this if you do not want the interface. -/// The VoiceOverrideSystem is probably what your looking for (Or you might have to make something similar)! -/// -[RegisterComponent] -public sealed partial class VoiceMaskComponent : Component -{ - /// - /// The name that will override an entities default name. If null, it will use the default override. - /// - [DataField] - public string? VoiceMaskName = null; - - /// - /// The speech verb that will override an entities default one. If null, it will use the entities default verb. - /// - [DataField] - public ProtoId? VoiceMaskSpeechVerb; - - /// - /// The action that gets displayed when the voice mask is equipped. - /// - [DataField] - public EntProtoId Action = "ActionChangeVoiceMask"; - - /// - /// Reference to the action. - /// - [DataField] - public EntityUid? ActionEntity; -} diff --git a/Content.Server/VoiceMask/VoiceMaskSystem.cs b/Content.Server/VoiceMask/VoiceMaskSystem.cs index 528acd58b0a..a67bfb8b669 100644 --- a/Content.Server/VoiceMask/VoiceMaskSystem.cs +++ b/Content.Server/VoiceMask/VoiceMaskSystem.cs @@ -1,13 +1,16 @@ +using Content.Server.Speech; using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Clothing; using Content.Shared.Database; +using Content.Shared.IdentityManagement; +using Content.Shared.IdentityManagement.Components; +using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.Lock; using Content.Shared.Popups; -using Content.Shared.Preferences; using Content.Shared.Speech; using Content.Shared.VoiceMask; using Robust.Shared.Configuration; @@ -26,6 +29,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly LockSystem _lock = default!; [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly IdentitySystem _identity = default!; // CCVar. private int _maxNameLength; @@ -33,20 +37,75 @@ public sealed partial class VoiceMaskSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent>(OnTransformSpeakerName); + SubscribeLocalEvent>(OnTransformSpeakerNameInventory); + SubscribeLocalEvent>(OnTransformSpeakerNameImplant); + SubscribeLocalEvent>(OnSeeIdentityAttemptEvent); + SubscribeLocalEvent(OnImplantImplantedEvent); + SubscribeLocalEvent(OnImplantRemovedEventEvent); SubscribeLocalEvent(OnLockToggled); SubscribeLocalEvent(OnChangeName); SubscribeLocalEvent(OnChangeVerb); + SubscribeLocalEvent(OnToggle); + SubscribeLocalEvent(OnAccentToggle); SubscribeLocalEvent(OnEquip); SubscribeLocalEvent(OpenUI); + SubscribeLocalEvent(OnTransformSpeech, before: [typeof(AccentSystem)]); + SubscribeLocalEvent>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]); + SubscribeLocalEvent>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]); Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); } - private void OnTransformSpeakerName(Entity entity, ref InventoryRelayedEvent args) + /// + /// Hides accent if the voice mask is on and the option to block accents is on + /// + private void TransformSpeech(Entity entity, TransformSpeechEvent args) { - args.Args.VoiceName = GetCurrentVoiceName(entity); - args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb; + if (entity.Comp.AccentHide && entity.Comp.Active) + args.Cancel(); + } + + private void OnTransformSpeech(Entity entity, ref TransformSpeechEvent args) + { + TransformSpeech(entity, args); + } + + private void OnTransformSpeechInventory(Entity entity, ref InventoryRelayedEvent args) + { + TransformSpeech(entity, args.Args); + } + + private void OnTransformSpeechImplant(Entity entity, ref ImplantRelayEvent args) + { + TransformSpeech(entity, args.Event); + } + + private void OnTransformSpeakerNameInventory(Entity entity, ref InventoryRelayedEvent args) + { + TransformVoice(entity, args.Args); + } + + private void OnTransformSpeakerNameImplant(Entity entity, ref ImplantRelayEvent args) + { + TransformVoice(entity, args.Event); + } + + private void OnSeeIdentityAttemptEvent(Entity entity, ref ImplantRelayEvent args) + { + if (!entity.Comp.OverrideIdentity || !entity.Comp.Active) + return; + + args.Event.NameOverride = GetCurrentVoiceName(entity); + } + + private void OnImplantImplantedEvent(Entity entity, ref ImplantImplantedEvent ev) + { + _identity.QueueIdentityUpdate(ev.Implanted); + } + + private void OnImplantRemovedEventEvent(Entity entity, ref ImplantRemovedEvent ev) + { + _identity.QueueIdentityUpdate(ev.Implanted); } private void OnLockToggled(Entity ent, ref LockToggledEvent args) @@ -79,6 +138,9 @@ private void OnChangeName(Entity entity, ref VoiceMaskChange return; } + var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name); + RaiseLocalEvent(message.Actor, ref nameUpdatedEvent); + entity.Comp.VoiceMaskName = message.Name; _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}"); @@ -86,6 +148,21 @@ private void OnChangeName(Entity entity, ref VoiceMaskChange UpdateUI(entity); } + + private void OnToggle(Entity entity, ref VoiceMaskToggleMessage args) + { + _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-toggle"), entity, args.Actor); + entity.Comp.Active = !entity.Comp.Active; + + // Update identity because of possible name override + _identity.QueueIdentityUpdate(args.Actor); + } + + private void OnAccentToggle(Entity entity, ref VoiceMaskAccentToggleMessage args) + { + _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-accent-toggle"), entity, args.Actor); + entity.Comp.AccentHide = !entity.Comp.AccentHide; + } #endregion #region UI @@ -114,7 +191,7 @@ private void OpenUI(VoiceMaskSetNameEvent ev) private void UpdateUI(Entity entity) { if (_uiSystem.HasUi(entity, VoiceMaskUIKey.Key)) - _uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb)); + _uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide)); } #endregion @@ -123,5 +200,14 @@ private string GetCurrentVoiceName(Entity entity) { return entity.Comp.VoiceMaskName ?? Loc.GetString("voice-mask-default-name-override"); } + + private void TransformVoice(Entity entity, TransformSpeakerNameEvent args) + { + if (!entity.Comp.Active) + return; + + args.VoiceName = GetCurrentVoiceName(entity); + args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.SpeechVerb; + } #endregion } diff --git a/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs b/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs index ade861af981..7b58278d08f 100644 --- a/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs +++ b/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs @@ -30,10 +30,9 @@ public override void Initialize() private void VoiceTriggered(Entity ent, ref VoiceTriggeredEvent args) { - // Check if the component has any slot restrictions via AllowedSlots // If it has slot restrictions, check if the item is in a slot that is allowed - if (ent.Comp.AllowedSlots != null && _inventory.TryGetContainingSlot(ent.Owner, out var itemSlot) && - (itemSlot.SlotFlags & ent.Comp.AllowedSlots) == 0) + if (ent.Comp.AllowedSlots is { } allowedSlots + && !_inventory.InSlotWithAnyFlags(ent.Owner, allowedSlots)) return; // Get the storage component diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index fe05ba3117f..4283ec8d895 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -6,12 +6,12 @@ using Content.Server.Discord.WebhookMessages; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; -using Content.Server.Maps; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.Maps; using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Voting; @@ -308,10 +308,15 @@ private void CreateMapVote(ICommonSession? initiator) var ticker = _entityManager.EntitySysManager.GetEntitySystem(); if (ticker.CanUpdateMap()) { - if (_gameMapManager.TrySelectMapIfEligible(picked.ID)) + if (_gameMapManager.CheckMapExists(picked.ID)) { + _gameMapManager.SelectMap(picked.ID); ticker.UpdateInfoText(); } + else + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-invalid", ("winner", maps[picked]))); + } } else { @@ -373,14 +378,7 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) } var targetUid = located.UserId; var targetHWid = located.LastHWId; - (IPAddress, int)? targetIP = null; - - if (located.LastAddress is not null) - { - targetIP = located.LastAddress.AddressFamily is AddressFamily.InterNetwork - ? (located.LastAddress, 32) // People with ipv4 addresses get a /32 address so we ban that - : (located.LastAddress, 64); // This can only be an ipv6 address. People with ipv6 address should get /64 addresses so we ban that. - } + var targetIP = located.LastAddress; if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession)) { @@ -458,7 +456,7 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) (Loc.GetString("ui-vote-votekick-abstain"), "abstain") }, Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)), - InitiatorTimeout = TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.VotekickTimeout)), + InitiatorTimeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout)), VoterEligibility = voterEligibility, DisplayVotes = false, TargetEntity = targetNetEntity @@ -473,7 +471,7 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVotekickWebhook), Loc.GetString("votekick-webhook-name"), options.Title + "\n" + Loc.GetString("votekick-webhook-description", ("initiator", initiatorName), ("target", targetSession))); // Time out the vote now that we know it will happen - TimeoutStandardVote(StandardVoteType.Votekick); + TimeoutStandardVote(StandardVoteType.Votekick, TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimeout))); vote.OnFinished += (_, eventArgs) => { @@ -546,7 +544,15 @@ private async void CreateVotekickVote(ICommonSession? initiator, string[]? args) uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration); - _bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, Loc.GetString("votekick-ban-reason", ("reason", reason))); + var banInfo = new CreateServerBanInfo(Loc.GetString("votekick-ban-reason", ("reason", reason))); + banInfo.AddUser(targetUid, target); + banInfo.AddHWId(targetHWid); + banInfo.AddAddress(targetIP); + banInfo.WithSeverity(severity); + if (minutes > 0) + banInfo.WithMinutes(minutes); + + _bans.CreateServerBan(banInfo); } } else @@ -580,9 +586,9 @@ private void AnnounceCancelledVotekickForVoters(string target) } } - private void TimeoutStandardVote(StandardVoteType type) + private void TimeoutStandardVote(StandardVoteType type, TimeSpan? timeoutOverride = null) { - var timeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout)); + var timeout = timeoutOverride ?? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout)); _standardVoteTimeout[type] = _timing.RealTime + timeout; DirtyCanCallVoteAll(); } diff --git a/Content.Server/Wagging/WaggingSystem.cs b/Content.Server/Wagging/WaggingSystem.cs index 88b82a9ca5d..d369ea86899 100644 --- a/Content.Server/Wagging/WaggingSystem.cs +++ b/Content.Server/Wagging/WaggingSystem.cs @@ -1,12 +1,12 @@ using Content.Server.Actions; -using Content.Server.Humanoid; +using Content.Shared.Body; using Content.Shared.Cloning.Events; -using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Mobs; using Content.Shared.Toggleable; using Content.Shared.Wagging; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Server.Wagging; @@ -16,7 +16,7 @@ namespace Content.Server.Wagging; public sealed class WaggingSystem : EntitySystem { [Dependency] private readonly ActionsSystem _actions = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; public override void Initialize() @@ -35,78 +35,100 @@ private void OnCloning(Entity ent, ref CloningEvent args) if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; - EnsureComp(args.CloneUid); + // Make sure to set the datafields before adding the component so that the correct action gets spawned on map init. + var cloneComp = Factory.GetComponent(); + cloneComp.Action = ent.Comp.Action; + cloneComp.Layer = ent.Comp.Layer; + cloneComp.Organ = ent.Comp.Organ; + cloneComp.Suffix = ent.Comp.Suffix; + AddComp(args.CloneUid, cloneComp, true); } - private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args) + private void OnWaggingMapInit(Entity ent, ref MapInitEvent args) { - _actions.AddAction(uid, ref component.ActionEntity, component.Action, uid); + _actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action, ent); } - private void OnWaggingShutdown(EntityUid uid, WaggingComponent component, ComponentShutdown args) + private void OnWaggingShutdown(Entity ent, ref ComponentShutdown args) { - _actions.RemoveAction(uid, component.ActionEntity); + _actions.RemoveAction(ent.Owner, ent.Comp.ActionEntity); } - private void OnWaggingToggle(EntityUid uid, WaggingComponent component, ref ToggleActionEvent args) + private void OnWaggingToggle(Entity ent, ref ToggleActionEvent args) { if (args.Handled) return; - TryToggleWagging(uid, wagging: component); + TryToggleWagging(ent.AsNullable()); } - private void OnMobStateChanged(EntityUid uid, WaggingComponent component, MobStateChangedEvent args) + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) { - if (component.Wagging) - TryToggleWagging(uid, wagging: component); + if (ent.Comp.Wagging) + TryToggleWagging(ent.AsNullable()); } - public bool TryToggleWagging(EntityUid uid, WaggingComponent? wagging = null, HumanoidAppearanceComponent? humanoid = null) + private bool TryToggleWagging(Entity ent) { - if (!Resolve(uid, ref wagging, ref humanoid)) + if (!Resolve(ent, ref ent.Comp)) return false; - if (!humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Tail, out var markings)) + if (!_visualBody.TryGatherMarkingsData(ent.Owner, + [ent.Comp.Layer], + out _, + out _, + out var applied)) + { return false; + } - if (markings.Count == 0) + if (!applied.TryGetValue(ent.Comp.Organ, out var markingsSet)) return false; - wagging.Wagging = !wagging.Wagging; + ent.Comp.Wagging = !ent.Comp.Wagging; - for (var idx = 0; idx < markings.Count; idx++) // Animate all possible tails + markingsSet = markingsSet.ShallowClone(); + foreach (var (layers, markings) in markingsSet) { - var currentMarkingId = markings[idx].MarkingId; - string newMarkingId; + markingsSet[layers] = markingsSet[layers].ShallowClone(); + var layerMarkings = markingsSet[layers]; - if (wagging.Wagging) - { - newMarkingId = $"{currentMarkingId}{wagging.Suffix}"; - } - else + for (int i = 0; i < layerMarkings.Count; i++) { - if (currentMarkingId.EndsWith(wagging.Suffix)) + var currentMarkingId = layerMarkings[i].MarkingId; + string newMarkingId; + + if (ent.Comp.Wagging) { - newMarkingId = currentMarkingId[..^wagging.Suffix.Length]; + newMarkingId = $"{currentMarkingId}{ent.Comp.Suffix}"; } else { - newMarkingId = currentMarkingId; - Log.Warning($"Unable to revert wagging for {currentMarkingId}"); + if (currentMarkingId.Id.EndsWith(ent.Comp.Suffix)) + { + newMarkingId = currentMarkingId.Id[..^ent.Comp.Suffix.Length]; + } + else + { + newMarkingId = currentMarkingId; + Log.Warning($"Unable to revert wagging for {currentMarkingId}"); + } } - } - if (!_prototype.HasIndex(newMarkingId)) - { - Log.Warning($"{ToPrettyString(uid)} tried toggling wagging but {newMarkingId} marking doesn't exist"); - continue; - } + if (!_prototype.HasIndex(newMarkingId)) + { + Log.Warning($"{ToPrettyString(ent):ent} tried toggling wagging but {newMarkingId} marking doesn't exist"); + continue; + } - _humanoidAppearance.SetMarkingId(uid, MarkingCategories.Tail, idx, newMarkingId, - humanoid: humanoid); + layerMarkings[i] = new Marking(newMarkingId, layerMarkings[i].MarkingColors); + } } + _visualBody.ApplyMarkings(ent, new() + { + [ent.Comp.Organ] = markingsSet + }); return true; } } diff --git a/Content.Server/Weapons/DamageMarkerSystem.cs b/Content.Server/Weapons/DamageMarkerSystem.cs index be65bca1686..4e57b98ae06 100644 --- a/Content.Server/Weapons/DamageMarkerSystem.cs +++ b/Content.Server/Weapons/DamageMarkerSystem.cs @@ -2,7 +2,4 @@ namespace Content.Server.Weapons; -public sealed class DamageMarkerSystem : SharedDamageMarkerSystem -{ - -} +public sealed class DamageMarkerSystem : SharedDamageMarkerSystem { } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 1e8f3334ac5..2efe94c20e5 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Movement.Systems; -using Content.Shared.Damage.Events; -using Content.Shared.Damage.Systems; +using Content.Shared.Chat; using Content.Shared.Effects; using Content.Shared.Speech.Components; using Content.Shared.Weapons.Melee; @@ -16,28 +15,14 @@ namespace Content.Server.Weapons.Melee; public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem { [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly LagCompensationSystem _lag = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnSpeechHit); - SubscribeLocalEvent(OnMeleeExamineDamage); - } - - private void OnMeleeExamineDamage(EntityUid uid, MeleeWeaponComponent component, ref DamageExamineEvent args) - { - if (component.Hidden) - return; - var damageSpec = GetDamage(uid, args.User, component); - - if (damageSpec.Empty) - return; - - _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), Loc.GetString("damage-melee")); + SubscribeLocalEvent(OnSpeechHit); } protected override bool ArcRaySuccessful(EntityUid targetUid, diff --git a/Content.Server/Weapons/Misc/TetherGunSystem.cs b/Content.Server/Weapons/Misc/TetherGunSystem.cs index 2bf53d46f4b..d1984448c5d 100644 --- a/Content.Server/Weapons/Misc/TetherGunSystem.cs +++ b/Content.Server/Weapons/Misc/TetherGunSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.PowerCell; using Content.Shared.Item.ItemToggle; using Content.Shared.PowerCell; using Content.Shared.Weapons.Misc; diff --git a/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs b/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs index 3815ce15dbf..5e70b840d20 100644 --- a/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs @@ -3,4 +3,4 @@ namespace Content.Server.Weapons.Ranged.Components; [RegisterComponent] -public sealed partial class AmmoCounterComponent : SharedAmmoCounterComponent {} +public sealed partial class AmmoCounterComponent : SharedAmmoCounterComponent { } diff --git a/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs b/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs index b9a5eca09b6..928ec257bbf 100644 --- a/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs @@ -1,11 +1,10 @@ -namespace Content.Server.Weapons.Ranged.Components +namespace Content.Server.Weapons.Ranged.Components; + +[RegisterComponent] +public sealed partial class ChemicalAmmoComponent : Component { - [RegisterComponent] - public sealed partial class ChemicalAmmoComponent : Component - { - public const string DefaultSolutionName = "ammo"; + public const string DefaultSolutionName = "ammo"; - [DataField("solution")] - public string SolutionName { get; set; } = DefaultSolutionName; - } + [DataField("solution")] + public string SolutionName { get; set; } = DefaultSolutionName; } diff --git a/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs b/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs index ccb5119af86..d9b96a1adc4 100644 --- a/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs @@ -16,15 +16,13 @@ public sealed partial class RangedDamageSoundComponent : Component /// Specified sounds to apply when the entity takes damage with the specified group. /// Will fallback to defaults if none specified. /// - [DataField("soundGroups", - customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] public Dictionary? SoundGroups; /// /// Specified sounds to apply when the entity takes damage with the specified type. /// Will fallback to defaults if none specified. /// - [DataField("soundTypes", - customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] public Dictionary? SoundTypes; } diff --git a/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs b/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs index 089ef23a144..4ee1e9fd929 100644 --- a/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs @@ -1,49 +1,48 @@ using Content.Server.Weapons.Ranged.Components; using Content.Shared.Chemistry.Components; -using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Weapons.Ranged.Events; using System.Linq; -namespace Content.Server.Weapons.Ranged.Systems +namespace Content.Server.Weapons.Ranged.Systems; + +public sealed class ChemicalAmmoSystem : EntitySystem { - public sealed class ChemicalAmmoSystem : EntitySystem - { - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - public override void Initialize() - { - SubscribeLocalEvent(OnFire); - } + public override void Initialize() + { + SubscribeLocalEvent(OnFire); + } - private void OnFire(Entity entity, ref AmmoShotEvent args) - { - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var ammoSoln, out var ammoSolution)) - return; + private void OnFire(Entity entity, ref AmmoShotEvent args) + { + if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var ammoSoln, out var ammoSolution)) + return; - var projectiles = args.FiredProjectiles; + var projectiles = args.FiredProjectiles; - var projectileSolutionContainers = new List<(EntityUid, Entity)>(); - foreach (var projectile in projectiles) + var projectileSolutionContainers = new List<(EntityUid, Entity)>(); + foreach (var projectile in projectiles) + { + if (_solutionContainerSystem + .TryGetSolution(projectile, entity.Comp.SolutionName, out var projectileSoln, out _)) { - if (_solutionContainerSystem - .TryGetSolution(projectile, entity.Comp.SolutionName, out var projectileSoln, out _)) - { - projectileSolutionContainers.Add((projectile, projectileSoln.Value)); - } + projectileSolutionContainers.Add((projectile, projectileSoln.Value)); } + } - if (!projectileSolutionContainers.Any()) - return; - - var solutionPerProjectile = ammoSolution.Volume * (1 / projectileSolutionContainers.Count); + if (!projectileSolutionContainers.Any()) + return; - foreach (var (_, projectileSolution) in projectileSolutionContainers) - { - var solutionToTransfer = _solutionContainerSystem.SplitSolution(ammoSoln.Value, solutionPerProjectile); - _solutionContainerSystem.TryAddSolution(projectileSolution, solutionToTransfer); - } + var solutionPerProjectile = ammoSolution.Volume * (1 / projectileSolutionContainers.Count); - _solutionContainerSystem.RemoveAllSolution(ammoSoln.Value); + foreach (var (_, projectileSolution) in projectileSolutionContainers) + { + var solutionToTransfer = _solutionContainerSystem.SplitSolution(ammoSoln.Value, solutionPerProjectile); + _solutionContainerSystem.TryAddSolution(projectileSolution, solutionToTransfer); } + + _solutionContainerSystem.RemoveAllSolution(ammoSoln.Value); } } diff --git a/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs b/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs index f9986767fe4..03a46de1c24 100644 --- a/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs @@ -2,4 +2,4 @@ namespace Content.Server.Weapons.Ranged.Systems; -public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {} +public sealed class FlyBySoundSystem : SharedFlyBySoundSystem { } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs index 47b0d5f3c6f..18a91cd6455 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs @@ -1,4 +1,4 @@ -using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Map; @@ -27,15 +27,15 @@ public override void Update(float frameTime) if (!autoShoot.Enabled) continue; - AttemptShoot(uid, gun); + AttemptShoot((uid, gun)); } else if (gun.BurstActivated) { var parent = TransformSystem.GetParentUid(uid); if (HasComp(parent)) - AttemptShoot(parent, uid, gun, gun.ShootCoordinates ?? new EntityCoordinates(uid, gun.DefaultDirection)); + AttemptShoot(parent, (uid, gun), gun.ShootCoordinates ?? new EntityCoordinates(uid, gun.DefaultDirection)); else - AttemptShoot(uid, gun); + AttemptShoot((uid, gun)); } } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index 60680deaaaa..87d958d9d15 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -6,32 +6,32 @@ namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { - protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates) + protected override void Cycle(Entity ent, MapCoordinates coordinates) { - EntityUid? ent = null; + EntityUid? ammoEnt = null; // TODO: Combine with TakeAmmo - if (component.Entities.Count > 0) + if (ent.Comp.Entities.Count > 0) { - var existing = component.Entities[^1]; - component.Entities.RemoveAt(component.Entities.Count - 1); - DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities)); + var existing = ent.Comp.Entities[^1]; + ent.Comp.Entities.RemoveAt(ent.Comp.Entities.Count - 1); + DirtyField(ent.AsNullable(), nameof(BallisticAmmoProviderComponent.Entities)); - Containers.Remove(existing, component.Container); + Containers.Remove(existing, ent.Comp.Container); EnsureShootable(existing); } - else if (component.UnspawnedCount > 0) + else if (ent.Comp.UnspawnedCount > 0) { - component.UnspawnedCount--; - DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount)); - ent = Spawn(component.Proto, coordinates); - EnsureShootable(ent.Value); + ent.Comp.UnspawnedCount--; + DirtyField(ent.AsNullable(), nameof(BallisticAmmoProviderComponent.UnspawnedCount)); + ammoEnt = Spawn(ent.Comp.Proto, coordinates); + EnsureShootable(ammoEnt.Value); } - if (ent != null) - EjectCartridge(ent.Value); + if (ammoEnt != null) + EjectCartridge(ammoEnt.Value); var cycledEvent = new GunCycledEvent(); - RaiseLocalEvent(uid, ref cycledEvent); + RaiseLocalEvent(ent, ref cycledEvent); } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs deleted file mode 100644 index 4865d08782d..00000000000 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Content.Server.Power.Components; -using Content.Shared.Damage; -using Content.Shared.Damage.Events; -using Content.Shared.FixedPoint; -using Content.Shared.PowerCell.Components; -using Content.Shared.Projectiles; -using Content.Shared.Weapons.Ranged; -using Content.Shared.Weapons.Ranged.Components; -using Content.Shared.Weapons.Ranged.Events; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weapons.Ranged.Systems; - -public sealed partial class GunSystem -{ - protected override void InitializeBattery() - { - base.InitializeBattery(); - - // Hitscan - SubscribeLocalEvent(OnBatteryStartup); - SubscribeLocalEvent(OnBatteryChargeChange); - SubscribeLocalEvent(OnBatteryDamageExamine); - SubscribeLocalEvent(OnPowerCellChanged); - - // Projectile - SubscribeLocalEvent(OnBatteryStartup); - SubscribeLocalEvent(OnBatteryChargeChange); - SubscribeLocalEvent(OnBatteryDamageExamine); - SubscribeLocalEvent(OnPowerCellChanged); - } - - private void OnBatteryStartup(Entity entity, ref ComponentStartup args) where T : BatteryAmmoProviderComponent - { - UpdateShots(entity, entity.Comp); - } - - private void OnBatteryChargeChange(Entity entity, ref ChargeChangedEvent args) where T : BatteryAmmoProviderComponent - { - UpdateShots(entity, entity.Comp, args.Charge, args.MaxCharge); - } - - private void OnPowerCellChanged(Entity entity, ref PowerCellChangedEvent args) where T : BatteryAmmoProviderComponent - { - UpdateShots(entity, entity.Comp); - } - - private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component) - { - var ev = new GetChargeEvent(); - RaiseLocalEvent(uid, ref ev); - - UpdateShots(uid, component, ev.CurrentCharge, ev.MaxCharge); - } - - private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component, float charge, float maxCharge) - { - var shots = (int) (charge / component.FireCost); - var maxShots = (int) (maxCharge / component.FireCost); - - if (component.Shots != shots || component.Capacity != maxShots) - { - Dirty(uid, component); - } - - component.Shots = shots; - - if (maxShots > 0) - component.Capacity = maxShots; - - UpdateBatteryAppearance(uid, component); - - var updateAmmoEv = new UpdateClientAmmoEvent(); - RaiseLocalEvent(uid, ref updateAmmoEv); - } - - private void OnBatteryDamageExamine(Entity entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent - { - var damageSpec = GetDamage(entity.Comp); - - if (damageSpec == null) - return; - - var damageType = entity.Comp switch - { - HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"), - ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"), - _ => throw new ArgumentOutOfRangeException(), - }; - - _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType); - } - - private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component) - { - if (component is ProjectileBatteryAmmoProviderComponent battery) - { - if (ProtoManager.Index(battery.Prototype).Components - .TryGetValue(Factory.GetComponentName(), out var projectile)) - { - var p = (ProjectileComponent) projectile.Component; - - if (!p.Damage.Empty) - { - return p.Damage * Damageable.UniversalProjectileDamageModifier; - } - } - - return null; - } - - if (component is HitscanBatteryAmmoProviderComponent hitscan) - { - var dmg = ProtoManager.Index(hitscan.Prototype).Damage; - return dmg == null ? dmg : dmg * Damageable.UniversalHitscanDamageModifier; - } - - return null; - } - - protected override void TakeCharge(Entity entity) - { - var ev = new ChangeChargeEvent(-entity.Comp.FireCost); - RaiseLocalEvent(entity, ref ev); - } -} diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs index 59e53f1f72a..7f2fa28b13d 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs @@ -4,15 +4,15 @@ namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { - protected override void SpinRevolver(EntityUid revolverUid, RevolverAmmoProviderComponent component, EntityUid? user = null) + protected override void SpinRevolver(Entity ent, EntityUid? user = null) { - base.SpinRevolver(revolverUid, component, user); - var index = Random.Next(component.Capacity); + base.SpinRevolver(ent, user); + var index = Random.Next(ent.Comp.Capacity); - if (component.CurrentIndex == index) + if (ent.Comp.CurrentIndex == index) return; - component.CurrentIndex = index; - Dirty(revolverUid, component); + ent.Comp.CurrentIndex = index; + Dirty(ent); } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs index d0898efaaf8..eddc8739bb3 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Solution.cs @@ -23,63 +23,63 @@ protected override void InitializeSolution() private void OnSolutionMapInit(Entity entity, ref MapInitEvent args) { - UpdateSolutionShots(entity.Owner, entity.Comp); + UpdateSolutionShots(entity); } private void OnSolutionChanged(Entity entity, ref SolutionContainerChangedEvent args) { if (args.Solution.Name == entity.Comp.SolutionId) - UpdateSolutionShots(entity.Owner, entity.Comp, args.Solution); + UpdateSolutionShots(entity, args.Solution); } - protected override void UpdateSolutionShots(EntityUid uid, SolutionAmmoProviderComponent component, Solution? solution = null) + protected override void UpdateSolutionShots(Entity ent, Solution? solution = null) { var shots = 0; var maxShots = 0; - if (solution == null && !_solutionContainer.TryGetSolution(uid, component.SolutionId, out _, out solution)) + if (solution == null && !_solutionContainer.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out _, out solution)) { - component.Shots = shots; - DirtyField(uid, component, nameof(SolutionAmmoProviderComponent.Shots)); - component.MaxShots = maxShots; - DirtyField(uid, component, nameof(SolutionAmmoProviderComponent.MaxShots)); + ent.Comp.Shots = shots; + DirtyField(ent.AsNullable(), nameof(SolutionAmmoProviderComponent.Shots)); + ent.Comp.MaxShots = maxShots; + DirtyField(ent.AsNullable(), nameof(SolutionAmmoProviderComponent.MaxShots)); return; } - shots = (int) (solution.Volume / component.FireCost); - maxShots = (int) (solution.MaxVolume / component.FireCost); + shots = (int)(solution.Volume / ent.Comp.FireCost); + maxShots = (int)(solution.MaxVolume / ent.Comp.FireCost); - component.Shots = shots; - DirtyField(uid, component, nameof(SolutionAmmoProviderComponent.Shots)); + ent.Comp.Shots = shots; + DirtyField(ent.AsNullable(), nameof(SolutionAmmoProviderComponent.Shots)); - component.MaxShots = maxShots; - DirtyField(uid, component, nameof(SolutionAmmoProviderComponent.MaxShots)); + ent.Comp.MaxShots = maxShots; + DirtyField(ent.AsNullable(), nameof(SolutionAmmoProviderComponent.MaxShots)); - UpdateSolutionAppearance(uid, component); + UpdateSolutionAppearance(ent); } - protected override (EntityUid Entity, IShootable) GetSolutionShot(EntityUid uid, SolutionAmmoProviderComponent component, EntityCoordinates position) + protected override (EntityUid Entity, IShootable) GetSolutionShot(Entity ent, EntityCoordinates position) { - var (ent, shootable) = base.GetSolutionShot(uid, component, position); + var (shot, shootable) = base.GetSolutionShot(ent, position); - if (!_solutionContainer.TryGetSolution(uid, component.SolutionId, out var solution, out _)) - return (ent, shootable); + if (!_solutionContainer.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out var solution, out _)) + return (shot, shootable); - var newSolution = _solutionContainer.SplitSolution(solution.Value, component.FireCost); + var newSolution = _solutionContainer.SplitSolution(solution.Value, ent.Comp.FireCost); if (newSolution.Volume <= FixedPoint2.Zero) - return (ent, shootable); + return (shot, shootable); - if (TryComp(ent, out var appearance)) + if (TryComp(shot, out var appearance)) { - Appearance.SetData(ent, VaporVisuals.Color, newSolution.GetColor(ProtoManager).WithAlpha(1f), appearance); - Appearance.SetData(ent, VaporVisuals.State, true, appearance); + Appearance.SetData(shot, VaporVisuals.Color, newSolution.GetColor(ProtoManager).WithAlpha(1f), appearance); + Appearance.SetData(shot, VaporVisuals.State, true, appearance); } // Add the solution to the vapor and actually send the thing - if (_solutionContainer.TryGetSolution(ent, VaporComponent.SolutionName, out var vaporSolution, out _)) + if (_solutionContainer.TryGetSolution(shot, VaporComponent.SolutionName, out var vaporSolution, out _)) { _solutionContainer.TryAddSolution(vaporSolution.Value, newSolution); } - return (ent, shootable); + return (shot, shootable); } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index ac5267a470c..e3da4393ea0 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -1,23 +1,19 @@ -using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; using Content.Server.Weapons.Ranged.Components; using Content.Shared.Cargo; using Content.Shared.Damage; using Content.Shared.Damage.Systems; -using Content.Shared.Database; -using Content.Shared.Effects; using Content.Shared.Projectiles; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; -using Content.Shared.Weapons.Reflect; -using Content.Shared.Damage.Components; +using Content.Shared.Weapons.Hitscan.Components; +using Content.Shared.Weapons.Hitscan.Events; using Robust.Shared.Audio; using Robust.Shared.Map; -using Robust.Shared.Physics; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -25,13 +21,13 @@ using Content.Server.Imperial.Medieval.Weapons; using Robust.Shared.Threading; using Robust.Shared.Random; -using Content.Server.PowerCell; +using Content.Shared.Effects; +using Content.Shared.PowerCell; namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem : SharedGunSystem { - [Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedStaminaSystem _stamina = default!; @@ -47,23 +43,23 @@ public override void Initialize() SubscribeLocalEvent(OnBallisticPrice); } - private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args) + private void OnBallisticPrice(Entity ent, ref PriceCalculationEvent args) { - if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0) + if (string.IsNullOrEmpty(ent.Comp.Proto) || ent.Comp.UnspawnedCount == 0) return; - if (!ProtoManager.TryIndex(component.Proto, out var proto)) + if (!ProtoManager.TryIndex(ent.Comp.Proto, out var proto)) { - Log.Error($"Unable to find fill prototype for price on {component.Proto} on {ToPrettyString(uid)}"); + Log.Error($"Unable to find fill prototype for price on {ent.Comp.Proto} on {ToPrettyString(ent)}"); return; } // Probably good enough for most. var price = _pricing.GetEstimatedPrice(proto); - args.Price += price * component.UnspawnedCount; + args.Price += price * ent.Comp.UnspawnedCount; } - public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, + public override void Shoot(Entity gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false) { userImpulse = true; @@ -72,7 +68,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? if (user != null) { - var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo); + var selfEvent = new SelfBeforeGunShotEvent(user.Value, gun, ammo); RaiseLocalEvent(user.Value, selfEvent); if (selfEvent.Cancelled) { @@ -80,7 +76,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? return; } // Imperial Medieval Skills start - var spreadEv = new GetGunSpreadModifiersEvent(gun.DefaultSpread); + var spreadEv = new GetGunSpreadModifiersEvent(gun.Comp.DefaultSpread); // Imperial Medieval: Entity RaiseLocalEvent(user.Value, ref spreadEv); spread = _random.NextFloat(-spreadEv.Modifier, spreadEv.Modifier); // Imperial Medieval Skills end @@ -112,10 +108,11 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? // pneumatic cannon doesn't shoot bullets it just throws them, ignore ammo handling if (throwItems && ent != null) { - ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user); + ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, user); continue; } + // TODO: Clean this up in a gun refactor at some point - too much copy pasting switch (shootable) { // Cartridge shoots something else @@ -138,7 +135,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? else { userImpulse = false; - Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); + Audio.PlayPredicted(gun.Comp.SoundEmpty, gun, user); } // Something like ballistic might want to leave it in the container still @@ -154,116 +151,30 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? CreateAndFireProjectiles(ent.Value, newAmmo); break; - case HitscanPrototype hitscan: - - EntityUid? lastHit = null; - - var from = fromMap; - // can't use map coords above because funny FireEffects - var fromEffect = fromCoordinates; - var dir = mapDirection.Normalized(); - - //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case - var lastUser = user ?? gunUid; - - if (hitscan.Reflective != ReflectType.None) - { - for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++) - { - var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask); - var rayCastResults = - Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList(); - if (!rayCastResults.Any()) - break; - - var result = rayCastResults[0]; - - // Check if laser is shot from in a container - if (!_container.IsEntityOrParentInContainer(lastUser)) - { - // Checks if the laser should pass over unless targeted by its user - foreach (var collide in rayCastResults) - { - if (collide.HitEntity != gun.Target && - CompOrNull(collide.HitEntity)?.Active == true) - { - continue; - } - - result = collide; - break; - } - } - - var hit = result.HitEntity; - lastHit = hit; - - FireEffects(fromEffect, result.Distance, dir.Normalized().ToAngle(), hitscan, hit); - - var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false); - RaiseLocalEvent(hit, ref ev); - - if (!ev.Reflected) - break; - - fromEffect = Transform(hit).Coordinates; - from = TransformSystem.ToMapCoordinates(fromEffect); - dir = ev.Direction; - lastUser = hit; - } - } + case HitscanAmmoComponent: + if (ent == null) + break; - if (lastHit != null) + var hitscanEv = new HitscanTraceEvent { - var hitEntity = lastHit.Value; - if (hitscan.StaminaDamage > 0f) - _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user); - - var dmg = hitscan.Damage; + FromCoordinates = fromCoordinates, + ShotDirection = mapDirection.Normalized(), + Gun = gun, + Shooter = user, + Target = gun.Comp.Target, + }; + RaiseLocalEvent(ent.Value, ref hitscanEv); - var hitName = ToPrettyString(hitEntity); - if (dmg != null) - dmg = Damageable.TryChangeDamage(hitEntity, dmg * Damageable.UniversalHitscanDamageModifier, origin: user); + Del(ent); - // check null again, as TryChangeDamage returns modified damage values - if (dmg != null) - { - if (!Deleted(hitEntity)) - { - if (dmg.AnyPositive()) - { - _color.RaiseEffect(Color.Red, new List() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); - } - - // TODO get fallback position for playing hit sound. - PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); - } - - if (user != null) - { - Logs.Add(LogType.HitScanHit, - $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage"); - } - else - { - Logs.Add(LogType.HitScanHit, - $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage"); - } - } - } - else - { - FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan); - } - - Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user); break; default: throw new ArgumentOutOfRangeException(); } } - RaiseLocalEvent(gunUid, new AmmoShotEvent() + RaiseLocalEvent(gun, new AmmoShotEvent() { FiredProjectiles = shotProjectiles, }); @@ -273,35 +184,35 @@ void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp) if (TryComp(ammoEnt, out var ammoSpreadComp)) { var spreadEvent = new GunGetAmmoSpreadEvent(ammoSpreadComp.Spread); - RaiseLocalEvent(gunUid, ref spreadEvent); + RaiseLocalEvent(gun, ref spreadEvent); var angles = LinearSpread(mapAngle - spreadEvent.Spread / 2, mapAngle + spreadEvent.Spread / 2, ammoSpreadComp.Count); - ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, gunUid, user); + ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, user); shotProjectiles.Add(ammoEnt); for (var i = 1; i < ammoSpreadComp.Count; i++) { var newuid = Spawn(ammoSpreadComp.Proto, fromEnt); - ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); + ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, user); shotProjectiles.Add(newuid); } } else { - ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, gunUid, user); + ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, user); shotProjectiles.Add(ammoEnt); } - MuzzleFlash(gunUid, ammoComp, mapDirection.ToAngle(), user); - Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + MuzzleFlash(gun, ammoComp, mapDirection.ToAngle(), user); + Audio.PlayPredicted(gun.Comp.SoundGunshotModified, gun, user); } } - private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user) + private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, Entity gun, EntityUid? user) { - if (gun.Target is { } target && !TerminatingOrDeleted(target)) + if (gun.Comp.Target is { } target && !TerminatingOrDeleted(target)) { var targeted = EnsureComp(uid); targeted.Target = target; @@ -313,11 +224,11 @@ private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVeloci { RemoveShootable(uid); // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack. - ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeedModified, user); + ThrowingSystem.TryThrow(uid, mapDirection, gun.Comp.ProjectileSpeedModified, user); return; } - ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified); + ShootProjectile(uid, mapDirection, gunVelocity, gun, user, gun.Comp.ProjectileSpeedModified); } /// @@ -366,7 +277,7 @@ protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, RaiseNetworkEvent(message, filter); } - public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) + public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted"); @@ -397,69 +308,4 @@ public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDama Audio.PlayPvs(weaponSound, otherEntity); } } - - // TODO: Pseudo RNG so the client can predict these. - #region Hitscan effects - - private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null) - { - // Lord - // Forgive me for the shitcode I am about to do - // Effects tempt me not - var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>(); - var fromXform = Transform(fromCoordinates.EntityId); - - // We'll get the effects relative to the grid / map of the firer - // Look you could probably optimise this a bit with redundant transforms at this point. - - var gridUid = fromXform.GridUid; - if (gridUid != fromCoordinates.EntityId && TryComp(gridUid, out TransformComponent? gridXform)) - { - var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform); - var map = TransformSystem.ToMapCoordinates(fromCoordinates); - fromCoordinates = new EntityCoordinates(gridUid.Value, Vector2.Transform(map.Position, gridInvMatrix)); - angle -= gridRot; - } - else - { - angle -= TransformSystem.GetWorldRotation(fromXform); - } - - if (distance >= 1f) - { - if (hitscan.MuzzleFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f)); - } - - if (hitscan.TravelFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f)); - } - } - - if (hitscan.ImpactFlash != null) - { - var coords = fromCoordinates.Offset(angle.ToVec() * distance); - var netCoords = GetNetCoordinates(coords); - - sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f)); - } - - if (sprites.Count > 0) - { - RaiseNetworkEvent(new HitscanEvent - { - Sprites = sprites, - }, Filter.Pvs(fromCoordinates, entityMan: EntityManager)); - } - } - - #endregion } diff --git a/Content.Server/Weather/Commands/WeatherAddCommand.cs b/Content.Server/Weather/Commands/WeatherAddCommand.cs new file mode 100644 index 00000000000..6054012db0c --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherAddCommand.cs @@ -0,0 +1,89 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Add specific weather to map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherAddCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatheradd"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + //Time parsing + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TryAddWeather(mapId, weatherProto, out _, duration); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/Commands/WeatherRemoveCommand.cs b/Content.Server/Weather/Commands/WeatherRemoveCommand.cs new file mode 100644 index 00000000000..df8f91003f0 --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherRemoveCommand.cs @@ -0,0 +1,82 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Remove specific weather from map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherRemoveCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherremove"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + if (!_weather.HasWeather(mapId, weatherProto)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-weather")); + return; + } + + _weather.TryRemoveWeather(mapId, weatherProto); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) //TODO: dont show ALL weathers here, only weathers applied to selected map + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/Commands/WeatherSetCommand.cs b/Content.Server/Weather/Commands/WeatherSetCommand.cs new file mode 100644 index 00000000000..77e8ed786d7 --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherSetCommand.cs @@ -0,0 +1,91 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherSetCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherset"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId? weatherProto = args[1]; + if (args[1] == "null") + weatherProto = null; + else if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + //Time parsing + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TrySetWeather(mapId, weatherProto, out _, duration); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/WeatherSystem.cs b/Content.Server/Weather/WeatherSystem.cs index 1ed61b8626b..b3a0bea8822 100644 --- a/Content.Server/Weather/WeatherSystem.cs +++ b/Content.Server/Weather/WeatherSystem.cs @@ -1,92 +1,28 @@ -using Content.Server.Administration; -using Content.Shared.Administration; using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.GameStates; -using Robust.Shared.Map; -using System.Linq; +using Robust.Server.GameStates; namespace Content.Server.Weather; public sealed class WeatherSystem : SharedWeatherSystem { - [Dependency] private readonly IConsoleHost _console = default!; - [Dependency] private readonly SharedMapSystem _mapSystem = default!; + //I dont really like to PVS override weather entities, but map status effect containers dont PVS-ing out of the box + [Dependency] private readonly PvsOverrideSystem _pvs = default!; public override void Initialize() { - base.Initialize(); - SubscribeLocalEvent(OnWeatherGetState); - _console.RegisterCommand("weather", - Loc.GetString("cmd-weather-desc"), - Loc.GetString("cmd-weather-help"), - WeatherTwo, - WeatherCompletion); + SubscribeLocalEvent(OnCompInit); + SubscribeLocalEvent(OnCompShutdown); } - private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args) + private void OnCompInit(Entity ent, ref ComponentInit args) { - args.State = new WeatherComponentState(component.Weather); + // The map entitiy itself is networked by PVS if the player is on that map but not anything inside a container, + // So we need to add an overridce to make sure the client sees it. + _pvs.AddGlobalOverride(ent); } - [AdminCommand(AdminFlags.Fun)] - private void WeatherTwo(IConsoleShell shell, string argStr, string[] args) + private void OnCompShutdown(Entity ent, ref ComponentShutdown args) { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_mapSystem.MapExists(mapId)) - return; - - if (!_mapSystem.TryGetMap(mapId, out var mapUid)) - return; - - var weatherComp = EnsureComp(mapUid.Value); - - //Weather Proto parsing - WeatherPrototype? weather = null; - if (!args[1].Equals("null")) - { - if (!ProtoMan.TryIndex(args[1], out weather)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - } - - //Time parsing - TimeSpan? endTime = null; - if (args.Length == 3) - { - var curTime = Timing.CurTime; - if (int.TryParse(args[2], out var durationInt)) - { - endTime = curTime + TimeSpan.FromSeconds(durationInt); - } - else - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - } - - SetWeather(mapId, weather, endTime); - } - - private CompletionResult WeatherCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map Id"); - - var a = CompletionHelper.PrototypeIDs(true, ProtoMan); - var b = a.Concat(new[] { new CompletionOption("null", Loc.GetString("cmd-weather-null")) }); - return CompletionResult.FromHintOptions(b, Loc.GetString("cmd-weather-hint")); + _pvs.RemoveGlobalOverride(ent); } } diff --git a/Content.Server/Wires/WiresSystem.cs b/Content.Server/Wires/WiresSystem.cs index c9fd429c479..e4d7861029f 100644 --- a/Content.Server/Wires/WiresSystem.cs +++ b/Content.Server/Wires/WiresSystem.cs @@ -28,7 +28,6 @@ public sealed class WiresSystem : SharedWiresSystem [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ConstructionSystem _construction = default!; @@ -448,7 +447,7 @@ private void OnInteractUsing(EntityUid uid, WiresComponent component, InteractUs { if (TryComp(args.User, out ActorComponent? actor)) { - _uiSystem.OpenUi(uid, WiresUiKey.Key, actor.PlayerSession); + UI.OpenUi(uid, WiresUiKey.Key, actor.PlayerSession); args.Handled = true; } } @@ -459,7 +458,7 @@ private void OnPanelChanged(Entity ent, ref PanelChangedEvent ar if (args.Open) return; - _uiSystem.CloseUi(ent.Owner, WiresUiKey.Key); + UI.CloseUi(ent.Owner, WiresUiKey.Key); } private void OnMapInit(EntityUid uid, WiresComponent component, MapInitEvent args) @@ -548,7 +547,7 @@ private void UpdateUserInterface(EntityUid uid, WiresComponent? wires = null, Us statuses.Sort((a, b) => a.position.CompareTo(b.position)); - _uiSystem.SetUiState((uid, ui), WiresUiKey.Key, new WiresBoundUserInterfaceState( + UI.SetUiState((uid, ui), WiresUiKey.Key, new WiresBoundUserInterfaceState( clientList.ToArray(), statuses.Select(p => new StatusEntry(p.key, p.value)).ToArray(), Loc.GetString(wires.BoardName), @@ -556,11 +555,6 @@ private void UpdateUserInterface(EntityUid uid, WiresComponent? wires = null, Us wires.WireSeed)); } - public void OpenUserInterface(EntityUid uid, ICommonSession player) - { - _uiSystem.OpenUi(uid, WiresUiKey.Key, player); - } - /// /// Tries to get a wire on this entity by its integer id. /// @@ -602,7 +596,7 @@ public void SetWiresPanelSecurity(EntityUid uid, WiresPanelSecurityComponent com if (!args.WiresAccessible) { - _uiSystem.CloseUi(uid, WiresUiKey.Key); + UI.CloseUi(uid, WiresUiKey.Key); } } diff --git a/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs b/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs deleted file mode 100644 index 08571cd588c..00000000000 --- a/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Biomes; -using Robust.Shared.Prototypes; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for selecting the biome(s) to be used during world generation. -/// -[RegisterComponent] -[Access(typeof(BiomeSelectionSystem))] -public sealed partial class BiomeSelectionComponent : Component -{ - /// - /// The list of biomes available to this selector. - /// - /// This is always sorted by priority after ComponentStartup. - [DataField(required: true)] - public List> Biomes = new(); -} - diff --git a/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs b/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs deleted file mode 100644 index 28724d20a49..00000000000 --- a/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Carvers; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Carvers; - -/// -/// This is used for carving out empty space in the game world, providing byways through the debris field. -/// -[RegisterComponent] -[Access(typeof(NoiseRangeCarverSystem))] -public sealed partial class NoiseRangeCarverComponent : Component -{ - /// - /// The noise channel to use as a density controller. - /// - /// This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log. - [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string NoiseChannel { get; private set; } = default!; - - /// - /// The index of ranges in which to cut debris generation. - /// - [DataField("ranges", required: true)] - public List Ranges { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs b/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs deleted file mode 100644 index a1317ae2edf..00000000000 --- a/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Worldgen.Systems.Debris; -using Content.Shared.Maps; -using Robust.Shared.Prototypes; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for constructing asteroid debris. -/// -[RegisterComponent] -[Access(typeof(BlobFloorPlanBuilderSystem))] -public sealed partial class BlobFloorPlanBuilderComponent : Component -{ - /// - /// The probability that placing a floor tile will add up to three-four neighboring tiles as well. - /// - [DataField("blobDrawProb")] public float BlobDrawProb; - - /// - /// The maximum radius for the structure. - /// - [DataField("radius", required: true)] public float Radius; - - /// - /// The tiles to be used for the floor plan. - /// - [DataField(required: true)] - public List> FloorTileset { get; private set; } = default!; - - /// - /// The number of floor tiles to place when drawing the asteroid layout. - /// - [DataField("floorPlacements", required: true)] - public int FloorPlacements { get; private set; } -} - diff --git a/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs b/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs deleted file mode 100644 index ae61f0581e3..00000000000 --- a/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Debris; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for controlling the debris feature placer. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class DebrisFeaturePlacerControllerComponent : Component -{ - /// - /// Whether or not to clip debris that would spawn at a location that has a density of zero. - /// - [DataField("densityClip")] public bool DensityClip = true; - - /// - /// Whether or not entities are already spawned. - /// - public bool DoSpawns = true; - - [DataField("ownedDebris")] public Dictionary OwnedDebris = new(); - - /// - /// The chance spawning a piece of debris will just be cancelled randomly. - /// - [DataField("randomCancelChance")] public float RandomCancellationChance = 0.1f; - - /// - /// Radius in which there should be no objects for debris to spawn. - /// - [DataField("safetyZoneRadius")] public float SafetyZoneRadius = 16.0f; - - /// - /// The noise channel to use as a density controller. - /// - [DataField("densityNoiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DensityNoiseChannel { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs deleted file mode 100644 index af4ef7f1cf0..00000000000 --- a/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Storage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for selecting debris with a probability determined by a noise channel. -/// Takes priority over SimpleDebrisSelectorComponent and should likely be used in combination. -/// -[RegisterComponent] -[Access(typeof(NoiseDrivenDebrisSelectorSystem))] -public sealed partial class NoiseDrivenDebrisSelectorComponent : Component -{ - private EntitySpawnCollectionCache? _cache; - - /// - /// The prototype-facing debris table entries. - /// - [DataField("debrisTable", required: true)] - private List _entries = default!; - - /// - /// The debris entity spawn collection. - /// - public EntitySpawnCollectionCache CachedDebrisTable - { - get - { - _cache ??= new EntitySpawnCollectionCache(_entries); - return _cache; - } - } - - /// - /// The noise channel to use as a density controller. - /// - /// This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log. - [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string NoiseChannel { get; private set; } = default!; -} - diff --git a/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs b/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs deleted file mode 100644 index b2112779972..00000000000 --- a/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Systems.Debris; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for attaching a piece of debris to it's owning controller. -/// Mostly just syncs deletion. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class OwnedDebrisComponent : Component -{ - /// - /// The last location in the controller's internal structure for this debris. - /// - [DataField("lastKey")] public Vector2 LastKey; - - /// - /// The DebrisFeaturePlacerController-having entity that owns this. - /// - [DataField("owningController")] public EntityUid OwningController; -} - diff --git a/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs deleted file mode 100644 index 5db9bad9255..00000000000 --- a/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Storage; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for a very simple debris selection for simple biomes. Just uses a spawn table. -/// -[RegisterComponent] -[Access(typeof(DebrisFeaturePlacerSystem))] -public sealed partial class SimpleDebrisSelectorComponent : Component -{ - private EntitySpawnCollectionCache? _cache; - - /// - /// The prototype-facing debris table entries. - /// - [DataField("debrisTable", required: true)] - private List _entries = default!; - - /// - /// The debris entity spawn collection. - /// - public EntitySpawnCollectionCache CachedDebrisTable - { - get - { - _cache ??= new EntitySpawnCollectionCache(_entries); - return _cache; - } - } -} - diff --git a/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs deleted file mode 100644 index 4865773bf34..00000000000 --- a/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Systems.Debris; -using Content.Server.Worldgen.Tools; -using Content.Shared.Maps; -using Content.Shared.Storage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; - -namespace Content.Server.Worldgen.Components.Debris; - -/// -/// This is used for populating a grid with random entities automatically. -/// -[RegisterComponent] -[Access(typeof(SimpleFloorPlanPopulatorSystem))] -public sealed partial class SimpleFloorPlanPopulatorComponent : Component -{ - private Dictionary? _caches; - - /// - /// The prototype facing floor plan populator entries. - /// - [DataField("entries", required: true, - customTypeSerializer: typeof(PrototypeIdDictionarySerializer, ContentTileDefinition>))] - private Dictionary> _entries = default!; - - /// - /// The spawn collections used to place entities on different tile types. - /// - [ViewVariables] - public Dictionary Caches - { - get - { - if (_caches is null) - { - _caches = _entries - .Select(x => - new KeyValuePair(x.Key, - new EntitySpawnCollectionCache(x.Value))) - .ToDictionary(x => x.Key, x => x.Value); - } - - return _caches; - } - } -} - diff --git a/Content.Server/Worldgen/Components/LoadedChunkComponent.cs b/Content.Server/Worldgen/Components/LoadedChunkComponent.cs deleted file mode 100644 index d2743ad4ab3..00000000000 --- a/Content.Server/Worldgen/Components/LoadedChunkComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for marking a chunk as loaded. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class LoadedChunkComponent : Component -{ - /// - /// The current list of entities loading this chunk. - /// - [ViewVariables] public List? Loaders = null; -} - diff --git a/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs b/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs deleted file mode 100644 index 1d37ab34c90..00000000000 --- a/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for sending a signal to the entity it's on to load contents whenever a loader gets close enough. -/// Does not support unloading. -/// -[RegisterComponent] -[Access(typeof(LocalityLoaderSystem))] -public sealed partial class LocalityLoaderComponent : Component -{ - /// - /// The maximum distance an entity can be from the loader for it to not load. - /// Once a loader is closer than this, the event is fired and this component removed. - /// - [DataField("loadingDistance")] public int LoadingDistance = 32; -} - diff --git a/Content.Server/Worldgen/Components/NoiseIndexComponent.cs b/Content.Server/Worldgen/Components/NoiseIndexComponent.cs deleted file mode 100644 index 06d84d2f85f..00000000000 --- a/Content.Server/Worldgen/Components/NoiseIndexComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Worldgen.Prototypes; -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for containing configured noise generators. -/// -[RegisterComponent] -[Access(typeof(NoiseIndexSystem))] -public sealed partial class NoiseIndexComponent : Component -{ - /// - /// An index of generators, to avoid having to recreate them every time a noise channel is used. - /// Keyed by noise generator prototype ID. - /// - [Access(typeof(NoiseIndexSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.None)] - public Dictionary Generators { get; } = new(); -} - diff --git a/Content.Server/Worldgen/Components/WorldChunkComponent.cs b/Content.Server/Worldgen/Components/WorldChunkComponent.cs deleted file mode 100644 index 3a91c007564..00000000000 --- a/Content.Server/Worldgen/Components/WorldChunkComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for marking an entity as being a world chunk. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldChunkComponent : Component -{ - /// - /// The coordinates of the chunk, in chunk space. - /// - [DataField("coordinates")] public Vector2i Coordinates; - - /// - /// The map this chunk belongs to. - /// - [DataField("map")] public EntityUid Map; -} - diff --git a/Content.Server/Worldgen/Components/WorldControllerComponent.cs b/Content.Server/Worldgen/Components/WorldControllerComponent.cs deleted file mode 100644 index 63580e7541a..00000000000 --- a/Content.Server/Worldgen/Components/WorldControllerComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.Worldgen.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for controlling overall world loading, containing an index of all chunks in the map. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldControllerComponent : Component -{ - /// - /// The prototype to use for chunks on this world map. - /// - [DataField("chunkProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ChunkProto = "WorldChunk"; - - /// - /// An index of chunks owned by the controller. - /// - [DataField("chunks")] public Dictionary Chunks = new(); -} - diff --git a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs deleted file mode 100644 index e6bb7781e96..00000000000 --- a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.Worldgen.Systems; - -namespace Content.Server.Worldgen.Components; - -/// -/// This is used for allowing some objects to load the game world. -/// -[RegisterComponent] -[Access(typeof(WorldControllerSystem))] -public sealed partial class WorldLoaderComponent : Component -{ - /// - /// The radius in which the loader loads the world. - /// - [ViewVariables(VVAccess.ReadWrite)] [DataField("radius")] - public int Radius = 128; -} - diff --git a/Content.Server/Worldgen/GridPointsNearEnumerator.cs b/Content.Server/Worldgen/GridPointsNearEnumerator.cs deleted file mode 100644 index 24b710626bd..00000000000 --- a/Content.Server/Worldgen/GridPointsNearEnumerator.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; - -namespace Content.Server.Worldgen; - -/// -/// A struct enumerator of points on a grid within the given radius. -/// -public struct GridPointsNearEnumerator -{ - private readonly int _radius; - private readonly Vector2i _center; - private int _x; - private int _y; - - /// - /// Initializes a new enumerator with the given center and radius. - /// - public GridPointsNearEnumerator(Vector2i center, int radius) - { - _radius = radius; - _center = center; - _x = -_radius; - _y = -_radius; - } - - /// - /// Gets the next point in the enumeration. - /// - /// The computed point, if any - /// Success - [Pure] - public bool MoveNext([NotNullWhen(true)] out Vector2i? chunk) - { - while (!(_x * _x + _y * _y <= _radius * _radius)) - { - if (_y > _radius) - { - chunk = null; - return false; - } - - if (_x > _radius) - { - _x = -_radius; - _y++; - } - else - { - _x++; - } - } - - chunk = _center + new Vector2i(_x, _y); - _x++; - return true; - } -} - diff --git a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs b/Content.Server/Worldgen/Prototypes/BiomePrototype.cs deleted file mode 100644 index 1c3633fb5ff..00000000000 --- a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Numerics; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a prototype for biome selection, allowing the component list of a chunk to be amended based on the output -/// of noise channels at that location. -/// -[Prototype("spaceBiome")] -public sealed partial class BiomePrototype : IPrototype, IInheritingPrototype -{ - /// - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; private set; } - - /// - [NeverPushInheritance] - [AbstractDataField] - public bool Abstract { get; private set; } - - /// - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// The valid ranges of noise values under which this biome can be picked. - /// - [DataField("noiseRanges", required: true)] - public Dictionary> NoiseRanges = default!; - - /// - /// Higher priority biomes get picked before lower priority ones. - /// - [DataField("priority", required: true)] - public int Priority { get; private set; } - - /// - /// The components that get added to the target map. - /// - [DataField("chunkComponents")] - [AlwaysPushInheritance] - public ComponentRegistry ChunkComponents = new(); - - //TODO: Get someone to make this a method on componentregistry that does it Correctly. - /// - /// Applies the worldgen config to the given target (presumably a map.) - /// - public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager) - { - // Add all components required by the prototype. Engine update for this whenst. - foreach (var data in ChunkComponents.Values) - { - var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true); - entityManager.AddComponent(target, comp); - } - } -} - diff --git a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs b/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs deleted file mode 100644 index 98ba2d5b315..00000000000 --- a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Numerics; -using Robust.Shared.Noise; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a config for noise channels, used by worldgen. -/// -[Virtual] -public class NoiseChannelConfig -{ - /// - /// The noise type used by the noise generator. - /// - [DataField("noiseType")] - public FastNoiseLite.NoiseType NoiseType { get; private set; } = FastNoiseLite.NoiseType.Cellular; - - /// - /// The fractal type used by the noise generator. - /// - [DataField("fractalType")] - public FastNoiseLite.FractalType FractalType { get; private set; } = FastNoiseLite.FractalType.FBm; - - /// - /// Multiplied by pi in code when used. - /// - [DataField("fractalLacunarityByPi")] - public float FractalLacunarityByPi { get; private set; } = 2.0f / 3.0f; - - /// - /// Ranges of values that get clamped down to the "clipped" value. - /// - [DataField("clippingRanges")] - public List ClippingRanges { get; private set; } = new(); - - /// - /// The value clipped chunks are set to. - /// - [DataField("clippedValue")] - public float ClippedValue { get; private set; } - - /// - /// A value the output is multiplied by. - /// - [DataField("outputMultiplier")] - public float OutputMultiplier { get; private set; } = 1.0f; - - /// - /// A value the input is multiplied by. - /// - [DataField("inputMultiplier")] - public float InputMultiplier { get; private set; } = 1.0f; - - /// - /// Remaps the output of the noise function from the range (-1, 1) to (0, 1). This is done before all other output - /// transformations. - /// - [DataField("remapTo0Through1")] - public bool RemapTo0Through1 { get; private set; } - - /// - /// For when the transformation you need is too complex to describe in YAML. - /// - [DataField("noisePostProcess")] - public NoisePostProcess? NoisePostProcess { get; private set; } - - /// - /// For when you need a complex transformation of the input coordinates. - /// - [DataField("noiseCoordinateProcess")] - public NoiseCoordinateProcess? NoiseCoordinateProcess { get; private set; } - - /// - /// The "center" of the range of values. Or the minimum if mapped 0 through 1. - /// - [DataField("minimum")] - public float Minimum { get; private set; } -} - -[Prototype] -public sealed partial class NoiseChannelPrototype : NoiseChannelConfig, IPrototype, IInheritingPrototype -{ - /// - [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; private set; } - - /// - [NeverPushInheritance] - [AbstractDataField] - public bool Abstract { get; private set; } - - /// - [IdDataField] - public string ID { get; private set; } = default!; -} - -/// -/// A wrapper around FastNoise's noise generation, using noise channel configs. -/// -public struct NoiseGenerator -{ - private readonly NoiseChannelConfig _config; - private readonly FastNoiseLite _noise; - - /// - /// Produces a new noise generator from the given channel config and rng seed. - /// - public NoiseGenerator(NoiseChannelConfig config, int seed) - { - _config = config; - _noise = new FastNoiseLite(); - _noise.SetSeed(seed); - _noise.SetNoiseType(_config.NoiseType); - _noise.SetFractalType(_config.FractalType); - _noise.SetFractalLacunarity(_config.FractalLacunarityByPi * MathF.PI); - } - - /// - /// Evaluates the noise generator at the provided coordinates. - /// - /// Coordinates to use as input - /// Computed noise value - public float Evaluate(Vector2 coords) - { - var finCoords = coords * _config.InputMultiplier; - - if (_config.NoiseCoordinateProcess is not null) - finCoords = _config.NoiseCoordinateProcess.Process(finCoords); - - var value = _noise.GetNoise(finCoords.X, finCoords.Y); - - if (_config.RemapTo0Through1) - value = (value + 1.0f) / 2.0f; - - foreach (var range in _config.ClippingRanges) - { - if (range.X < value && value < range.Y) - { - value = _config.ClippedValue; - break; - } - } - - if (_config.NoisePostProcess is not null) - value = _config.NoisePostProcess.Process(value); - value *= _config.OutputMultiplier; - return value + _config.Minimum; - } -} - -/// -/// A processing class that adjusts the input coordinate space to a noise channel. -/// -[ImplicitDataDefinitionForInheritors] -public abstract partial class NoiseCoordinateProcess -{ - public abstract Vector2 Process(Vector2 inp); -} - -/// -/// A processing class that adjusts the final result of the noise channel. -/// -[ImplicitDataDefinitionForInheritors] -public abstract partial class NoisePostProcess -{ - public abstract float Process(float inp); -} - diff --git a/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs b/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs deleted file mode 100644 index c9107fa2bd8..00000000000 --- a/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; - -namespace Content.Server.Worldgen.Prototypes; - -/// -/// This is a prototype for controlling overall world generation. -/// The components included are applied to the map that world generation is configured on. -/// -[Prototype] -public sealed partial class WorldgenConfigPrototype : IPrototype -{ - /// - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// The components that get added to the target map. - /// - [DataField("components", required: true)] - public ComponentRegistry Components { get; private set; } = default!; - - //TODO: Get someone to make this a method on componentregistry that does it Correctly. - /// - /// Applies the worldgen config to the given target (presumably a map.) - /// - public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager) - { - // Add all components required by the prototype. Engine update for this whenst. - foreach (var data in Components.Values) - { - var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true); - entityManager.AddComponent(target, comp); - } - } -} - diff --git a/Content.Server/Worldgen/Systems/BaseWorldSystem.cs b/Content.Server/Worldgen/Systems/BaseWorldSystem.cs deleted file mode 100644 index 7a9e74375cb..00000000000 --- a/Content.Server/Worldgen/Systems/BaseWorldSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Components; -using JetBrains.Annotations; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This provides some additional functions for world generation systems. -/// Exists primarily for convenience and to avoid code duplication. -/// -[PublicAPI] -public abstract class BaseWorldSystem : EntitySystem -{ - [Dependency] private readonly WorldControllerSystem _worldController = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - - /// - /// Gets a chunk's coordinates in chunk space as an integer value. - /// - /// - /// - /// Chunk space coordinates - [Pure] - public Vector2i GetChunkCoords(EntityUid ent, TransformComponent? xform = null) - { - if (!Resolve(ent, ref xform)) - throw new Exception("Failed to resolve transform, somehow."); - - return WorldGen.WorldToChunkCoords(_transformSystem.GetWorldPosition(xform)).Floored(); - } - - /// - /// Gets a chunk's coordinates in chunk space as a floating point value. - /// - /// - /// - /// Chunk space coordinates - [Pure] - public Vector2 GetFloatingChunkCoords(EntityUid ent, TransformComponent? xform = null) - { - if (!Resolve(ent, ref xform)) - throw new Exception("Failed to resolve transform, somehow."); - - return WorldGen.WorldToChunkCoords(_transformSystem.GetWorldPosition(xform)); - } - - /// - /// Attempts to get a chunk, creating it if it doesn't exist. - /// - /// Chunk coordinates to get the chunk entity for. - /// Map the chunk is in. - /// The controller this chunk belongs to. - /// A chunk, if available. - [Pure] - public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null) - { - return _worldController.GetOrCreateChunk(chunk, map, controller); - } -} - diff --git a/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs b/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs deleted file mode 100644 index 1827f6deeda..00000000000 --- a/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; - -namespace Content.Server.Worldgen.Systems.Biomes; - -/// -/// This handles biome selection, evaluating which biome to apply to a chunk based on noise channels. -/// -public sealed class BiomeSelectionSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _noiseIdx = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly ISerializationManager _ser = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnBiomeSelectionStartup); - SubscribeLocalEvent(OnWorldChunkAdded); - } - - private void OnWorldChunkAdded(EntityUid uid, BiomeSelectionComponent component, ref WorldChunkAddedEvent args) - { - var coords = args.Coords; - foreach (var biomeId in component.Biomes) - { - var biome = _proto.Index(biomeId); - if (!CheckBiomeValidity(args.Chunk, biome, coords)) - continue; - - biome.Apply(args.Chunk, _ser, EntityManager); - return; - } - - Log.Error($"Biome selection ran out of biomes to select? See biomes list: {component.Biomes}"); - } - - private void OnBiomeSelectionStartup(EntityUid uid, BiomeSelectionComponent component, ComponentStartup args) - { - // surely this can't be THAAAAAAAAAAAAAAAT bad right???? - var sorted = component.Biomes - .Select(x => (Id: x, _proto.Index(x).Priority)) - .OrderByDescending(x => x.Priority) - .Select(x => x.Id) - .ToList(); - - component.Biomes = sorted; // my hopes and dreams rely on this being pre-sorted by priority. - } - - private bool CheckBiomeValidity(EntityUid chunk, BiomePrototype biome, Vector2i coords) - { - foreach (var (noise, ranges) in biome.NoiseRanges) - { - var value = _noiseIdx.Evaluate(chunk, noise, coords); - var anyValid = false; - foreach (var range in ranges) - { - if (range.X < value && value < range.Y) - { - anyValid = true; - break; - } - } - - if (!anyValid) - return false; - } - - return true; - } -} - diff --git a/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs b/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs deleted file mode 100644 index 1207d6f157e..00000000000 --- a/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Server.Worldgen.Components.Carvers; -using Content.Server.Worldgen.Systems.Debris; - -namespace Content.Server.Worldgen.Systems.Carvers; - -/// -/// This handles carving out holes in world generation according to a noise channel. -/// -public sealed class NoiseRangeCarverSystem : EntitySystem -{ - [Dependency] private readonly NoiseIndexSystem _index = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnPrePlaceDebris); - } - - private void OnPrePlaceDebris(EntityUid uid, NoiseRangeCarverComponent component, - ref PrePlaceDebrisFeatureEvent args) - { - var coords = WorldGen.WorldToChunkCoords(_transform.ToMapCoordinates(args.Coords).Position); - var val = _index.Evaluate(uid, component.NoiseChannel, coords); - - foreach (var (low, high) in component.Ranges) - { - if (low > val || high < val) - continue; - - args.Handled = true; - return; - } - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs b/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs deleted file mode 100644 index ba0a3a7132e..00000000000 --- a/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components.Debris; -using Content.Shared.Maps; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles building the floor plans for "blobby" debris. -/// -public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ITileDefinitionManager _tileDefinition = default!; - [Dependency] private readonly TileSystem _tiles = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnBlobFloorPlanBuilderStartup); - } - - private void OnBlobFloorPlanBuilderStartup(EntityUid uid, BlobFloorPlanBuilderComponent component, - ComponentStartup args) - { - PlaceFloorplanTiles(uid, component, Comp(uid)); - } - - private void PlaceFloorplanTiles(EntityUid gridUid, BlobFloorPlanBuilderComponent comp, MapGridComponent grid) - { - // NO MORE THAN TWO ALLOCATIONS THANK YOU VERY MUCH. - // TODO: Just put these on a field instead then? - // Also the end of the method has a big LINQ which is gonna blow this out the water. - var spawnPoints = new HashSet(comp.FloorPlacements * 6); - var taken = new Dictionary(comp.FloorPlacements * 5); - - void PlaceTile(Vector2i point) - { - // Assume we already know that the spawn point is safe. - spawnPoints.Remove(point); - var north = point.Offset(Direction.North); - var south = point.Offset(Direction.South); - var east = point.Offset(Direction.East); - var west = point.Offset(Direction.West); - var radsq = Math.Pow(comp.Radius, - 2); // I'd put this outside but i'm not 100% certain caching it between calls is a gain. - - // The math done is essentially a fancy way of comparing the distance from 0,0 to the radius, - // and skipping the sqrt normally needed for dist. - if (!taken.ContainsKey(north) && Math.Pow(north.X, 2) + Math.Pow(north.Y, 2) <= radsq) - spawnPoints.Add(north); - if (!taken.ContainsKey(south) && Math.Pow(south.X, 2) + Math.Pow(south.Y, 2) <= radsq) - spawnPoints.Add(south); - if (!taken.ContainsKey(east) && Math.Pow(east.X, 2) + Math.Pow(east.Y, 2) <= radsq) - spawnPoints.Add(east); - if (!taken.ContainsKey(west) && Math.Pow(west.X, 2) + Math.Pow(west.Y, 2) <= radsq) - spawnPoints.Add(west); - - var tileDef = _tileDefinition[_random.Pick(comp.FloorTileset)]; - taken.Add(point, new Tile(tileDef.TileId, 0, _tiles.PickVariant((ContentTileDefinition) tileDef))); - } - - PlaceTile(Vector2i.Zero); - - for (var i = 0; i < comp.FloorPlacements; i++) - { - var point = _random.Pick(spawnPoints); - PlaceTile(point); - - if (comp.BlobDrawProb > 0.0f) - { - if (!taken.ContainsKey(point.Offset(Direction.North)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.North)); - if (!taken.ContainsKey(point.Offset(Direction.South)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.South)); - if (!taken.ContainsKey(point.Offset(Direction.East)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.East)); - if (!taken.ContainsKey(point.Offset(Direction.West)) && _random.Prob(comp.BlobDrawProb)) - PlaceTile(point.Offset(Direction.West)); - } - } - - _map.SetTiles(gridUid, grid, taken.Select(x => (x.Key, x.Value)).ToList()); - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs b/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs deleted file mode 100644 index b609f7e1f7e..00000000000 --- a/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Components.Debris; -using Content.Server.Worldgen.Tools; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles placing debris within the world evenly with rng, primarily for structures like asteroid fields. -/// -public sealed class DebrisFeaturePlacerSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _noiseIndex = default!; - [Dependency] private readonly PoissonDiskSampler _sampler = default!; - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - private ISawmill _sawmill = default!; - - private List> _mapGrids = new(); - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world.debris.feature_placer"); - SubscribeLocalEvent(OnChunkLoaded); - SubscribeLocalEvent(OnChunkUnloaded); - SubscribeLocalEvent(OnDebrisShutdown); - SubscribeLocalEvent(OnDebrisMove); - SubscribeLocalEvent( - OnTryGetPlacableDebrisEvent); - } - - /// - /// Handles debris moving, and making sure it stays parented to a chunk for loading purposes. - /// - private void OnDebrisMove(EntityUid uid, OwnedDebrisComponent component, ref MoveEvent args) - { - if (!HasComp(component.OwningController)) - return; // Redundant logic, prolly needs it's own handler for your custom system. - - var placer = Comp(component.OwningController); - var xform = args.Component; - var ownerXform = Transform(component.OwningController); - if (xform.MapUid is null || ownerXform.MapUid is null) - return; // not our problem - - if (xform.MapUid != ownerXform.MapUid) - { - _sawmill.Error($"Somehow debris {uid} left it's expected map! Unparenting it to avoid issues."); - RemCompDeferred(uid); - placer.OwnedDebris.Remove(component.LastKey); - return; - } - - placer.OwnedDebris.Remove(component.LastKey); - var newChunk = GetOrCreateChunk(GetChunkCoords(uid), xform.MapUid!.Value); - if (newChunk is null || !TryComp(newChunk, out var newPlacer)) - { - // Whelp. - RemCompDeferred(uid); - return; - } - - newPlacer.OwnedDebris[_xformSys.GetWorldPosition(xform)] = uid; // Change our owner. - component.OwningController = newChunk.Value; - } - - /// - /// Handles debris shutdown/detach. - /// - private void OnDebrisShutdown(EntityUid uid, OwnedDebrisComponent component, ComponentShutdown args) - { - if (!TryComp(component.OwningController, out var placer)) - return; - - placer.OwnedDebris[component.LastKey] = null; - if (Terminating(uid)) - placer.OwnedDebris.Remove(component.LastKey); - } - - /// - /// Queues all debris owned by the placer for garbage collection. - /// - private void OnChunkUnloaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component, - ref WorldChunkUnloadedEvent args) - { - component.DoSpawns = true; - } - - /// - /// Handles providing a debris type to place for SimpleDebrisSelectorComponent. - /// This randomly picks a debris type from the EntitySpawnCollectionCache. - /// - private void OnTryGetPlacableDebrisEvent(EntityUid uid, SimpleDebrisSelectorComponent component, - ref TryGetPlaceableDebrisFeatureEvent args) - { - if (args.DebrisProto is not null) - return; - - var l = new List(1); - component.CachedDebrisTable.GetSpawns(_random, ref l); - - switch (l.Count) - { - case 0: - return; - case > 1: - _sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}"); - break; - } - - args.DebrisProto = l[0]; - } - - /// - /// Handles loading in debris. This does the following: - /// - Checks if the debris is currently supposed to do spawns, if it isn't, aborts immediately. - /// - Evaluates the density value to be used for placement, if it's zero, aborts. - /// - Generates the points to generate debris at, if and only if they've not been selected already by a prior load. - /// - Does the following in a loop over all generated points: - /// - Raises an event to check if something else wants to intercept debris placement, if the event is handled, - /// continues to the next point without generating anything. - /// - Raises an event to get the debris type that should be used for generation. - /// - Spawns the given debris at the point, adding it to the placer's index. - /// - private void OnChunkLoaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component, - ref WorldChunkLoadedEvent args) - { - if (component.DoSpawns == false) - return; - - component.DoSpawns = false; // Don't repeat yourself if this crashes. - - if (!TryComp(args.Chunk, out var chunk)) - return; - - var chunkMap = chunk.Map; - - if (!TryComp(chunkMap, out var map)) - return; - - var densityChannel = component.DensityNoiseChannel; - var density = _noiseIndex.Evaluate(uid, densityChannel, chunk.Coordinates + new Vector2(0.5f, 0.5f)); - if (density == 0) - return; - - List? points = null; - - // If we've been loaded before, reuse the same coordinates. - if (component.OwnedDebris.Count != 0) - { - //TODO: Remove LINQ. - points = component.OwnedDebris - .Where(x => !Deleted(x.Value)) - .Select(static x => x.Key) - .ToList(); - } - - points ??= GeneratePointsInChunk(args.Chunk, density, chunk.Coordinates, chunkMap); - - var mapId = map.MapId; - - var safetyBounds = Box2.UnitCentered.Enlarged(component.SafetyZoneRadius); - var failures = 0; // Avoid severe log spam. - foreach (var point in points) - { - if (component.OwnedDebris.TryGetValue(point, out var existing)) - { - DebugTools.Assert(Exists(existing)); - continue; - } - - var pointDensity = _noiseIndex.Evaluate(uid, densityChannel, WorldGen.WorldToChunkCoords(point)); - if (pointDensity == 0 && component.DensityClip || _random.Prob(component.RandomCancellationChance)) - continue; - - if (HasCollisions(mapId, safetyBounds.Translated(point))) - continue; - - var coords = new EntityCoordinates(chunkMap, point); - - var preEv = new PrePlaceDebrisFeatureEvent(coords, args.Chunk); - RaiseLocalEvent(uid, ref preEv); - if (uid != args.Chunk) - RaiseLocalEvent(args.Chunk, ref preEv); - - if (preEv.Handled) - continue; - - var debrisFeatureEv = new TryGetPlaceableDebrisFeatureEvent(coords, args.Chunk); - RaiseLocalEvent(uid, ref debrisFeatureEv); - - if (debrisFeatureEv.DebrisProto == null) - { - // Try on the chunk...? - if (uid != args.Chunk) - RaiseLocalEvent(args.Chunk, ref debrisFeatureEv); - - if (debrisFeatureEv.DebrisProto == null) - { - // Nope. - failures++; - continue; - } - } - - var ent = Spawn(debrisFeatureEv.DebrisProto, coords); - component.OwnedDebris.Add(point, ent); - - var owned = EnsureComp(ent); - owned.OwningController = uid; - owned.LastKey = point; - } - - if (failures > 0) - _sawmill.Error($"Failed to place {failures} debris at chunk {args.Chunk}"); - } - - /// - /// Checks to see if the potential spawn point is clear - /// - /// - /// - /// - private bool HasCollisions(MapId mapId, Box2 point) - { - _mapGrids.Clear(); - _mapManager.FindGridsIntersecting(mapId, point, ref _mapGrids); - return _mapGrids.Count > 0; - } - - /// - /// Generates the points to put into a chunk using a poisson disk sampler. - /// - private List GeneratePointsInChunk(EntityUid chunk, float density, Vector2 coords, EntityUid map) - { - var offs = (int) ((WorldGen.ChunkSize - WorldGen.ChunkSize / 8.0f) / 2.0f); - var topLeft = new Vector2(-offs, -offs); - var lowerRight = new Vector2(offs, offs); - var enumerator = _sampler.SampleRectangle(topLeft, lowerRight, density); - var debrisPoints = new List(); - - var realCenter = WorldGen.ChunkToWorldCoordsCentered(coords.Floored()); - - while (enumerator.MoveNext(out var debrisPoint)) - { - debrisPoints.Add(realCenter + debrisPoint.Value); - } - - return debrisPoints; - } -} - -/// -/// Fired directed on the debris feature placer controller and the chunk, ahead of placing a debris piece. -/// -[ByRefEvent] -[PublicAPI] -public record struct PrePlaceDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, bool Handled = false); - -/// -/// Fired directed on the debris feature placer controller and the chunk, to select which debris piece to place. -/// -[ByRefEvent] -[PublicAPI] -public record struct TryGetPlaceableDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, - string? DebrisProto = null); - diff --git a/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs b/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs deleted file mode 100644 index a02ff81362c..00000000000 --- a/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Server.Worldgen.Components.Debris; -using Robust.Server.GameObjects; -using Robust.Shared.Physics; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles selecting debris with probability decided by a noise channel. -/// -public sealed class NoiseDrivenDebrisSelectorSystem : BaseWorldSystem -{ - [Dependency] private readonly NoiseIndexSystem _index = default!; - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - private ISawmill _sawmill = default!; - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world.debris.noise_debris_selector"); - // Event is forcibly ordered to always be handled after the simple selector. - SubscribeLocalEvent(OnSelectDebrisKind, - after: new[] {typeof(DebrisFeaturePlacerSystem)}); - } - - private void OnSelectDebrisKind(EntityUid uid, NoiseDrivenDebrisSelectorComponent component, - ref TryGetPlaceableDebrisFeatureEvent args) - { - var coords = WorldGen.WorldToChunkCoords(_xformSys.ToMapCoordinates(args.Coords).Position); - var prob = _index.Evaluate(uid, component.NoiseChannel, coords); - - if (prob is < 0 or > 1) - { - _sawmill.Error( - $"Sampled a probability of {prob}, which is outside the [0, 1] range, at {coords} aka {args.Coords}."); - return; - } - - if (!_random.Prob(prob)) - return; - - var l = new List(1); - component.CachedDebrisTable.GetSpawns(_random, ref l); - - switch (l.Count) - { - case 0: - return; - case > 1: - _sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}"); - break; - } - - args.DebrisProto = l[0]; - } -} - diff --git a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs deleted file mode 100644 index dcb7b7fc8fc..00000000000 --- a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Content.Server.Worldgen.Components.Debris; -using Content.Shared.Maps; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems.Debris; - -/// -/// This handles populating simple structures, simply using a loot table for each tile. -/// -public sealed class SimpleFloorPlanPopulatorSystem : BaseWorldSystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly TurfSystem _turf = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnFloorPlanBuilt); - } - - private void OnFloorPlanBuilt(EntityUid uid, SimpleFloorPlanPopulatorComponent component, - LocalStructureLoadedEvent args) - { - var placeables = new List(4); - var grid = Comp(uid); - var enumerator = _map.GetAllTilesEnumerator(uid, grid); - while (enumerator.MoveNext(out var tile)) - { - var coords = _map.GridTileToLocal(uid, grid, tile.Value.GridIndices); - var selector = _turf.GetContentTileDefinition(tile.Value).ID; - if (!component.Caches.TryGetValue(selector, out var cache)) - continue; - - placeables.Clear(); - cache.GetSpawns(_random, ref placeables); - - foreach (var proto in placeables) - { - if (proto is null) - continue; - - Spawn(proto, coords); - } - } - } -} - diff --git a/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs b/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs deleted file mode 100644 index fb02e8aa0bd..00000000000 --- a/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Content.Server.Worldgen.Components; -using Robust.Server.GameObjects; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles loading in objects based on distance from player, using some metadata on chunks. -/// -public sealed class LocalityLoaderSystem : BaseWorldSystem -{ - [Dependency] private readonly TransformSystem _xformSys = default!; - - /// - public override void Update(float frameTime) - { - var e = EntityQueryEnumerator(); - var loadedQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); - var controllerQuery = GetEntityQuery(); - - while (e.MoveNext(out var uid, out var loadable, out var xform)) - { - if (!controllerQuery.TryGetComponent(xform.MapUid, out var controller)) - { - RaiseLocalEvent(uid, new LocalStructureLoadedEvent()); - RemCompDeferred(uid); - continue; - } - - var coords = GetChunkCoords(uid, xform); - var done = false; - for (var i = -1; i < 2 && !done; i++) - { - for (var j = -1; j < 2 && !done; j++) - { - var chunk = GetOrCreateChunk(coords + (i, j), xform.MapUid!.Value, controller); - if (!loadedQuery.TryGetComponent(chunk, out var loaded) || loaded.Loaders is null) - continue; - - foreach (var loader in loaded.Loaders) - { - if (!xformQuery.TryGetComponent(loader, out var loaderXform)) - continue; - - if ((_xformSys.GetWorldPosition(loaderXform) - _xformSys.GetWorldPosition(xform)).Length() > loadable.LoadingDistance) - continue; - - RaiseLocalEvent(uid, new LocalStructureLoadedEvent()); - RemCompDeferred(uid); - done = true; - break; - } - } - } - } - } -} - -/// -/// A directed fired on a loadable entity when a local loader enters it's vicinity. -/// -public record struct LocalStructureLoadedEvent; - diff --git a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs b/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs deleted file mode 100644 index 5a7e02c803a..00000000000 --- a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Numerics; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles the noise index. -/// -public sealed class NoiseIndexSystem : EntitySystem -{ - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - /// - /// Gets a particular noise channel from the index on the given entity. - /// - /// The holder of the index - /// The channel prototype ID - /// An initialized noise generator - public NoiseGenerator Get(EntityUid holder, string protoId) - { - var idx = EnsureComp(holder); - if (idx.Generators.TryGetValue(protoId, out var generator)) - return generator; - var proto = _prototype.Index(protoId); - var gen = new NoiseGenerator(proto, _random.Next()); - idx.Generators[protoId] = gen; - return gen; - } - - /// - /// Attempts to evaluate the given noise channel using the generator on the given entity. - /// - /// The holder of the index - /// The channel prototype ID - /// The coordinates to evaluate at - /// The result of evaluation - public float Evaluate(EntityUid holder, string protoId, Vector2 coords) - { - var gen = Get(holder, protoId); - return gen.Evaluate(coords); - } -} - diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs deleted file mode 100644 index 19c777e1ad6..00000000000 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Components; -using Content.Shared.Ghost; -using Content.Shared.Mind.Components; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Timing; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles putting together chunk entities and notifying them about important changes. -/// -public sealed class WorldControllerSystem : EntitySystem -{ - [Dependency] private readonly TransformSystem _xformSys = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - - private const int PlayerLoadRadius = 2; - - private ISawmill _sawmill = default!; - - /// - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("world"); - SubscribeLocalEvent(OnChunkLoadedCore); - SubscribeLocalEvent(OnChunkUnloadedCore); - SubscribeLocalEvent(OnChunkShutdown); - } - - /// - /// Handles deleting chunks properly. - /// - private void OnChunkShutdown(EntityUid uid, WorldChunkComponent component, ComponentShutdown args) - { - if (!TryComp(component.Map, out var controller)) - return; - - if (HasComp(uid)) - { - var ev = new WorldChunkUnloadedEvent(uid, component.Coordinates); - RaiseLocalEvent(component.Map, ref ev); - RaiseLocalEvent(uid, ref ev, broadcast: true); - } - - controller.Chunks.Remove(component.Coordinates); - } - - /// - /// Handles the inner logic of loading a chunk, i.e. events. - /// - private void OnChunkLoadedCore(EntityUid uid, LoadedChunkComponent component, ComponentStartup args) - { - if (!TryComp(uid, out var chunk)) - return; - - var ev = new WorldChunkLoadedEvent(uid, chunk.Coordinates); - RaiseLocalEvent(chunk.Map, ref ev); - RaiseLocalEvent(uid, ref ev, broadcast: true); - //_sawmill.Debug($"Loaded chunk {ToPrettyString(uid)} at {chunk.Coordinates}"); - } - - /// - /// Handles the inner logic of unloading a chunk, i.e. events. - /// - private void OnChunkUnloadedCore(EntityUid uid, LoadedChunkComponent component, ComponentShutdown args) - { - if (!TryComp(uid, out var chunk)) - return; - - if (Terminating(uid)) - return; // SAFETY: This is in case a loaded chunk gets deleted, to avoid double unload. - - var ev = new WorldChunkUnloadedEvent(uid, chunk.Coordinates); - RaiseLocalEvent(chunk.Map, ref ev); - RaiseLocalEvent(uid, ref ev); - //_sawmill.Debug($"Unloaded chunk {ToPrettyString(uid)} at {coords}"); - } - - /// - public override void Update(float frameTime) - { - //there was a to-do here about every frame alloc but it turns out it's a nothing burger here. - var chunksToLoad = new Dictionary>>(); - - var controllerEnum = EntityQueryEnumerator(); - while (controllerEnum.MoveNext(out var uid, out _)) - { - chunksToLoad[uid] = new Dictionary>(); - } - - if (chunksToLoad.Count == 0) - return; // Just bail early. - - var loaderEnum = EntityQueryEnumerator(); - - while (loaderEnum.MoveNext(out var uid, out var worldLoader, out var xform)) - { - var mapOrNull = xform.MapUid; - if (mapOrNull is null) - continue; - var map = mapOrNull.Value; - if (!chunksToLoad.ContainsKey(map)) - continue; - - var wc = _xformSys.GetWorldPosition(xform); - var coords = WorldGen.WorldToChunkCoords(wc); - var chunks = new GridPointsNearEnumerator(coords.Floored(), - (int) Math.Ceiling(worldLoader.Radius / (float) WorldGen.ChunkSize) + 1); - - var set = chunksToLoad[map]; - - while (chunks.MoveNext(out var chunk)) - { - if (!set.TryGetValue(chunk.Value, out _)) - set[chunk.Value] = new List(4); - set[chunk.Value].Add(uid); - } - } - - var mindEnum = EntityQueryEnumerator(); - var ghostQuery = GetEntityQuery(); - - // Mindful entities get special privilege as they're always a player and we don't want the illusion being broken around them. - while (mindEnum.MoveNext(out var uid, out var mind, out var xform)) - { - if (!mind.HasMind) - continue; - if (ghostQuery.HasComponent(uid)) - continue; - var mapOrNull = xform.MapUid; - if (mapOrNull is null) - continue; - var map = mapOrNull.Value; - if (!chunksToLoad.ContainsKey(map)) - continue; - - var wc = _xformSys.GetWorldPosition(xform); - var coords = WorldGen.WorldToChunkCoords(wc); - var chunks = new GridPointsNearEnumerator(coords.Floored(), PlayerLoadRadius); - - var set = chunksToLoad[map]; - - while (chunks.MoveNext(out var chunk)) - { - if (!set.TryGetValue(chunk.Value, out _)) - set[chunk.Value] = new List(4); - set[chunk.Value].Add(uid); - } - } - - var loadedEnum = EntityQueryEnumerator(); - var chunksUnloaded = 0; - - // Make sure these chunks get unloaded at the end of the tick. - while (loadedEnum.MoveNext(out var uid, out var _, out var chunk)) - { - var coords = chunk.Coordinates; - - if (!chunksToLoad[chunk.Map].ContainsKey(coords)) - { - RemCompDeferred(uid); - chunksUnloaded++; - } - } - - if (chunksUnloaded > 0) - _sawmill.Debug($"Queued {chunksUnloaded} chunks for unload."); - - if (chunksToLoad.All(x => x.Value.Count == 0)) - return; - - var startTime = _gameTiming.RealTime; - var count = 0; - var loadedQuery = GetEntityQuery(); - var controllerQuery = GetEntityQuery(); - foreach (var (map, chunks) in chunksToLoad) - { - var controller = controllerQuery.GetComponent(map); - foreach (var (chunk, loaders) in chunks) - { - var ent = GetOrCreateChunk(chunk, map, controller); // Ensure everything loads. - LoadedChunkComponent? c = null; - if (ent is not null && !loadedQuery.TryGetComponent(ent.Value, out c)) - { - c = AddComp(ent.Value); - count += 1; - } - - if (c is not null) - c.Loaders = loaders; - } - } - - if (count > 0) - { - var timeSpan = _gameTiming.RealTime - startTime; - _sawmill.Debug($"Loaded {count} chunks in {timeSpan.TotalMilliseconds:N2}ms."); - } - } - - /// - /// Attempts to get a chunk, creating it if it doesn't exist. - /// - /// Chunk coordinates to get the chunk entity for. - /// Map the chunk is in. - /// The controller this chunk belongs to. - /// A chunk, if available. - [Pure] - public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null) - { - if (!Resolve(map, ref controller)) - throw new Exception($"Tried to use {ToPrettyString(map)} as a world map, without actually being one."); - - if (controller.Chunks.TryGetValue(chunk, out var ent)) - return ent; - return CreateChunkEntity(chunk, map, controller); - } - - /// - /// Constructs a new chunk entity, attaching it to the map. - /// - /// The coordinates the new chunk should be initialized for. - /// - /// - /// - private EntityUid CreateChunkEntity(Vector2i chunkCoords, EntityUid map, WorldControllerComponent controller) - { - var chunk = Spawn(controller.ChunkProto, MapCoordinates.Nullspace); - StartupChunkEntity(chunk, chunkCoords, map, controller); - _metaData.SetEntityName(chunk, $"Chunk {chunkCoords.X}/{chunkCoords.Y}"); - return chunk; - } - - private void StartupChunkEntity(EntityUid chunk, Vector2i coords, EntityUid map, - WorldControllerComponent controller) - { - if (!TryComp(chunk, out var chunkComponent)) - { - _sawmill.Error($"Chunk {ToPrettyString(chunk)} is missing WorldChunkComponent."); - return; - } - - ref var chunks = ref controller.Chunks; - - chunks[coords] = chunk; // Add this entity to chunk index. - chunkComponent.Coordinates = coords; - chunkComponent.Map = map; - var ev = new WorldChunkAddedEvent(chunk, coords); - RaiseLocalEvent(map, ref ev, broadcast: true); - } -} - -/// -/// A directed event fired when a chunk is initially set up in the world. The chunk is not loaded at this point. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkAddedEvent(EntityUid Chunk, Vector2i Coords); - -/// -/// A directed event fired when a chunk is loaded into the world, i.e. a player or other world loader has entered vicinity. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Coords); - -/// -/// A directed event fired when a chunk is unloaded from the world, i.e. no world loaders remain nearby. -/// -[ByRefEvent] -[PublicAPI] -public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords); diff --git a/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs b/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs deleted file mode 100644 index cc0ec627338..00000000000 --- a/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Content.Server.Administration; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Events; -using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; -using Content.Shared.Administration; -using Content.Shared.CCVar; -using Robust.Shared.Configuration; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Systems; - -/// -/// This handles configuring world generation during round start. -/// -public sealed class WorldgenConfigSystem : EntitySystem -{ - [Dependency] private readonly GameTicker _gameTicker = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IConsoleHost _conHost = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly ISerializationManager _ser = default!; - - private bool _enabled; - private string _worldgenConfig = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnLoadingMaps); - _conHost.RegisterCommand("applyworldgenconfig", Loc.GetString("cmd-applyworldgenconfig-description"), Loc.GetString("cmd-applyworldgenconfig-help"), ApplyWorldgenConfigCommand); - Subs.CVar(_cfg, CCVars.WorldgenEnabled, b => _enabled = b, true); - Subs.CVar(_cfg, CCVars.WorldgenConfig, s => _worldgenConfig = s, true); - } - - [AdminCommand(AdminFlags.Mapping)] - private void ApplyWorldgenConfigCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 2), ("currentAmount", args.Length))); - return; - } - - if (!int.TryParse(args[0], out var mapInt) || !_map.MapExists(new MapId(mapInt))) - { - shell.WriteError(Loc.GetString("shell-invalid-map-id")); - return; - } - - var map = _map.GetMapOrInvalid(new MapId(mapInt)); - - if (!_proto.TryIndex(args[1], out var proto)) - { - shell.WriteError(Loc.GetString("shell-argument-must-be-prototype", ("index", 2), ("prototypeName", "cmd-applyworldgenconfig-prototype"))); - return; - } - - proto.Apply(map, _ser, EntityManager); - shell.WriteLine(Loc.GetString("cmd-applyworldgenconfig-success")); - } - - /// - /// Applies the world config to the default map if enabled. - /// - private void OnLoadingMaps(RoundStartingEvent ev) - { - if (_enabled == false) - return; - - var target = _map.GetMapOrInvalid(_gameTicker.DefaultMap); - Log.Debug($"Trying to configure {_gameTicker.DefaultMap}, aka {ToPrettyString(target)} aka {target}"); - var cfg = _proto.Index(_worldgenConfig); - - cfg.Apply(target, _ser, EntityManager); // Apply the config to the map. - - DebugTools.Assert(HasComp(target)); - } -} - diff --git a/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs b/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs deleted file mode 100644 index 54805754270..00000000000 --- a/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Linq; -using Content.Shared.Storage; -using Robust.Shared.Random; - -namespace Content.Server.Worldgen.Tools; - -/// -/// A faster version of EntitySpawnCollection that requires caching to work. -/// -public sealed class EntitySpawnCollectionCache -{ - [ViewVariables] private readonly Dictionary _orGroups = new(); - - public EntitySpawnCollectionCache(IEnumerable entries) - { - // collect groups together, create singular items that pass probability - foreach (var entry in entries) - { - if (!_orGroups.TryGetValue(entry.GroupId ?? string.Empty, out var orGroup)) - { - orGroup = new OrGroup(); - _orGroups.Add(entry.GroupId ?? string.Empty, orGroup); - } - - orGroup.Entries.Add(entry); - orGroup.CumulativeProbability += entry.SpawnProbability; - } - } - - /// - /// Using a collection of entity spawn entries, picks a random list of entity prototypes to spawn from that collection. - /// - /// - /// This does not spawn the entities. The caller is responsible for doing so, since it may want to do something - /// special to those entities (offset them, insert them into storage, etc) - /// - /// Resolve param. - /// List that spawned entities are inserted into. - /// A list of entity prototypes that should be spawned. - /// This is primarily useful if you're calling it many times over, as it lets you reuse the list repeatedly. - public void GetSpawns(IRobustRandom random, ref List spawned) - { - // handle orgroup spawns - foreach (var spawnValue in _orGroups.Values) - { - //HACK: This doesn't seem to work without this if there's only a single orgroup entry. Not sure how to fix the original math properly, but it works in every other case. - if (spawnValue.Entries.Count == 1) - { - var entry = spawnValue.Entries.First(); - var amount = entry.Amount; - - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); - - for (var index = 0; index < amount; index++) - { - spawned.Add(entry.PrototypeId); - } - - continue; - } - - // For each group use the added cumulative probability to roll a double in that range - var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability; - // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item. - var cumulative = 0.0; - foreach (var entry in spawnValue.Entries) - { - cumulative += entry.SpawnProbability; - if (diceRoll > cumulative) - continue; - // Dice roll succeeded, add item and break loop - - var amount = entry.Amount; - - if (entry.MaxAmount > amount) - amount = random.Next(amount, entry.MaxAmount); - - for (var index = 0; index < amount; index++) - { - spawned.Add(entry.PrototypeId); - } - - break; - } - } - } - - private sealed class OrGroup - { - [ViewVariables] public List Entries { get; } = new(); - - [ViewVariables] public float CumulativeProbability { get; set; } - } -} - diff --git a/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs b/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs deleted file mode 100644 index cb7c5a14114..00000000000 --- a/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Worldgen.Tools; - -/// -/// An implementation of Poisson Disk Sampling, for evenly spreading points across a given area. -/// -public sealed class PoissonDiskSampler -{ - public const int DefaultPointsPerIteration = 30; - [Dependency] private readonly IRobustRandom _random = default!; - - /// - /// Samples for points within the given circle. - /// - /// Center of the sample - /// Radius of the sample - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator SampleCircle(Vector2 center, float radius, float minimumDistance, - int pointsPerIteration = DefaultPointsPerIteration) - { - return Sample(center - new Vector2(radius, radius), center + new Vector2(radius, radius), radius, - minimumDistance, pointsPerIteration); - } - - /// - /// Samples for points within the given rectangle. - /// - /// The top left of the rectangle - /// The bottom right of the rectangle - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator SampleRectangle(Vector2 topLeft, Vector2 lowerRight, float minimumDistance, - int pointsPerIteration = DefaultPointsPerIteration) - { - return Sample(topLeft, lowerRight, null, minimumDistance, pointsPerIteration); - } - - /// - /// Samples for points within the given rectangle, with an optional rejection distance. - /// - /// The top left of the rectangle - /// The bottom right of the rectangle - /// The distance at which points will be discarded, if any - /// Minimum distance between points. Must be above 0! - /// The number of points placed per iteration of the algorithm - /// An enumerator of points - public SampleEnumerator Sample(Vector2 topLeft, Vector2 lowerRight, float? rejectionDistance, - float minimumDistance, int pointsPerIteration) - { - // This still doesn't guard against dangerously low but non-zero distances, but this will do for now. - DebugTools.Assert(minimumDistance > 0, "Minimum distance must be above 0, or else an infinite number of points would be generated."); - - var settings = new SampleSettings - { - TopLeft = topLeft, LowerRight = lowerRight, - Dimensions = lowerRight - topLeft, - Center = (topLeft + lowerRight) / 2, - CellSize = minimumDistance / (float) Math.Sqrt(2), - MinimumDistance = minimumDistance, - RejectionSqDistance = rejectionDistance * rejectionDistance - }; - - settings.GridWidth = (int) (settings.Dimensions.X / settings.CellSize) + 1; - settings.GridHeight = (int) (settings.Dimensions.Y / settings.CellSize) + 1; - - var state = new State - { - Grid = new Vector2?[settings.GridWidth, settings.GridHeight], - ActivePoints = new List() - }; - - return new SampleEnumerator(this, state, settings, pointsPerIteration); - } - - private Vector2 AddFirstPoint(ref SampleSettings settings, ref State state) - { - while (true) - { - var d = _random.NextDouble(); - var xr = settings.TopLeft.X + settings.Dimensions.X * d; - - d = _random.NextDouble(); - var yr = settings.TopLeft.Y + settings.Dimensions.Y * d; - - var p = new Vector2((float) xr, (float) yr); - if (settings.RejectionSqDistance != null && - (settings.Center - p).LengthSquared() > settings.RejectionSqDistance) - continue; - - var index = Denormalize(p, settings.TopLeft, settings.CellSize); - - state.Grid[(int) index.X, (int) index.Y] = p; - - state.ActivePoints.Add(p); - return p; - } - } - - private Vector2? AddNextPoint(Vector2 point, ref SampleSettings settings, ref State state) - { - var q = GenerateRandomAround(point, settings.MinimumDistance); - - if (q.X >= settings.TopLeft.X && q.X < settings.LowerRight.X && - q.Y > settings.TopLeft.Y && q.Y < settings.LowerRight.Y && - (settings.RejectionSqDistance == null || - (settings.Center - q).LengthSquared() <= settings.RejectionSqDistance)) - { - var qIndex = Denormalize(q, settings.TopLeft, settings.CellSize); - var tooClose = false; - - for (var i = (int) Math.Max(0, qIndex.X - 2); - i < Math.Min(settings.GridWidth, qIndex.X + 3) && !tooClose; - i++) - for (var j = (int) Math.Max(0, qIndex.Y - 2); - j < Math.Min(settings.GridHeight, qIndex.Y + 3) && !tooClose; - j++) - { - if (state.Grid[i, j].HasValue && (state.Grid[i, j]!.Value - q).Length() < settings.MinimumDistance) - tooClose = true; - } - - if (!tooClose) - { - state.ActivePoints.Add(q); - state.Grid[(int) qIndex.X, (int) qIndex.Y] = q; - return q; - } - } - - return null; - } - - private Vector2 GenerateRandomAround(Vector2 center, float minimumDistance) - { - var d = _random.NextDouble(); - var radius = minimumDistance + minimumDistance * d; - - d = _random.NextDouble(); - var angle = Math.PI * 2 * d; - - var newX = radius * Math.Sin(angle); - var newY = radius * Math.Cos(angle); - - return new Vector2((float) (center.X + newX), (float) (center.Y + newY)); - } - - private static Vector2 Denormalize(Vector2 point, Vector2 origin, double cellSize) - { - return new Vector2((int) ((point.X - origin.X) / cellSize), (int) ((point.Y - origin.Y) / cellSize)); - } - - public struct SampleEnumerator - { - private PoissonDiskSampler _pds; - private State _state; - private SampleSettings _settings; - // These variables make up the state machine. - private bool _returnedFirstPoint; - private int _pointsPerIteration; - private int _iterationListIndex; - private bool _iterationFound; - private int _iterationPosition; - - // This has internal access because C# nested type access is being weird. - internal SampleEnumerator(PoissonDiskSampler pds, State state, SampleSettings settings, int ppi) - { - _pds = pds; - _state = state; - _settings = settings; - _pointsPerIteration = ppi; - } - - public bool MoveNext([NotNullWhen(true)] out Vector2? point) - { - // First point is chosen via a very particular method. - if (!_returnedFirstPoint) - { - _returnedFirstPoint = true; - point = _pds.AddFirstPoint(ref _settings, ref _state); - return true; - } - - // Remaining points have to be fed out carefully. - // We can be interrupted (by a successful point) mid-stream. - while (_state.ActivePoints.Count != 0) - { - if (_iterationPosition == 0) - { - // First point of iteration. - _iterationListIndex = _pds._random.Next(_state.ActivePoints.Count); - _iterationFound = false; - } - - var basePoint = _state.ActivePoints[_iterationListIndex]; - - point = _pds.AddNextPoint(basePoint, ref _settings, ref _state); - - // Set this now, return later after processing is complete. - _iterationFound |= point != null; - - // Iteration loop advance. - _iterationPosition++; - if (_iterationPosition == _pointsPerIteration) - { - // Reached end of this iteration. - _iterationPosition = 0; - if (!_iterationFound) - _state.ActivePoints.RemoveAt(_iterationListIndex); - } - - if (point != null) - return true; - } - point = null; - return false; - } - } - - internal struct State - { - public Vector2?[,] Grid; - public List ActivePoints; - } - - internal struct SampleSettings - { - public Vector2 TopLeft, LowerRight, Center; - public Vector2 Dimensions; - public float? RejectionSqDistance; - public float MinimumDistance; - public float CellSize; - public int GridWidth, GridHeight; - } -} - - - diff --git a/Content.Server/Worldgen/WorldGen.cs b/Content.Server/Worldgen/WorldGen.cs deleted file mode 100644 index 1ed20b9f1fa..00000000000 --- a/Content.Server/Worldgen/WorldGen.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Numerics; - -namespace Content.Server.Worldgen; - -/// -/// Contains a few world-generation related constants and static functions. -/// -public static class WorldGen -{ - /// - /// The size of each chunk (isn't that self-explanatory.) - /// Be careful about how small you make this. - /// - public const int ChunkSize = 128; - - /// - /// Converts world coordinates to chunk coordinates. - /// - /// World coordinates - /// Chunk coordinates - [Pure] - public static Vector2i WorldToChunkCoords(Vector2i inp) - { - return (inp * new Vector2(1.0f / ChunkSize, 1.0f / ChunkSize)).Floored(); - } - - /// - /// Converts world coordinates to chunk coordinates. - /// - /// World coordinates - /// Chunk coordinates - [Pure] - public static Vector2 WorldToChunkCoords(Vector2 inp) - { - return inp * new Vector2(1.0f / ChunkSize, 1.0f / ChunkSize); - } - - /// - /// Converts chunk coordinates to world coordinates. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoords(Vector2i inp) - { - return inp * ChunkSize; - } - - /// - /// Converts chunk coordinates to world coordinates. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoords(Vector2 inp) - { - return inp * ChunkSize; - } - - /// - /// Converts chunk coordinates to world coordinates, getting the center of the chunk. - /// - /// Chunk coordinates - /// World coordinates - [Pure] - public static Vector2 ChunkToWorldCoordsCentered(Vector2i inp) - { - return inp * ChunkSize + Vector2i.One * (ChunkSize / 2); - } -} - diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs index 1e9489e9ec9..88faf2fdcdb 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs @@ -22,5 +22,5 @@ public sealed partial class XAEEmpInAreaComponent : Component /// Duration (in seconds) for which devices going to be disabled. /// [DataField] - public float DisableDuration = 60f; + public TimeSpan DisableDuration = TimeSpan.FromSeconds(60); } diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs index 5e9bf7352b0..437e082581b 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs @@ -1,6 +1,7 @@ -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Xenoarchaeology.Artifact.XAE.Components; +using Content.Shared.Power.Components; +using Content.Shared.Power.EntitySystems; using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.XAE; @@ -20,12 +21,12 @@ public sealed class XAEChargeBatterySystem : BaseXAESystem protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args) { - var chargeBatteryComponent = ent.Comp; _batteryEntities.Clear(); - _lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities); + + _lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _batteryEntities); foreach (var battery in _batteryEntities) { - _battery.SetCharge(battery, battery.Comp.MaxCharge, battery); + _battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge); } } } diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs index 8418a3baf94..faf9c3a384d 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs @@ -19,7 +19,7 @@ public sealed class XAEPolymorphSystem : BaseXAESystem [Dependency] private readonly SharedAudioSystem _audio = default!; /// Pre-allocated and re-used collection. - private readonly HashSet> _humanoids = new(); + private readonly HashSet> _humanoids = new(); /// protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args) diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs index acc36f0f8d0..6bede34b68f 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPortalSystem.cs @@ -20,14 +20,10 @@ public sealed class XAEPortalSystem : BaseXAESystem [Dependency] private readonly LinkedEntitySystem _link = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly IGameTiming _timing = default!; /// protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - var map = Transform(ent).MapID; var validMinds = new ValueList(); var mindQuery = EntityQueryEnumerator(); diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs index d045682d559..5656182a9e5 100644 --- a/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs +++ b/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs @@ -54,7 +54,7 @@ private void OnMagnetActivated(ref SalvageMagnetActivatedEvent args) if (node.Attached == null) continue; - var artifact = _xenoArtifactQuery.Get(GetEntity(node.Attached.Value)); + var artifact = _xenoArtifactQuery.Get(node.Attached.Value); if (!CanTrigger(artifact, (uid, node))) continue; diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs index 885186f79c1..7087499c93a 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs @@ -1,87 +1,24 @@ -using Content.Server.Body.Systems; -using Content.Server.Popups; -using Content.Server.Power.EntitySystems; using Content.Server.Stack; -using Content.Shared.Body.Components; -using Content.Shared.Damage; -using Content.Shared.Power; +using Content.Shared.Gibbing; using Content.Shared.Storage.Components; -using Content.Shared.Verbs; using Content.Shared.Whitelist; using Content.Shared.Xenoarchaeology.Equipment; using Content.Shared.Xenoarchaeology.Equipment.Components; using Robust.Shared.Collections; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Xenoarchaeology.Equipment.Systems; /// public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem { - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly BodySystem _body = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly GibbingSystem _gibbing = default!; [Dependency] private readonly StackSystem _stack = default!; - [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnGetVerbs); - SubscribeLocalEvent(OnPowerChanged); - } - - private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null || ent.Comp.Crushing) - return; - - if (!TryComp(ent, out var entityStorageComp) || - entityStorageComp.Contents.ContainedEntities.Count == 0) - return; - - if (!this.IsPowered(ent, EntityManager)) - return; - - var verb = new AlternativeVerb - { - Text = Loc.GetString("artifact-crusher-verb-start-crushing"), - Priority = 2, - Act = () => StartCrushing((ent, ent.Comp, entityStorageComp)) - }; - args.Verbs.Add(verb); - } - - private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) - { - if (!args.Powered) - StopCrushing(ent); - } - - public void StartCrushing(Entity ent) - { - var (uid, crusher, _) = ent; - - if (crusher.Crushing) - return; - - if (crusher.AutoLock) - _popup.PopupEntity(Loc.GetString("artifact-crusher-autolocks-enable"), uid); - - crusher.Crushing = true; - crusher.NextSecond = _timing.CurTime + TimeSpan.FromSeconds(1); - crusher.CrushEndTime = _timing.CurTime + crusher.CrushDuration; - crusher.CrushingSoundEntity = AudioSystem.PlayPvs(crusher.CrushingSound, ent); - Appearance.SetData(ent, ArtifactCrusherVisuals.Crushing, true); - Dirty(ent, ent.Comp1); - } - - public void FinishCrushing(Entity ent) + // TODO: Move to shared once StackSystem spawning is in Shared and we have RandomPredicted + public override void FinishCrushing(Entity ent) { var (_, crusher, storage) = ent; StopCrushing((ent, ent.Comp1), false); @@ -96,49 +33,18 @@ public void FinishCrushing(Entity(contained, out var body)) - Del(contained); - - var gibs = _body.GibBody(contained, body: body, gibOrgans: true); + var gibs = _gibbing.Gib(contained); foreach (var gib in gibs) { ContainerSystem.Insert((gib, null, null, null), crusher.OutputContainer); } } } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var crusher, out var storage)) - { - if (!crusher.Crushing) - continue; - - if (crusher.NextSecond < _timing.CurTime) - { - var contents = new ValueList(storage.Contents.ContainedEntities); - foreach (var contained in contents) - { - _damageable.TryChangeDamage(contained, crusher.CrushingDamage); - } - crusher.NextSecond += TimeSpan.FromSeconds(1); - Dirty(uid, crusher); - } - - if (crusher.CrushEndTime < _timing.CurTime) - { - FinishCrushing((uid, crusher, storage)); - } - } - } } diff --git a/Content.Server/Xenoborgs/XenoborgSystem.cs b/Content.Server/Xenoborgs/XenoborgSystem.cs new file mode 100644 index 00000000000..8a046ce3c76 --- /dev/null +++ b/Content.Server/Xenoborgs/XenoborgSystem.cs @@ -0,0 +1,101 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Silicons.Borgs; +using Content.Shared.Destructible; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; +using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.Xenoborgs.Components; +using Robust.Shared.Audio; +using Robust.Shared.Player; + +namespace Content.Server.Xenoborgs; + +public sealed partial class XenoborgSystem : EntitySystem +{ + [Dependency] private readonly AntagSelectionSystem _antag = default!; + [Dependency] private readonly BorgSystem _borg = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly XenoborgsRuleSystem _xenoborgsRule = default!; + + private static readonly Color XenoborgBriefingColor = Color.BlueViolet; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnXenoborgDestroyed); + SubscribeLocalEvent(OnCoreDestroyed); + + SubscribeLocalEvent(OnXenoborgMindAdded); + SubscribeLocalEvent(OnXenoborgMindRemoved); + } + + private void OnXenoborgDestroyed(EntityUid uid, XenoborgComponent component, DestructionEventArgs args) + { + // if a xenoborg is destroyed, it will check to see if it was the last one + var xenoborgQuery = AllEntityQuery(); // paused xenoborgs still count + while (xenoborgQuery.MoveNext(out var xenoborg, out _)) + { + if (xenoborg != uid) + return; + } + + var mothershipCoreQuery = AllEntityQuery(); // paused mothership cores still count + var mothershipCoreAlive = mothershipCoreQuery.MoveNext(out _, out _); + + var xenoborgsRuleQuery = EntityQueryEnumerator(); + if (xenoborgsRuleQuery.MoveNext(out var xenoborgsRuleEnt, out var xenoborgsRuleComp)) + _xenoborgsRule.SendXenoborgDeathAnnouncement((xenoborgsRuleEnt, xenoborgsRuleComp), mothershipCoreAlive); + } + + private void OnCoreDestroyed(EntityUid ent, MothershipCoreComponent component, DestructionEventArgs args) + { + // if a mothership core is destroyed, it will see if there are any others + var mothershipCoreQuery = AllEntityQuery(); // paused mothership cores still count + while (mothershipCoreQuery.MoveNext(out var mothershipCoreEnt, out _)) + { + // if it finds a mothership core that is different from the one just destroyed, + // it doesn't explode the xenoborgs + if (mothershipCoreEnt != ent) + return; + } + + var xenoborgsRuleQuery = EntityQueryEnumerator(); + if (xenoborgsRuleQuery.MoveNext(out var xenoborgsRuleEnt, out var xenoborgsRuleComp)) + _xenoborgsRule.SendMothershipDeathAnnouncement((xenoborgsRuleEnt, xenoborgsRuleComp)); + + // explode all xenoborgs + var xenoborgQuery = AllEntityQuery(); // paused xenoborgs still explode + while (xenoborgQuery.MoveNext(out var xenoborgEnt, out _, out _)) + { + if (HasComp(xenoborgEnt)) + continue; + + // I got tired to trying to make this work via the device network. + // so brute force it is... + _borg.Destroy(xenoborgEnt); + } + } + + private void OnXenoborgMindAdded(EntityUid ent, XenoborgComponent comp, MindAddedMessage args) + { + _roles.MindAddRole(args.Mind, comp.MindRole, silent: true); + + if (!TryComp(ent, out var actorComp)) + return; + + _antag.SendBriefing(actorComp.PlayerSession, + Loc.GetString(comp.BriefingText), + XenoborgBriefingColor, + comp.BriefingSound + ); + } + + private void OnXenoborgMindRemoved(EntityUid ent, XenoborgComponent comp, MindRemovedMessage args) + { + _roles.MindRemoveRole(args.Mind.Owner, comp.MindRole); + } +} diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 7cdcec78c2a..9e4fbb286de 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Administration.Managers; using Content.Server.Atmos.Components; using Content.Server.Body.Components; @@ -13,16 +14,15 @@ using Content.Server.NPC.Systems; using Content.Server.StationEvents.Components; using Content.Server.Speech.Components; -using Content.Server.Temperature.Components; +using Content.Shared.Body; using Content.Shared.Body.Components; -using Content.Shared.Chat; using Content.Shared.CombatMode; using Content.Shared.CombatMode.Pacification; -using Content.Shared.Damage; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Humanoid; using Content.Shared.Interaction.Components; +using Content.Shared.Imperial.Traits; //Imperial "CrawlingForLegsParalyzed" using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Movement.Pulling.Components; @@ -38,12 +38,15 @@ using Content.Shared.Traits.Assorted; using Robust.Shared.Audio.Systems; using Content.Shared.Ghost.Roles.Components; +using Content.Shared.Humanoid.Markings; using Content.Shared.IdentityManagement; using Content.Shared.Tag; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; +using Content.Shared.Temperature.Components; +using Robust.Shared.Utility; namespace Content.Server.Zombies; @@ -62,7 +65,7 @@ public sealed partial class ZombieSystem [Dependency] private readonly NpcFactionSystem _faction = default!; [Dependency] private readonly GhostSystem _ghost = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly ServerInventorySystem _inventory = default!; [Dependency] private readonly MindSystem _mind = default!; @@ -77,6 +80,7 @@ public sealed partial class ZombieSystem private static readonly ProtoId ZombieFaction = "Zombie"; private static readonly string MindRoleZombie = "MindRoleZombie"; private static readonly List> BannableZombiePrototypes = ["Zombie"]; + internal static readonly HashSet AdditionalZombieLayers = [HumanoidVisualLayers.Tail, HumanoidVisualLayers.HeadSide, HumanoidVisualLayers.HeadTop, HumanoidVisualLayers.Snout]; /// /// Handles an entity turning into a zombie when they die or go into crit @@ -141,6 +145,7 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) RemComp(target); RemComp(target); RemComp(target); + RemComp(target); //Imperial "CrawlingForLegsParalyzed" RemComp(target); RemComp(target); @@ -156,7 +161,6 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) var combat = EnsureComp(target); RemComp(target); _combat.SetCanDisarm(target, false, combat); - _combat.SetInCombatMode(target, true, combat); //This is the actual damage of the zombie. We assign the visual appearance //and range here because of stuff we'll find out later @@ -189,27 +193,49 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) _autoEmote.AddEmote(target, "ZombieGroan"); } - //We have specific stuff for humanoid zombies because they matter more - if (TryComp(target, out var huApComp)) //huapcomp - { - //store some values before changing them in case the humanoid get cloned later - zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor; - zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor; - zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers); - if (TryComp(target, out var stream)) - zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent; - - _humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp); + if (TryComp(target, out var stream) && stream.BloodReferenceSolution is { } reagents) + zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone(); - // Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right - huApComp.EyeColor = zombiecomp.EyeColor; + if (_visualBody.TryGatherMarkingsData(target, null, out var profiles, out _, out var markings)) + { + // TODO: My kingdom for ZombieSystem just using cloning system + zombiecomp.BeforeZombifiedProfiles = profiles; + zombiecomp.BeforeZombifiedMarkings = markings.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToDictionary( + it => it.Key, + it => it.Value.ShallowClone())); + + var zombifiedProfiles = profiles.ToDictionary(pair => pair.Key, + pair => pair.Value with { EyeColor = zombiecomp.EyeColor, SkinColor = zombiecomp.SkinColor }); + _visualBody.ApplyProfiles(target, zombifiedProfiles); + + var newMarkings = markings.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToDictionary( + it => it.Key, + it => it.Value.ShallowClone())); + + foreach (var markingSet in newMarkings.Values) + { + foreach (var (layer, layerMarkings) in markingSet) + { + if (!AdditionalZombieLayers.Contains(layer)) + continue; + + for (var i = 0; i < layerMarkings.Count; i++) + { + layerMarkings[i] = layerMarkings[i].WithColor(zombiecomp.SkinColor); + } + } + } - // this might not resync on clone? - _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp); - _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp); - _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp); - _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp); + _visualBody.ApplyMarkings(target, newMarkings); + } + //We have specific stuff for humanoid zombies because they matter more + if (HasComp(target)) + { //This is done here because non-humanoids shouldn't get baller damage melee.Damage = zombiecomp.DamageOnBite; @@ -231,7 +257,7 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) //NOTE: they are supposed to bleed, just not take damage _bloodstream.SetBloodLossThreshold(target, 0f); //Give them zombie blood - _bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent); + _bloodstream.ChangeBloodReagents(target, zombiecomp.NewBloodReagents); //This is specifically here to combat insuls, because frying zombies on grilles is funny as shit. _inventory.TryUnequip(target, "gloves", true, true); @@ -245,12 +271,11 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) _mind.MakeSentient(target); //Make the zombie not die in the cold. Good for space zombies - if (TryComp(target, out var tempComp)) + if (TryComp(target, out var tempComp)) tempComp.ColdDamage.ClampMax(0); //Heals the zombie from all the damage it took while human - if (TryComp(target, out var damageablecomp)) - _damageable.SetAllDamage(target, damageablecomp, 0); + _damageable.ClearAllDamage(target); _mobState.ChangeMobState(target, MobState.Alive); _faction.ClearFactions(target, dirty: false); diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 5107a87c2a0..0322f44183c 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Shared.NPC.Prototypes; using Content.Server.Actions; using Content.Server.Body.Systems; @@ -10,7 +9,8 @@ using Content.Shared.Armor; using Content.Shared.Bed.Sleep; using Content.Shared.Cloning.Events; -using Content.Shared.Damage; +using Content.Shared.Chat; +using Content.Shared.Damage.Systems; using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Mind; @@ -26,6 +26,8 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.CombatMode.Pacification; // Imperial Space edit +using Content.Shared.Imperial.Zombies; // Imperial Space edit namespace Content.Server.Zombies { @@ -96,6 +98,8 @@ private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, return; EnsureComp(uid, out PendingZombieComponent pendingComp); + EnsureComp(uid); // Imperial Space edit + EnsureComp(uid); // Imperial Space edit pendingComp.GracePeriod = _random.Next(pendingComp.MinInitialInfectedGrace, pendingComp.MaxInitialInfectedGrace); } @@ -117,7 +121,7 @@ public override void Update(float frameTime) var curTime = _timing.CurTime; // Hurt the living infected - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState)) { // Process only once per second @@ -137,11 +141,11 @@ public override void Update(float frameTime) ? comp.CritDamageMultiplier : 1f; - _damageable.TryChangeDamage(uid, comp.Damage * multiplier, true, false, damage); + _damageable.ChangeDamage((uid, damage), comp.Damage * multiplier, true, false); } // Heal the zombified - var zombQuery = EntityQueryEnumerator(); + var zombQuery = EntityQueryEnumerator(); while (zombQuery.MoveNext(out var uid, out var comp, out var damage, out var mobState)) { // Process only once per second @@ -158,7 +162,7 @@ public override void Update(float frameTime) : 1f; // Gradual healing for living zombies. - _damageable.TryChangeDamage(uid, comp.PassiveHealing * multiplier, true, false, damage); + _damageable.ChangeDamage((uid, damage), comp.PassiveHealing * multiplier, true, false); } } @@ -235,43 +239,47 @@ private float GetZombieInfectionChance(EntityUid uid, ZombieComponent zombieComp return MathF.Max(chance, zombieComponent.MinZombieInfectionChance); } - private void OnMeleeHit(EntityUid uid, ZombieComponent component, MeleeHitEvent args) + private void OnMeleeHit(Entity entity, ref MeleeHitEvent args) { - if (!TryComp(args.User, out _)) + if (!args.IsHit) return; - if (!args.HitEntities.Any()) - return; + var cannotSpread = HasComp(args.User); - foreach (var entity in args.HitEntities) + foreach (var uid in args.HitEntities) { - if (args.User == entity) + if (args.User == uid) continue; - if (!TryComp(entity, out var mobState)) + if (!TryComp(uid, out var mobState)) continue; - if (HasComp(entity)) - { - args.BonusDamage = -args.BaseDamage; - } - else + if (HasComp(uid) || HasComp(uid)) { - if (!HasComp(entity) && !HasComp(args.User) && _random.Prob(GetZombieInfectionChance(entity, component))) - { - EnsureComp(entity); - EnsureComp(entity); - } + // Don't infect, don't deal damage, do not heal from bites, don't pass go! + args.Handled = true; + continue; } - if (_mobState.IsIncapacitated(entity, mobState) && !HasComp(entity) && !HasComp(entity)) + if (_mobState.IsAlive(uid, mobState)) { - ZombifyEntity(entity); - args.BonusDamage = -args.BaseDamage; + _damageable.TryChangeDamage(args.User, entity.Comp.HealingOnBite, true, false); + + // If we cannot infect the living target, the zed will just heal itself. + if (HasComp(uid) || cannotSpread || !_random.Prob(GetZombieInfectionChance(uid, entity.Comp))) + continue; + + EnsureComp(uid); + EnsureComp(uid); } - else if (mobState.CurrentState == MobState.Alive) //heals when zombies bite live entities + else { - _damageable.TryChangeDamage(uid, component.HealingOnBite, true, false); + if (HasComp(uid) || cannotSpread) + continue; + + // If the target is dead and can be infected, infect. + ZombifyEntity(uid); + args.Handled = true; } } } @@ -291,17 +299,10 @@ public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombi if (!Resolve(source, ref zombiecomp)) return false; - foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers) - { - _humanoidAppearance.SetBaseLayerColor(target, layer, info.Color); - _humanoidAppearance.SetBaseLayerId(target, layer, info.Id); - } - if (TryComp(target, out var appcomp)) - { - appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor; - } - _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false); - _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent); + _visualBody.ApplyProfiles(target, zombiecomp.BeforeZombifiedProfiles); + _visualBody.ApplyMarkings(target, zombiecomp.BeforeZombifiedMarkings); + + _bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents); return true; } diff --git a/Content.Shared.Database/Bans.cs b/Content.Shared.Database/Bans.cs new file mode 100644 index 00000000000..9fb100f5db4 --- /dev/null +++ b/Content.Shared.Database/Bans.cs @@ -0,0 +1,33 @@ +namespace Content.Shared.Database; + +/// +/// Types of bans that can be stored in the database. +/// +public enum BanType : byte +{ + /// + /// A ban from the entire server. If a player matches the ban info, they will be refused connection. + /// + Server, + + /// + /// A ban from playing one or more roles. + /// + Role, +} + +/// +/// A single role for a database role ban. +/// +/// The type of role being banned, e.g. Job. +/// +/// The ID of the role being banned. This is likely a prototype ID based on . +/// +[Serializable] +public record struct BanRoleDef(string RoleType, string RoleId) +{ + public override string ToString() + { + return $"{RoleType}:{RoleId}"; + } +} diff --git a/Content.Shared.Database/Content.Shared.Database.csproj b/Content.Shared.Database/Content.Shared.Database.csproj index bc2cc300bff..5b14175f4fe 100644 --- a/Content.Shared.Database/Content.Shared.Database.csproj +++ b/Content.Shared.Database/Content.Shared.Database.csproj @@ -1,8 +1,9 @@ enable - enable + + diff --git a/Content.Shared.Database/Content.Shared.Database.csproj.lscache b/Content.Shared.Database/Content.Shared.Database.csproj.lscache new file mode 100644 index 00000000000..b1cde1d26d3 --- /dev/null +++ b/Content.Shared.Database/Content.Shared.Database.csproj.lscache @@ -0,0 +1,254 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Shared.Database +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Shared.Database +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=bin/Debug/net10.0/Content.Shared.Database.dll +TargetRefPath=obj/Debug/net10.0/ref/Content.Shared.Database.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\net10.0\Content.Shared.Database.dll +/refout:obj\Debug\net10.0\refint\Content.Shared.Database.dll +/target:library +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:NU1605,SYSLIB0011 + +[sourceFiles] +Bans.cs +LogImpact.cs +LogType.cs +NoteSeverity.cs +NoteType.cs +obj/Debug/net10.0/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Shared.Database.AssemblyInfo.cs + Content.Shared.Database.GlobalUsings.g.cs +TypedHwid.cs + +[metadataReferences] +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/net10.0/Content.Shared.Database.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index b96007358a2..fd70af8cd68 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -81,9 +81,9 @@ public enum LogType ChemicalReaction = 17, /// - /// Reagent effects related interactions. + /// EntityEffect related interactions. /// - ReagentEffect = 18, + EntityEffect = 18, /// /// Canister valve was opened or closed. @@ -480,7 +480,12 @@ public enum LogType Instrument = 103, /// - /// Medieval factions relations interactions. + /// Events related to players connecting/disconnecting. /// - MedievalFactionRelations = 104, + Connection = 104, + + /// + /// Imperial Medieval factions relations interactions. + /// + MedievalFactionRelations = 105, } diff --git a/Content.Shared/APC/SharedApc.cs b/Content.Shared/APC/SharedApc.cs index 802c06a6ab6..c8bf1cc3e88 100644 --- a/Content.Shared/APC/SharedApc.cs +++ b/Content.Shared/APC/SharedApc.cs @@ -181,13 +181,17 @@ public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable public readonly int Power; public readonly ApcExternalPowerState ApcExternalPower; public readonly float Charge; + public readonly float MaxLoad; + public readonly bool Tripped; - public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge) + public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge, float maxLoad, bool tripped) { MainBreaker = mainBreaker; Power = power; ApcExternalPower = apcExternalPower; Charge = charge; + MaxLoad = maxLoad; + Tripped = tripped; } public bool Equals(ApcBoundInterfaceState? other) @@ -197,7 +201,9 @@ public bool Equals(ApcBoundInterfaceState? other) return MainBreaker == other.MainBreaker && Power == other.Power && ApcExternalPower == other.ApcExternalPower && - MathHelper.CloseTo(Charge, other.Charge); + MathHelper.CloseTo(Charge, other.Charge) && + MathHelper.CloseTo(MaxLoad, other.MaxLoad) && + Tripped == other.Tripped; } public override bool Equals(object? obj) @@ -207,7 +213,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge); + return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge, MaxLoad, Tripped); } } diff --git a/Content.Shared/Access/Components/AccessOverriderComponent.cs b/Content.Shared/Access/Components/AccessOverriderComponent.cs index 6a1bf2c831a..27a6f7d5997 100644 --- a/Content.Shared/Access/Components/AccessOverriderComponent.cs +++ b/Content.Shared/Access/Components/AccessOverriderComponent.cs @@ -13,6 +13,12 @@ public sealed partial class AccessOverriderComponent : Component { public static string PrivilegedIdCardSlotId = "AccessOverrider-privilegedId"; + /// + /// If the Access Overrider UI will show info about the privileged ID + /// + [DataField] + public bool ShowPrivilegedId = true; + [DataField] public ItemSlot PrivilegedIdSlot = new(); @@ -48,6 +54,7 @@ public sealed class AccessOverriderBoundUserInterfaceState : BoundUserInterfaceS public readonly string PrivilegedIdName; public readonly bool IsPrivilegedIdPresent; public readonly bool IsPrivilegedIdAuthorized; + public readonly bool ShowPrivilegedIdGrid; public readonly ProtoId[]? TargetAccessReaderIdAccessList; public readonly ProtoId[]? AllowedModifyAccessList; public readonly ProtoId[]? MissingPrivilegesList; @@ -59,7 +66,8 @@ public AccessOverriderBoundUserInterfaceState(bool isPrivilegedIdPresent, ProtoId[]? missingPrivilegesList, string privilegedIdName, string targetLabel, - Color targetLabelColor) + Color targetLabelColor, + bool showPrivilegedIdGrid) { IsPrivilegedIdPresent = isPrivilegedIdPresent; IsPrivilegedIdAuthorized = isPrivilegedIdAuthorized; @@ -69,6 +77,7 @@ public AccessOverriderBoundUserInterfaceState(bool isPrivilegedIdPresent, PrivilegedIdName = privilegedIdName; TargetLabel = targetLabel; TargetLabelColor = targetLabelColor; + ShowPrivilegedIdGrid = showPrivilegedIdGrid; } } diff --git a/Content.Shared/Access/Components/AccessReaderComponent.cs b/Content.Shared/Access/Components/AccessReaderComponent.cs index c261c7decac..32662d78384 100644 --- a/Content.Shared/Access/Components/AccessReaderComponent.cs +++ b/Content.Shared/Access/Components/AccessReaderComponent.cs @@ -38,7 +38,7 @@ public sealed partial class AccessReaderComponent : Component /// An unmodified copy of the original list of the access groups that grant access to this reader. /// /// - /// If null, the access lists of this entity have not been modified yet. + /// If null, entity isn't intialized yet. /// [DataField] public List>>? AccessListsOriginal = null; diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs index 93c61ea9f09..f4105e3ecd8 100644 --- a/Content.Shared/Access/Components/IdCardComponent.cs +++ b/Content.Shared/Access/Components/IdCardComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared.Access.Components; [RegisterComponent, NetworkedComponent] -[AutoGenerateComponentState] +[AutoGenerateComponentState(true)] [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)] public sealed partial class IdCardComponent : Component { @@ -27,7 +27,7 @@ public sealed partial class IdCardComponent : Component private string? _jobTitle; [Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWriteExecute)] - public string? LocalizedJobTitle { set => _jobTitle = value; get => _jobTitle ?? Loc.GetString(JobTitle ?? string.Empty); } + public string? LocalizedJobTitle { set => _jobTitle = value; get => _jobTitle ?? (JobTitle != null ? Loc.GetString(JobTitle) : string.Empty); } /// /// The state of the job icon rsi. diff --git a/Content.Shared/Access/Components/IdCardConsoleComponent.cs b/Content.Shared/Access/Components/IdCardConsoleComponent.cs index 8c965971a8e..89c24940ba3 100644 --- a/Content.Shared/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Shared/Access/Components/IdCardConsoleComponent.cs @@ -26,9 +26,9 @@ public sealed class WriteToTargetIdMessage : BoundUserInterfaceMessage public readonly string FullName; public readonly string JobTitle; public readonly List> AccessList; - public readonly ProtoId JobPrototype; + public readonly ProtoId? JobPrototype; - public WriteToTargetIdMessage(string fullName, string jobTitle, List> accessList, ProtoId jobPrototype) + public WriteToTargetIdMessage(string fullName, string jobTitle, List> accessList, ProtoId? jobPrototype) { FullName = fullName; JobTitle = jobTitle; @@ -57,6 +57,8 @@ public WriteToTargetIdMessage(string fullName, string jobTitle, List(OnMapInit); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnLinkAttempt); SubscribeLocalEvent(OnConfigurationAttempt); + SubscribeLocalEvent(OnFindAvailableLocks); + SubscribeLocalEvent(OnCheckLockAccess); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); } + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.AccessListsOriginal ??= [.. ent.Comp.AccessLists]; + Dirty(ent); + } + private void OnExamined(Entity ent, ref ExaminedEvent args) { - if (!GetMainAccessReader(ent, out var mainAccessReader)) + if (!GetMainAccessReader(ent, out var mainAccessReader) || + mainAccessReader.Value.Comp.AccessListsOriginal == null) return; - mainAccessReader.Value.Comp.AccessListsOriginal ??= new(mainAccessReader.Value.Comp.AccessLists); - var accessHasBeenModified = mainAccessReader.Value.Comp.AccessLists.Count != mainAccessReader.Value.Comp.AccessListsOriginal.Count; if (!accessHasBeenModified) @@ -70,9 +79,10 @@ private void OnExamined(Entity ent, ref ExaminedEvent arg } } + var examiner = args.Examiner; var canSeeAccessModification = accessHasBeenModified && - (HasComp(ent) || - _inventorySystem.TryGetInventoryEntity(args.Examiner, out _)); + (HasComp(examiner) || + _inventorySystem.TryGetInventoryEntity(examiner, out _)); if (canSeeAccessModification) { @@ -168,6 +178,22 @@ private void OnConfigurationAttempt(Entity ent, ref Acces ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists); } + private void OnFindAvailableLocks(Entity ent, ref FindAvailableLocksEvent args) + { + args.FoundReaders |= LockTypes.Access; + } + + private void OnCheckLockAccess(Entity ent, ref CheckUserHasLockAccessEvent args) + { + // Are we looking for an access lock? + if (!args.FoundReaders.HasFlag(LockTypes.Access)) + return; + + // If the user has access to this lock, we pass it into the event. + if (IsAllowed(args.User, ent)) + args.HasAccess |= LockTypes.Access; + } + /// /// Searches the source for access tags /// then compares it with the all targets accesses to see if it is allowed. diff --git a/Content.Shared/Access/Systems/ActivatableUIRequiresAccessSystem.cs b/Content.Shared/Access/Systems/ActivatableUIRequiresAccessSystem.cs index 9020518f216..eb94ed324d1 100644 --- a/Content.Shared/Access/Systems/ActivatableUIRequiresAccessSystem.cs +++ b/Content.Shared/Access/Systems/ActivatableUIRequiresAccessSystem.cs @@ -23,7 +23,7 @@ private void OnUIOpenAttempt(Entity activa if (!_access.IsAllowed(args.User, activatableUI)) { args.Cancel(); - if (activatableUI.Comp.PopupMessage != null) + if (activatableUI.Comp.PopupMessage != null && !args.Silent) _popup.PopupClient(Loc.GetString(activatableUI.Comp.PopupMessage), activatableUI, args.User); } } diff --git a/Content.Shared/Access/Systems/SharedIdCardSystem.cs b/Content.Shared/Access/Systems/SharedIdCardSystem.cs index 513abb20eef..709afce251f 100644 --- a/Content.Shared/Access/Systems/SharedIdCardSystem.cs +++ b/Content.Shared/Access/Systems/SharedIdCardSystem.cs @@ -25,6 +25,7 @@ public abstract class SharedIdCardSystem : EntitySystem [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedJobStatusSystem _jobStatus = default!; // CCVar. private int _maxNameLength; @@ -35,6 +36,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnTryGetIdentityShortInfo); SubscribeLocalEvent(OnRename); @@ -77,6 +79,15 @@ private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent ev) ev.Handled = true; } + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + // Try to update the job status icon of the player owning the ID, if any. + if (HasComp(Transform(ent).ParentUid)) + _jobStatus.UpdateStatus(Transform(Transform(ent).ParentUid).ParentUid); //ID is inside a PDA + else + _jobStatus.UpdateStatus(Transform(ent).ParentUid); //ID is held/directly in the ID slot + } + /// /// Attempt to find an ID card on an entity. This will look in the entity itself, in the entity's hands, and /// in the entity's inventory. diff --git a/Content.Shared/Access/Systems/SharedJobStatusSystem.cs b/Content.Shared/Access/Systems/SharedJobStatusSystem.cs new file mode 100644 index 00000000000..0e74716aa49 --- /dev/null +++ b/Content.Shared/Access/Systems/SharedJobStatusSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Access.Components; +using Content.Shared.Hands; +using Content.Shared.Inventory.Events; +using Content.Shared.PDA; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Access.Systems; + +public abstract class SharedJobStatusSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessReader = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private static readonly ProtoId JobIconForNoId = "JobIconNoId"; + + public override void Initialize() + { + base.Initialize(); + + // if the mob picks up, drops or (un)equips a pda or Id card then update their crew status + SubscribeLocalEvent((uid, comp, _) => UpdateStatus((uid, comp))); + SubscribeLocalEvent((uid, comp, _) => UpdateStatus((uid, comp))); + SubscribeLocalEvent((uid, comp, _) => UpdateStatus((uid, comp))); + SubscribeLocalEvent((uid, comp, _) => UpdateStatus((uid, comp))); + } + + /// + /// Updates this mob's job and crew status depending on their currently equipped or held pda or Id card. + /// + public void UpdateStatus(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + var iconId = JobIconForNoId; + + if (_accessReader.FindAccessItemsInventory(ent.Owner, out var items)) + { + foreach (var item in items) + { + // ID Card + if (TryComp(item, out var id)) + { + iconId = id.JobIcon; + break; + } + + // PDA + if (TryComp(item, out var pda) + && pda.ContainedId != null + && TryComp(pda.ContainedId, out id)) + { + iconId = id.JobIcon; + break; + } + } + } + + ent.Comp.JobStatusIcon = iconId; + ent.Comp.IsCrew = _prototype.Index(iconId).IsCrewJob; + Dirty(ent); + } +} diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index 08eac657c02..18c85c3aaa8 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -23,22 +23,11 @@ public sealed class ActionBlockerSystem : EntitySystem { [Dependency] private readonly SharedContainerSystem _container = default!; - private EntityQuery _complexInteractionQuery; - - public override void Initialize() - { - base.Initialize(); - - _complexInteractionQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnMoverStartup); - } - - private void OnMoverStartup(EntityUid uid, InputMoverComponent component, ComponentStartup args) - { - UpdateCanMove(uid, component); - } + [Dependency] private readonly EntityQuery _complexInteractionQuery = default!; + // These two methods should probably both live in SharedMoverController + // but they're called in a million places and I'm not doing that + // refactor right now. public bool CanMove(EntityUid uid, InputMoverComponent? component = null) { return Resolve(uid, ref component, false) && component.CanMove; @@ -167,15 +156,21 @@ public bool CanDrop(EntityUid uid) return !ev.Cancelled; } - public bool CanPickup(EntityUid user, EntityUid item) + /// + /// Whether a user can pickup the given item. + /// + /// The mob trying to pick up the item. + /// The item being picked up. + /// Whether or not to show a popup to the player telling them why the attempt failed. + public bool CanPickup(EntityUid user, EntityUid item, bool showPopup = false) { - var userEv = new PickupAttemptEvent(user, item); + var userEv = new PickupAttemptEvent(user, item, showPopup); RaiseLocalEvent(user, userEv); if (userEv.Cancelled) return false; - var itemEv = new GettingPickedUpAttemptEvent(user, item); + var itemEv = new GettingPickedUpAttemptEvent(user, item, showPopup); RaiseLocalEvent(item, itemEv); return !itemEv.Cancelled; diff --git a/Content.Shared/Actions/ActionContainerSystem.cs b/Content.Shared/Actions/ActionContainerSystem.cs index 534f0d3ee74..642131a3655 100644 --- a/Content.Shared/Actions/ActionContainerSystem.cs +++ b/Content.Shared/Actions/ActionContainerSystem.cs @@ -23,14 +23,12 @@ public sealed class ActionContainerSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMindSystem _mind = default!; - private EntityQuery _query; + [Dependency] private readonly EntityQuery _query = default!; public override void Initialize() { base.Initialize(); - _query = GetEntityQuery(); - SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnEntityRemoved); diff --git a/Content.Shared/Actions/Components/ActionComponent.cs b/Content.Shared/Actions/Components/ActionComponent.cs index 963d211aded..b78279f87d7 100644 --- a/Content.Shared/Actions/Components/ActionComponent.cs +++ b/Content.Shared/Actions/Components/ActionComponent.cs @@ -197,7 +197,7 @@ public EntityUid? EntityIcon } [DataRecord, Serializable, NetSerializable] -public record struct ActionCooldown +public partial record struct ActionCooldown { [DataField(required: true, customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan Start; diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 9bb15c9fe08..b12067e7de8 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -34,19 +34,15 @@ public abstract partial class SharedActionsSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - private EntityQuery _actionQuery; - private EntityQuery _actionsQuery; - private EntityQuery _mindQuery; + [Dependency] private readonly EntityQuery _actionQuery = default!; + [Dependency] private readonly EntityQuery _actionsQuery = default!; + [Dependency] private readonly EntityQuery _mindQuery = default!; public override void Initialize() { base.Initialize(); InitializeActionDoAfter(); - _actionQuery = GetEntityQuery(); - _actionsQuery = GetEntityQuery(); - _mindQuery = GetEntityQuery(); - SubscribeLocalEvent(OnActionMapInit); SubscribeLocalEvent(OnActionShutdown); @@ -449,7 +445,7 @@ public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity ent, ref DidEquipEvent args) if (GameTiming.ApplyingState) return; - var ev = new GetItemActionsEvent(_actionContainer, args.Equipee, args.Equipment, args.SlotFlags); + var ev = new GetItemActionsEvent(_actionContainer, args.EquipTarget, args.Equipment, args.SlotFlags); RaiseLocalEvent(args.Equipment, ev); if (ev.Actions.Count == 0) diff --git a/Content.Shared/Administration/AdminFrozenSystem.cs b/Content.Shared/Administration/AdminFrozenSystem.cs index 7419137da81..ee0afb543a8 100644 --- a/Content.Shared/Administration/AdminFrozenSystem.cs +++ b/Content.Shared/Administration/AdminFrozenSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.ActionBlocker; +using Content.Shared.Chat; using Content.Shared.Emoting; using Content.Shared.Interaction.Events; using Content.Shared.Item; @@ -31,6 +32,8 @@ public override void Initialize() SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnEmoteAttempt); SubscribeLocalEvent(OnSpeakAttempt); + SubscribeLocalEvent(OnInGameOocMessageAttempt); + SubscribeLocalEvent(OnInGameOocMessageAttemptBroadcast); } /// @@ -56,6 +59,20 @@ private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, Speak args.Cancel(); } + private void OnInGameOocMessageAttempt(Entity ent, ref InGameOocMessageAttemptEvent args) + { + if (!ent.Comp.Muted) + return; + + // Despite Type being available, Admin Mute does not care to differentiate. If you are out, you are out. + args.Cancelled = true; + } + + private void OnInGameOocMessageAttemptBroadcast(ref InGameOocMessageAttemptEvent args) + { + //TODO Player LOOC mute/ban. Session is in the args, but where to store/check the muted state? + } + private void OnAttempt(EntityUid uid, AdminFrozenComponent component, CancellableEntityEventArgs args) { args.Cancel(); diff --git a/Content.Shared/Administration/BanList/BanListEuiState.cs b/Content.Shared/Administration/BanList/BanListEuiState.cs index 09faa9706ea..c885ff1f706 100644 --- a/Content.Shared/Administration/BanList/BanListEuiState.cs +++ b/Content.Shared/Administration/BanList/BanListEuiState.cs @@ -6,7 +6,7 @@ namespace Content.Shared.Administration.BanList; [Serializable, NetSerializable] public sealed class BanListEuiState : EuiStateBase { - public BanListEuiState(string banListPlayerName, List bans, List roleBans) + public BanListEuiState(string banListPlayerName, List bans, List roleBans) { BanListPlayerName = banListPlayerName; Bans = bans; @@ -14,6 +14,6 @@ public BanListEuiState(string banListPlayerName, List bans, Lis } public string BanListPlayerName { get; } - public List Bans { get; } - public List RoleBans { get; } + public List Bans { get; } + public List RoleBans { get; } } diff --git a/Content.Shared/Administration/BanList/SharedBan.cs b/Content.Shared/Administration/BanList/SharedBan.cs new file mode 100644 index 00000000000..68ccc02c58f --- /dev/null +++ b/Content.Shared/Administration/BanList/SharedBan.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; +using Content.Shared.Database; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration.BanList; + +[Serializable, NetSerializable] +public record SharedBan( + int? Id, + BanType Type, + ImmutableArray UserIds, + ImmutableArray<(string address, int cidrMask)> Addresses, + ImmutableArray HWIds, + DateTime BanTime, + DateTime? ExpirationTime, + string Reason, + string? BanningAdminName, + SharedUnban? Unban, + ImmutableArray? Roles); diff --git a/Content.Shared/Administration/BanList/SharedServerBan.cs b/Content.Shared/Administration/BanList/SharedServerBan.cs deleted file mode 100644 index a8b9ce0d9a1..00000000000 --- a/Content.Shared/Administration/BanList/SharedServerBan.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public record SharedServerBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban -); diff --git a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs b/Content.Shared/Administration/BanList/SharedServerRoleBan.cs deleted file mode 100644 index fca2ea15839..00000000000 --- a/Content.Shared/Administration/BanList/SharedServerRoleBan.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public sealed record SharedServerRoleBan( - int? Id, - NetUserId? UserId, - (string address, int cidrMask)? Address, - string? HWId, - DateTime BanTime, - DateTime? ExpirationTime, - string Reason, - string? BanningAdminName, - SharedServerUnban? Unban, - string Role -) : SharedServerBan(Id, UserId, Address, HWId, BanTime, ExpirationTime, Reason, BanningAdminName, Unban); diff --git a/Content.Shared/Administration/BanList/SharedServerUnban.cs b/Content.Shared/Administration/BanList/SharedServerUnban.cs deleted file mode 100644 index f3a57e41590..00000000000 --- a/Content.Shared/Administration/BanList/SharedServerUnban.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Administration.BanList; - -[Serializable, NetSerializable] -public sealed record SharedServerUnban( - string? UnbanningAdmin, - DateTime UnbanTime -); diff --git a/Content.Shared/Administration/BanList/SharedUnban.cs b/Content.Shared/Administration/BanList/SharedUnban.cs new file mode 100644 index 00000000000..d60bb9184e2 --- /dev/null +++ b/Content.Shared/Administration/BanList/SharedUnban.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration.BanList; + +[Serializable, NetSerializable] +public sealed record SharedUnban( + string? UnbanningAdmin, + DateTime UnbanTime +); diff --git a/Content.Shared/Administration/Components/AdminMinigunComponent.cs b/Content.Shared/Administration/Components/AdminMinigunComponent.cs new file mode 100644 index 00000000000..04a7aacd692 --- /dev/null +++ b/Content.Shared/Administration/Components/AdminMinigunComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Administration.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class AdminMinigunComponent : Component; diff --git a/Content.Shared/Administration/Components/BufferingComponent.cs b/Content.Shared/Administration/Components/BufferingComponent.cs new file mode 100644 index 00000000000..3d365d00aa0 --- /dev/null +++ b/Content.Shared/Administration/Components/BufferingComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Administration.Systems; + +namespace Content.Shared.Administration.Components; + +[RegisterComponent] +[Access(typeof(SharedBufferingSystem))] +public sealed partial class BufferingComponent : Component +{ + [DataField("minBufferTime")] + public float MinimumBufferTime = 0.5f; + + [DataField("maxBufferTime")] + public float MaximumBufferTime = 1.5f; + + [DataField("minTimeTilNextBuffer")] + public float MinimumTimeTilNextBuffer = 10.0f; + + [DataField("maxTimeTilNextBuffer")] + public float MaximumTimeTilNextBuffer = 120.0f; + + [DataField] + public float TimeTilNextBuffer = 15.0f; + + [DataField] + public EntityUid? BufferingIcon = null; + + [DataField] + public float BufferingTimer = 0.0f; +} diff --git a/Content.Shared/Administration/Components/DisarmProneComponent.cs b/Content.Shared/Administration/Components/DisarmProneComponent.cs index 4ba11df8e23..86addab4f64 100644 --- a/Content.Shared/Administration/Components/DisarmProneComponent.cs +++ b/Content.Shared/Administration/Components/DisarmProneComponent.cs @@ -8,7 +8,4 @@ namespace Content.Shared.Administration.Components; /// [RegisterComponent, NetworkedComponent] [Access(typeof(SharedMeleeWeaponSystem))] -public sealed partial class DisarmProneComponent : Component -{ - -} +public sealed partial class DisarmProneComponent : Component; diff --git a/Content.Shared/Administration/Components/SharedHeadstandComponent.cs b/Content.Shared/Administration/Components/HeadstandComponent.cs similarity index 100% rename from Content.Shared/Administration/Components/SharedHeadstandComponent.cs rename to Content.Shared/Administration/Components/HeadstandComponent.cs diff --git a/Content.Shared/Administration/Components/KillSignComponent.cs b/Content.Shared/Administration/Components/KillSignComponent.cs new file mode 100644 index 00000000000..796a98a71da --- /dev/null +++ b/Content.Shared/Administration/Components/KillSignComponent.cs @@ -0,0 +1,43 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Administration.Components; + +/// +/// Displays a sprite above an entity. +/// By default a huge sign saying "KILL". +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +public sealed partial class KillSignComponent : Component +{ + /// + /// The sprite show above the entity. + /// + [DataField, AutoNetworkedField] + public SpriteSpecifier? Sprite = new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "kill"); + + /// + /// Whether the granted layer should always be forced to be unshaded. + /// + [DataField, AutoNetworkedField] + public bool ForceUnshaded = true; + + /// + /// Whether the granted layer should be offset to be above the entity. + /// + [DataField, AutoNetworkedField] + public bool DoOffset = true; + + /// + /// Prevents the sign from displaying to the owner of the component, allowing everyone but them to see it. + /// + [DataField, AutoNetworkedField] + public bool HideFromOwner = false; + + /// + /// The scale of the sprite. + /// + [DataField, AutoNetworkedField] + public Vector2 Scale = Vector2.One; +} diff --git a/Content.Shared/Administration/Components/SharedKillSignComponent.cs b/Content.Shared/Administration/Components/SharedKillSignComponent.cs deleted file mode 100644 index 9a95454f721..00000000000 --- a/Content.Shared/Administration/Components/SharedKillSignComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Administration.Components; - -[NetworkedComponent] -public abstract partial class SharedKillSignComponent : Component -{ - -} diff --git a/Content.Shared/Administration/Components/StationInfiniteBatteryTargetComponent.cs b/Content.Shared/Administration/Components/StationInfiniteBatteryTargetComponent.cs new file mode 100644 index 00000000000..d26527a4727 --- /dev/null +++ b/Content.Shared/Administration/Components/StationInfiniteBatteryTargetComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Administration.Components; + +/// +/// This is used for the admin map-wide/station-wide/grid-wide infinite power trick. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StationInfiniteBatteryTargetComponent : Component; diff --git a/Content.Shared/Administration/Logs/LogStringHandler.cs b/Content.Shared/Administration/Logs/LogStringHandler.cs index 9b65c6d723f..2fab949d581 100644 --- a/Content.Shared/Administration/Logs/LogStringHandler.cs +++ b/Content.Shared/Administration/Logs/LogStringHandler.cs @@ -40,7 +40,8 @@ private void AddFormat(string? format, T value, string? argument = null) format = argument[0] == '@' ? argument[1..] : argument; } - if (Values.TryAdd(Logger.ConvertName(format), value) + format = Logger.ConvertName(format); + if (Values.TryAdd(format, value) || Values[format] is T val && val.Equals(value) ) { return; @@ -50,7 +51,7 @@ private void AddFormat(string? format, T value, string? argument = null) var i = 2; format = $"{originalFormat}_{i}"; - while (!(Values.TryAdd(Logger.ConvertName(format), value) + while (!(Values.TryAdd(format, value) || Values[format] is T val2 && val2.Equals(value))) { format = $"{originalFormat}_{i}"; diff --git a/Content.Shared/Administration/Notes/SharedAdminNote.cs b/Content.Shared/Administration/Notes/SharedAdminNote.cs index 09d4f3f9478..d849d350784 100644 --- a/Content.Shared/Administration/Notes/SharedAdminNote.cs +++ b/Content.Shared/Administration/Notes/SharedAdminNote.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Content.Shared.Database; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -7,8 +8,8 @@ namespace Content.Shared.Administration.Notes; [Serializable, NetSerializable] public sealed record SharedAdminNote( int Id, // Id of note, message, watchlist, ban or role ban. Should be paired with NoteType to uniquely identify a shared admin note. - NetUserId Player, // Notes player - int? Round, // Which round was it added in? + ImmutableArray Players, // Notes player + ImmutableArray Rounds, // Which round was it added in? string? ServerName, // Which server was this added on? TimeSpan PlaytimeAtNote, // Playtime at the time of getting the note NoteType NoteType, // Type of note @@ -20,7 +21,7 @@ public sealed record SharedAdminNote( DateTime CreatedAt, // When was it created? DateTime? LastEditedAt, // When was it last edited? DateTime? ExpiryTime, // Does it expire? - string[]? BannedRoles, // Only valid for role bans. List of banned roles + ImmutableArray? BannedRoles, // Only valid for role bans. List of banned roles DateTime? UnbannedTime, // Only valid for bans. Set if unbanned string? UnbannedByName, // Only valid for bans. Set if unbanned bool? Seen // Only valid for messages, otherwise should be null. Has the user seen this message? diff --git a/Content.Server/Administration/Systems/AdminGunSystem.cs b/Content.Shared/Administration/Systems/AdminGunSystem.cs similarity index 80% rename from Content.Server/Administration/Systems/AdminGunSystem.cs rename to Content.Shared/Administration/Systems/AdminGunSystem.cs index 6270481a3c3..28ff1427098 100644 --- a/Content.Server/Administration/Systems/AdminGunSystem.cs +++ b/Content.Shared/Administration/Systems/AdminGunSystem.cs @@ -1,7 +1,7 @@ -using Content.Server.Administration.Components; +using Content.Shared.Administration.Components; using Content.Shared.Weapons.Ranged.Events; -namespace Content.Server.Administration.Systems; +namespace Content.Shared.Administration.Systems; public sealed class AdminGunSystem : EntitySystem { diff --git a/Content.Shared/Administration/Systems/RejuvenateSystem.cs b/Content.Shared/Administration/Systems/RejuvenateSystem.cs new file mode 100644 index 00000000000..d27ea63aced --- /dev/null +++ b/Content.Shared/Administration/Systems/RejuvenateSystem.cs @@ -0,0 +1,14 @@ +using Content.Shared.Rejuvenate; + +namespace Content.Shared.Administration.Systems; + +public sealed class RejuvenateSystem : EntitySystem +{ + /// + /// Fully heals the target, removing all damage, debuffs or other negative status effects. + /// + public void PerformRejuvenate(EntityUid target) + { + RaiseLocalEvent(target, new RejuvenateEvent()); + } +} diff --git a/Content.Shared/Administration/Systems/SharedBufferingSystem.cs b/Content.Shared/Administration/Systems/SharedBufferingSystem.cs new file mode 100644 index 00000000000..a1f375ebfd6 --- /dev/null +++ b/Content.Shared/Administration/Systems/SharedBufferingSystem.cs @@ -0,0 +1,5 @@ +namespace Content.Shared.Administration.Systems; + +public abstract class SharedBufferingSystem : EntitySystem +{ +} diff --git a/Content.Shared/Alert/AlertPrototype.cs b/Content.Shared/Alert/AlertPrototype.cs index 998eb4bd4d8..ce5f919ea5d 100644 --- a/Content.Shared/Alert/AlertPrototype.cs +++ b/Content.Shared/Alert/AlertPrototype.cs @@ -1,4 +1,5 @@ using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; namespace Content.Shared.Alert; @@ -7,8 +8,17 @@ namespace Content.Shared.Alert; /// An alert popup with associated icon, tooltip, and other data. /// [Prototype] -public sealed partial class AlertPrototype : IPrototype +public sealed partial class AlertPrototype : IPrototype, IInheritingPrototype { + /// + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + /// + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; private set; } + /// /// Type of alert, no 2 alert prototypes should have the same one. /// diff --git a/Content.Shared/Alert/AlertsSystem.cs b/Content.Shared/Alert/AlertsSystem.cs index 834a22b8deb..5c751b938fc 100644 --- a/Content.Shared/Alert/AlertsSystem.cs +++ b/Content.Shared/Alert/AlertsSystem.cs @@ -11,15 +11,13 @@ public abstract class AlertsSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - private EntityQuery _alertsQuery; + [Dependency] private readonly EntityQuery _alertsQuery = default!; private FrozenDictionary, AlertPrototype> _typeToAlert = default!; public override void Initialize() { base.Initialize(); - _alertsQuery = GetEntityQuery(); - SubscribeLocalEvent(HandleComponentStartup); SubscribeLocalEvent(HandleComponentShutdown); SubscribeLocalEvent(OnPlayerAttached); diff --git a/Content.Server/Anomaly/Components/AnomalousParticleComponent.cs b/Content.Shared/Anomaly/Components/AnomalousParticleComponent.cs similarity index 95% rename from Content.Server/Anomaly/Components/AnomalousParticleComponent.cs rename to Content.Shared/Anomaly/Components/AnomalousParticleComponent.cs index 5b05522bb9a..47c49956c99 100644 --- a/Content.Server/Anomaly/Components/AnomalousParticleComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalousParticleComponent.cs @@ -1,7 +1,4 @@ -using Content.Shared.Anomaly; -using Content.Shared.Anomaly.Components; - -namespace Content.Server.Anomaly.Components; +namespace Content.Shared.Anomaly.Components; /// /// This is used for projectiles which affect anomalies through colliding with them. diff --git a/Content.Shared/Anomaly/Components/AnomalyComponent.cs b/Content.Shared/Anomaly/Components/AnomalyComponent.cs index fb281ca9a33..47320ece03d 100644 --- a/Content.Shared/Anomaly/Components/AnomalyComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalyComponent.cs @@ -324,3 +324,10 @@ public sealed partial class AnomalyComponent : Component /// [ByRefEvent] public readonly record struct AnomalyBehaviorChangedEvent(EntityUid Anomaly, ProtoId? Old, ProtoId? New); + +/// +/// Event of anomaly being affected by exotic particle. +/// Is raised when particle collides with artifact. +/// +[ByRefEvent] +public record struct AnomalyAffectedByParticleEvent(EntityUid Anomaly, EntityUid Particle); diff --git a/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs b/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs index c49743f6304..842445b704b 100644 --- a/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs @@ -29,4 +29,10 @@ public sealed partial class AnomalyScannerComponent : Component /// [DataField] public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg"); + + /// + /// Whether to ignore the secret data on the anomaly. + /// + [DataField] + public bool IgnoreSecret; } diff --git a/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs b/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs index dfc1d561c46..c672c52dcf1 100644 --- a/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs +++ b/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Anomaly.Effects; -using Content.Shared.Body.Prototypes; using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Audio; using Robust.Shared.GameStates; diff --git a/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs b/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs index e48e0ae6a73..c05a3f6e41c 100644 --- a/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs +++ b/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs @@ -61,5 +61,5 @@ public sealed partial class ElectricityAnomalyComponent : Component /// Duration of devices being disabled by the emp pulse upon going supercritical. /// [DataField, ViewVariables(VVAccess.ReadWrite)] - public float EmpDisabledDuration = 60f; + public TimeSpan EmpDisabledDuration = TimeSpan.FromSeconds(60); } diff --git a/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs b/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs index 4c3cdb01465..0b2c4b7a16d 100644 --- a/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs +++ b/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs @@ -17,6 +17,8 @@ public abstract class SharedGravityAnomalySystem : EntitySystem [Dependency] private readonly SharedTransformSystem _xform = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly EntityQuery _physQuery = default!; + /// public override void Initialize() { @@ -30,17 +32,15 @@ private void OnAnomalyPulse(EntityUid uid, GravityAnomalyComponent component, re var range = component.MaxThrowRange * args.Severity * args.PowerModifier; var strength = component.MaxThrowStrength * args.Severity * args.PowerModifier; var lookup = _lookup.GetEntitiesInRange(uid, range, LookupFlags.Dynamic | LookupFlags.Sundries); - var xformQuery = GetEntityQuery(); - var worldPos = _xform.GetWorldPosition(xform, xformQuery); - var physQuery = GetEntityQuery(); + var worldPos = _xform.GetWorldPosition(xform); foreach (var ent in lookup) { - if (physQuery.TryGetComponent(ent, out var phys) + if (_physQuery.TryGetComponent(ent, out var phys) && (phys.CollisionMask & (int) CollisionGroup.GhostImpassable) != 0) continue; - var foo = _xform.GetWorldPosition(ent, xformQuery) - worldPos; + var foo = _xform.GetWorldPosition(ent) - worldPos; _throwing.TryThrow(ent, foo * 10, strength, uid, 0); } } @@ -64,16 +64,14 @@ private void OnSupercritical(EntityUid uid, GravityAnomalyComponent component, r var range = component.MaxThrowRange * 2 * args.PowerModifier; var strength = component.MaxThrowStrength * 2 * args.PowerModifier; var lookup = _lookup.GetEntitiesInRange(uid, range, LookupFlags.Dynamic | LookupFlags.Sundries); - var xformQuery = GetEntityQuery(); - var physQuery = GetEntityQuery(); foreach (var ent in lookup) { - if (physQuery.TryGetComponent(ent, out var phys) + if (_physQuery.TryGetComponent(ent, out var phys) && (phys.CollisionMask & (int) CollisionGroup.GhostImpassable) != 0) continue; - var foo = _xform.GetWorldPosition(ent, xformQuery) - worldPos; + var foo = _xform.GetWorldPosition(ent) - worldPos; _throwing.TryThrow(ent, foo * 5, strength, uid, 0); } } diff --git a/Content.Shared/Anomaly/SharedAnomalySystem.cs b/Content.Shared/Anomaly/SharedAnomalySystem.cs index f822556eb19..03aead97c5c 100644 --- a/Content.Shared/Anomaly/SharedAnomalySystem.cs +++ b/Content.Shared/Anomaly/SharedAnomalySystem.cs @@ -38,6 +38,8 @@ public abstract class SharedAnomalySystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly EntityQuery _physQuery = default!; + public override void Initialize() { base.Initialize(); @@ -409,7 +411,6 @@ private void SetScannerSupercritical(Entity anomalyEnt, bool v if (tilerefs.Count == 0) return null; - var physQuery = GetEntityQuery(); var resultList = new List(); while (resultList.Count < amount) { @@ -431,10 +432,13 @@ private void SetScannerSupercritical(Entity anomalyEnt, bool v if (!settings.CanSpawnOnEntities) { + // If it can't spawn on entities, ensure that maximum one entity will be spawned here this pulse. + tilerefs.Remove(tileref); + var valid = true; foreach (var ent in _map.GetAnchoredEntities(xform.GridUid.Value, grid, tileref.GridIndices)) { - if (!physQuery.TryGetComponent(ent, out var body)) + if (!_physQuery.TryGetComponent(ent, out var body)) continue; if (body.BodyType != BodyType.Static || @@ -447,7 +451,6 @@ private void SetScannerSupercritical(Entity anomalyEnt, bool v } if (!valid) { - tilerefs.Remove(tileref); continue; } } @@ -486,7 +489,7 @@ public AnomalyStabilityVisuals GetStabilityVisualOrStable(Entity /// should entities block spawning? diff --git a/Content.Shared/Armor/SharedArmorSystem.cs b/Content.Shared/Armor/SharedArmorSystem.cs index 1ff1bbc0733..6a3db3184df 100644 --- a/Content.Shared/Armor/SharedArmorSystem.cs +++ b/Content.Shared/Armor/SharedArmorSystem.cs @@ -1,4 +1,6 @@ -using Content.Shared.Damage; +using Content.Shared.Clothing.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Inventory; using Content.Shared.Silicons.Borgs; @@ -32,6 +34,9 @@ public override void Initialize() /// The event, contains the running count of armor percentage as a coefficient private void OnCoefficientQuery(Entity ent, ref InventoryRelayedEvent args) { + if (TryComp(ent, out var mask) && mask.IsToggled) + return; + foreach (var armorCoefficient in ent.Comp.Modifiers.Coefficients) { args.Args.DamageModifiers.Coefficients[armorCoefficient.Key] = args.Args.DamageModifiers.Coefficients.TryGetValue(armorCoefficient.Key, out var coefficient) ? coefficient * armorCoefficient.Value : armorCoefficient.Value; @@ -40,12 +45,18 @@ private void OnCoefficientQuery(Entity ent, ref InventoryRelayed private void OnDamageModify(EntityUid uid, ArmorComponent component, InventoryRelayedEvent args) { + if (TryComp(uid, out var mask) && mask.IsToggled) + return; + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, component.Modifiers); } private void OnBorgDamageModify(EntityUid uid, ArmorComponent component, ref BorgModuleRelayedEvent args) { + if (TryComp(uid, out var mask) && mask.IsToggled) + return; + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, component.Modifiers); } diff --git a/Content.Shared/Atmos/AtmosExposedEvents.cs b/Content.Shared/Atmos/AtmosExposedEvents.cs new file mode 100644 index 00000000000..41b65fe24f2 --- /dev/null +++ b/Content.Shared/Atmos/AtmosExposedEvents.cs @@ -0,0 +1,57 @@ +using Robust.Shared.Map; + +namespace Content.Shared.Atmos; + +/// +/// Raised on entities that have an AtmosExposedComponent when AtmosphereSystem updates. +/// Exposure events are raised by AtmosphereSystem at some fixed interval. +/// +/// The coordinates of the entity that is being exposed. +/// The gas mixture that the entity is exposed to. +/// The xform of the entity that is being exposed. +[ByRefEvent] +public readonly struct AtmosExposedUpdateEvent( + EntityCoordinates coordinates, + GasMixture mixture, + TransformComponent transform) +{ + public readonly EntityCoordinates Coordinates = coordinates; + public readonly GasMixture GasMixture = mixture; + public readonly TransformComponent Transform = transform; +} + +/// +/// Event that tries to query the mixture a certain entity is exposed to. +/// This is mainly intended for use with entities inside of containers. +/// This event is not raised for entities that are directly parented to the grid. +/// +[ByRefEvent] +public struct AtmosExposedGetAirEvent +{ + /// + /// The entity we want to query this for. + /// + public readonly Entity Entity; + + /// + /// The mixture that the entity is exposed to. Output parameter. + /// + public GasMixture? Gas = null; + + /// + /// Whether to excite the mixture, if possible. + /// + public readonly bool Excite = false; + + /// + /// Whether this event has been handled or not. + /// Check this before changing anything. + /// + public bool Handled = false; + + public AtmosExposedGetAirEvent(Entity entity, bool excite = false) + { + Entity = entity; + Excite = excite; + } +} diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs index 7ece5614c25..eca6a59101f 100644 --- a/Content.Shared/Atmos/Atmospherics.cs +++ b/Content.Shared/Atmos/Atmospherics.cs @@ -19,6 +19,11 @@ public static class Atmospherics /// public const float OneAtmosphere = 101.325f; + /// + /// Global Atmospherics epsilon. Used for all general floating-point comparisons. + /// + public const float Epsilon = 0.5f; + /// /// Maximum external pressure (in kPA) a gas miner will, by default, output to. /// This is used to initialize roundstart atmos rooms. @@ -99,6 +104,21 @@ public static class Atmospherics public const float OxygenMolesGasMiner = MolesCellGasMiner * OxygenStandard; public const float NitrogenMolesGasMiner = MolesCellGasMiner * NitrogenStandard; + /// + /// Converts Grams to Kilograms. + /// + public const float gToKg = 0.001f; + + /// + /// Convert kPa to Kg/m^2 + /// + public const float kPaToKg_m2 = 0.00980665f; + + /// + /// Convert Kg/m^2 to kPa + /// + public const float Kg_m2TokPa = 101.9716212978f; + #endregion /// @@ -133,8 +153,19 @@ public static class Atmospherics /// public const float MinimumAirToSuspend = (MolesCellStandard * MinimumAirRatioToSuspend); - public const float MinimumTemperatureToMove = (T20C + 100f); + /// + /// The minimum difference in temperature between s + /// (s) required + /// for LINDA to report a pressure difference between them for space wind. + /// In Kelvin. + /// + public const float MinimumTemperatureToMove = 5f; + /// + /// The minimum difference in moles between s + /// (s) required for LINDA to + /// report a pressure difference between them for space wind. + /// public const float MinimumMolesDeltaToMove = (MolesCellStandard * MinimumAirRatioToMove); /// @@ -159,31 +190,12 @@ public static class Atmospherics public const float MinimumHeatCapacity = 0.0003f; /// - /// For the purposes of making space "colder" + /// Allows Atmospherics to cool down rooms during spacing + /// by assigning a fake heat capacity to space, + /// making space "actually cold" for gameplay reasons. /// public const float SpaceHeatCapacity = 7000f; - /// - /// Dictionary of chemical abbreviations for - /// - public static Dictionary GasAbbreviations = new Dictionary() - { - [Gas.Ammonia] = Loc.GetString("gas-ammonia-abbreviation"), - [Gas.CarbonDioxide] = Loc.GetString("gas-carbon-dioxide-abbreviation"), - [Gas.Frezon] = Loc.GetString("gas-frezon-abbreviation"), - [Gas.Nitrogen] = Loc.GetString("gas-nitrogen-abbreviation"), - [Gas.NitrousOxide] = Loc.GetString("gas-nitrous-oxide-abbreviation"), - [Gas.Oxygen] = Loc.GetString("gas-oxygen-abbreviation"), - [Gas.Plasma] = Loc.GetString("gas-plasma-abbreviation"), - [Gas.Tritium] = Loc.GetString("gas-tritium-abbreviation"), - [Gas.WaterVapor] = Loc.GetString("gas-water-vapor-abbreviation"), - [Gas.Thermonium] = Loc.GetString("gas-thermonium-abbreviation"), /// Imperial Added Thermonium - [Gas.Phazonium] = Loc.GetString("gas-phazonium-abbreviation"), /// Imperial Added Phazonium and Ozonium - [Gas.Ozonium] = Loc.GetString("gas-ozonium-abbreviation"), /// Imperial Added Phazonium and Ozonium - [Gas.Hydrogen] = Loc.GetString("gas-hydrogen-abbreviation"), /// Imperial Added Hydrogen - - }; - #region Excited Groups /// @@ -211,7 +223,7 @@ public static class Atmospherics /// /// Total number of gases. Increase this if you want to add more! /// - public const int TotalNumberOfGases = 13; /// Imperial Added Thermonium + public const int TotalNumberOfGases = 17; /// Imperial Added Thermonium /// /// This is the actual length of the gases arrays in mixtures. @@ -222,7 +234,7 @@ public static class Atmospherics /// /// Amount of heat released per mole of burnt hydrogen or tritium (hydrogen isotope) /// - public const float FireHydrogenEnergyReleased = 284e3f; // hydrogen is 284 kJ/mol + public const float FireHydrogenEnergyReleased = 284e3f; public const float FireMinimumTemperatureToExist = T0C + 100f; public const float FireMinimumTemperatureToSpread = T0C + 150f; public const float FireSpreadRadiosityScale = 0.85f; @@ -233,8 +245,8 @@ public static class Atmospherics public const float SuperSaturationEnds = SuperSaturationThreshold / 3; public const float OxygenBurnRateBase = 1.4f; - public const float PlasmaMinimumBurnTemperature = (100f+T0C); - public const float PlasmaUpperTemperature = (1370f+T0C); + public const float PlasmaMinimumBurnTemperature = 100f + T0C; + public const float PlasmaUpperTemperature = 1370f + T0C; public const float PlasmaOxygenFullburn = 10f; public const float PlasmaBurnRateDelta = 9f; @@ -245,15 +257,14 @@ public static class Atmospherics public const float TritiumBurnOxyFactor = 100f; public const float TritiumBurnTritFactor = 10f; + public const float TritiumBurnFuelRatio = 2f; /// Imperial Added Hydrogen start public const float MinimumHydrogenOxyburnEnergy = 143000f; public const float HydrogenBurnOxyFactor = 100f; public const float HydrogenBurnHydrFactor = 10f; - public const float HydrogenProductionMaxEfficiencyTemperature = 2273f; - public const float HydrogenProductionConversionRate = 2273f; - public const float N2OzonProductionMaxEfficiencyTemperature = 2273f; - public const float N2OzonProductionConversionRate = 2273f; + public const float HydrogenProductionMaxEfficiencyTemperature = 227f; + public const float HydrogenProductionConversionRate = 220f; /// Imperial Added Hydrogen end public const float FrezonCoolLowerTemperature = 23.15f; @@ -282,7 +293,7 @@ public static class Atmospherics /// /// 1 mol of Tritium is required per X mol of oxygen. /// - public const float FrezonProductionTritRatio = 8.0f; + public const float FrezonProductionTritRatio = 50.0f; /// /// 1 / X of the tritium is converted into Frezon each tick @@ -311,6 +322,14 @@ public static class Atmospherics public const float OzoniumProductionMaxEfficiencyTemperature = 33.15f; /// Imperial Added Phazonium and Ozonium end + /// Imperial Imperial Atmos Update start + public const float DeuteriumProductionConversionRate = 220f; + public const float DeuteriumProductionMaxEfficiencyTemperature = 1400.15f; + public const float DeuteriumProductionMaxCapacity = 7000f; + public const float MinimumAntiNoblium = 5f; + public const float HydrogenMaxTransferRate = 650; + public const float HydrogenMaxOutputPressure = 9000; + /// Imperial Imperial Atmos Update end /// /// Determines at what pressure the ultra-high pressure red icon is displayed. @@ -401,6 +420,10 @@ public enum Gas : sbyte Thermonium = 9, /// Imperial Added Thermonium Phazonium = 10, /// Imperial Added Phazonium and Ozonium Ozonium = 11, /// Imperial Added Phazonium and Ozonium - Hydrogen = 12 /// Imperial Added Hydrogen + Hydrogen = 12, /// Imperial Added Hydrogen + Deuterium = 13, // Imperial Imperial Atmos Update start + HyperNoblium = 14, + AntiNoblium = 15, + BZ = 16 // Imperial Imperial Atmos Update end } } diff --git a/Content.Server/Atmos/Piping/Components/AtmosDeviceComponent.cs b/Content.Shared/Atmos/Components/AtmosDeviceComponent.cs similarity index 95% rename from Content.Server/Atmos/Piping/Components/AtmosDeviceComponent.cs rename to Content.Shared/Atmos/Components/AtmosDeviceComponent.cs index 5b55d9d3b67..2226f44981c 100644 --- a/Content.Server/Atmos/Piping/Components/AtmosDeviceComponent.cs +++ b/Content.Shared/Atmos/Components/AtmosDeviceComponent.cs @@ -1,7 +1,4 @@ -using Content.Server.Atmos.Components; -using Content.Shared.Atmos.Components; - -namespace Content.Server.Atmos.Piping.Components; +namespace Content.Shared.Atmos.Components; /// /// Component for atmos devices which are updated in line with atmos, as part of a diff --git a/Content.Server/Atmos/Components/DeltaPressureComponent.cs b/Content.Shared/Atmos/Components/DeltaPressureComponent.cs similarity index 82% rename from Content.Server/Atmos/Components/DeltaPressureComponent.cs rename to Content.Shared/Atmos/Components/DeltaPressureComponent.cs index 8031cd876da..0336abaa987 100644 --- a/Content.Server/Atmos/Components/DeltaPressureComponent.cs +++ b/Content.Shared/Atmos/Components/DeltaPressureComponent.cs @@ -1,52 +1,50 @@ -using Content.Server.Atmos.EntitySystems; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Content.Shared.Guidebook; +using Robust.Shared.GameStates; -namespace Content.Server.Atmos.Components; +namespace Content.Shared.Atmos.Components; /// /// Entities that have this component will have damage done to them depending on the local pressure /// environment that they reside in. /// /// Atmospherics.DeltaPressure batch-processes entities with this component in a list on -/// the grid's . +/// the grid's GridAtmosphereComponent. /// The entities are automatically added and removed from this list, and automatically /// added on initialization. /// -/// Note that the entity should have an and be a grid structure. +/// Note that the entity should have an AirtightComponent and be a grid structure. [RegisterComponent] +[NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedAtmosphereSystem), typeof(SharedDeltaPressureSystem))] public sealed partial class DeltaPressureComponent : Component { /// - /// Whether the entity is currently in the processing list of the grid's . + /// Whether the entity is currently in the processing list of the grid's GridAtmosphereComponent. /// [DataField(readOnly: true)] [ViewVariables(VVAccess.ReadOnly)] - [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))] public bool InProcessingList; /// /// Whether this entity is currently taking damage from pressure. /// - [DataField(readOnly: true)] - [ViewVariables(VVAccess.ReadOnly)] - [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))] + [DataField, AutoNetworkedField] public bool IsTakingDamage; - /// - /// The current cached position of this entity on the grid. - /// Updated via MoveEvent. - /// - [DataField(readOnly: true)] - public Vector2i CurrentPosition = Vector2i.Zero; - /// /// The grid this entity is currently joined to for processing. /// Required for proper deletion, as we cannot reference the grid /// for removal while the entity is being deleted. /// - [DataField] + /// Note that while AirtightComponent already stores the grid, + /// we cannot trust it to be available on init or when the entity is being deleted. Tragic. + /// Double note: this is set during ComponentInit and thus does not need to be a datafield + /// or else it will spam serialization. + /// TODO ATMOS: Simply use AirtightComponent's GridUID caching and handle entity removal from the processing list on an invalidation system similar to InvalidTiles. + [ViewVariables(VVAccess.ReadOnly)] public EntityUid? GridUid; /// @@ -69,7 +67,7 @@ public sealed partial class DeltaPressureComponent : Component [DataField] public DamageSpecifier BaseDamage = new() { - DamageDict = new Dictionary + DamageDict = new () { { "Structural", 10 }, }, diff --git a/Content.Shared/Atmos/Components/FlammableComponent.cs b/Content.Shared/Atmos/Components/FlammableComponent.cs index acfb9e25401..aa1d361fa02 100644 --- a/Content.Shared/Atmos/Components/FlammableComponent.cs +++ b/Content.Shared/Atmos/Components/FlammableComponent.cs @@ -3,14 +3,40 @@ using Robust.Shared.GameStates; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Atmos.Components { - [RegisterComponent, NetworkedComponent] + [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] // TODO: Access restriction so that other systems have to use the API to modify fire stacks public sealed partial class FlammableComponent : Component { + /// + /// Is the mob currently resisting being on fire + /// (i.e. throwing themselves onto the ground to extinguish themselves)? + /// + [ViewVariables] + public bool Resisting => ResistCompleteTime.HasValue; + + /// + /// The time stamp at which the active resist will be over and the mob's flame stacks will be reduced. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? ResistCompleteTime; + + /// + /// Timestamp for the next update. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextUpdate; + + /// + /// The time resisting being on fire will take. + /// The mob will be paralyzed for this duration. + /// [DataField] - public bool Resisting; + public TimeSpan ResistTime = TimeSpan.FromSeconds(2); [ViewVariables(VVAccess.ReadWrite)] [DataField] diff --git a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs index 115cb548929..cc3914fe677 100644 --- a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs +++ b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs @@ -1,101 +1,103 @@ using Robust.Shared.GameStates; -using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Atmos.Components; +/// +/// Used for gas analyzers, an item that shows players the gas contents of an atmos +/// device they use it on or of the tile they are standing on. +/// [RegisterComponent, NetworkedComponent] public sealed partial class GasAnalyzerComponent : Component { - [ViewVariables] + /// + /// The target entity currently being analyzed. + /// + [DataField] public EntityUid? Target; - [ViewVariables] - public EntityUid User; + /// + /// The current user of the gas analyzer. + /// + [DataField] + public EntityUid? User; - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + /// + /// Is the analyzer currently active? + /// + [DataField] public bool Enabled; +} - [Serializable, NetSerializable] - public enum GasAnalyzerUiKey - { - Key, - } +/// +/// Atmospheric data is gathered in the system and sent to the user. +/// +[Serializable, NetSerializable] +public sealed class GasAnalyzerUserMessage(GasMixEntry[] nodeGasMixes, string deviceName, NetEntity deviceUid, bool deviceFlipped) : BoundUserInterfaceMessage +{ + public string DeviceName = deviceName; + public NetEntity DeviceUid = deviceUid; + public bool DeviceFlipped = deviceFlipped; + public GasMixEntry[] NodeGasMixes = nodeGasMixes; +} +/// +/// Contains information on a gas mix entry, turns into a tab in the UI. +/// +[Serializable, NetSerializable] +public readonly record struct GasMixEntry(string Name, float Volume, float Pressure, float Temperature, GasEntry[]? Gases = null) +{ /// - /// Atmospheric data is gathered in the system and sent to the user + /// Name of the tab in the UI. /// - [Serializable, NetSerializable] - public sealed class GasAnalyzerUserMessage : BoundUserInterfaceMessage - { - public string DeviceName; - public NetEntity DeviceUid; - public bool DeviceFlipped; - public string? Error; - public GasMixEntry[] NodeGasMixes; - public GasAnalyzerUserMessage(GasMixEntry[] nodeGasMixes, string deviceName, NetEntity deviceUid, bool deviceFlipped, string? error = null) - { - NodeGasMixes = nodeGasMixes; - DeviceName = deviceName; - DeviceUid = deviceUid; - DeviceFlipped = deviceFlipped; - Error = error; - } - } - + public readonly string Name = Name; /// - /// Contains information on a gas mix entry, turns into a tab in the UI + /// Volume of this gas mixture. /// - [Serializable, NetSerializable] - public struct GasMixEntry - { - /// - /// Name of the tab in the UI - /// - public readonly string Name; - public readonly float Volume; - public readonly float Pressure; - public readonly float Temperature; - public readonly GasEntry[]? Gases; - - public GasMixEntry(string name, float volume, float pressure, float temperature, GasEntry[]? gases = null) - { - Name = name; - Volume = volume; - Pressure = pressure; - Temperature = temperature; - Gases = gases; - } - } - + public readonly float Volume = Volume; /// - /// Individual gas entry data for populating the UI + /// Pressure of this gas mixture. /// - [Serializable, NetSerializable] - public struct GasEntry - { - public readonly string Name; - public readonly float Amount; - public readonly string Color; + public readonly float Pressure = Pressure; + /// + /// Temperature of this gas mixture. + /// + public readonly float Temperature = Temperature; + /// + /// The gases contained in this gas mixture. + /// The gases below a certain mol threshold are not included. + /// + public readonly GasEntry[]? Gases = Gases; +} - public GasEntry(string name, float amount, string color) - { - Name = name; - Amount = amount; - Color = color; - } +/// +/// Individual gas entry data for populating the UI. +/// +[Serializable, NetSerializable] +public readonly record struct GasEntry(Gas Gas, float Amount) +{ + /// + /// The gas this entry represents. + /// + public readonly Gas Gas = Gas; + /// + /// The gas amount in mol. + /// + public readonly float Amount = Amount; +} - public override string ToString() - { - // e.g. "Plasma: 2000 mol" - return Loc.GetString( - "gas-entry-info", - ("gasName", Name), - ("gasAmount", Amount)); - } - } +/// +/// Key for the GasAnalyzerBoundUserInterface. +/// +[Serializable, NetSerializable] +public enum GasAnalyzerUiKey +{ + Key, } +/// +/// Individual gas entry data for populating the UI +/// [Serializable, NetSerializable] public enum GasAnalyzerVisuals : byte { diff --git a/Content.Shared/Atmos/Components/GasMaxPressureHolderComponent.cs b/Content.Shared/Atmos/Components/GasMaxPressureHolderComponent.cs new file mode 100644 index 00000000000..573d3cbdf2e --- /dev/null +++ b/Content.Shared/Atmos/Components/GasMaxPressureHolderComponent.cs @@ -0,0 +1,83 @@ +using Robust.Shared.Audio; + +namespace Content.Shared.Atmos.Components; + +/// +/// An atmos device that can hold and release gas. Such as a Gas Tank or Gas Canister. +/// Abstract as both of these devices share a lot of similar behavior. +/// +public abstract partial class GasMaxPressureHolderComponent : Component, IGasMaxPressureHolder +{ + private const float DefaultIntegrity = 3f; + private const float DefaultOutputPressure = Atmospherics.OneAtmosphere; + + /// + /// Minimum release pressure possible for the release valve. + /// + [DataField, AutoNetworkedField] + public float MinReleasePressure { get; set; } = DefaultOutputPressure / 10; + + /// + /// Maximum release pressure possible for the release valve. + /// + [DataField, AutoNetworkedField] + public float MaxReleasePressure { get; set; } = 3 * DefaultOutputPressure; + + /// + /// Current Valve release pressure. + /// + [DataField, AutoNetworkedField] + public float ReleasePressure { get; set; } = DefaultOutputPressure; + + /// + /// Whether the release valve is open on this device. + /// + [DataField, AutoNetworkedField] + public bool ReleaseValveOpen { get; set; } + + /// + /// Sound made when the valve on this device is opened or closed. + /// + [DataField] + public SoundSpecifier ValveSound = + new SoundCollectionSpecifier("valveSqueak") + { + Params = AudioParams.Default.WithVolume(-5f), + }; + + /// + /// Sound made when air is leaked out of this device. + /// + [DataField] + public SoundSpecifier? ReleaseSound { get; set; } = new SoundPathSpecifier("/Audio/Effects/spray.ogg") + { + Params = AudioParams.Default.WithVolume(-5f), + }; + + /// + /// The mixture of air contained in this device. + /// + [DataField, AutoNetworkedField] + public GasMixture Air { get; set; } + + // TODO ATMOS: Proper loud BANG sound, these are lethal concussive blast waves + [DataField] + public SoundSpecifier? RuptureSound { get; set; } = new SoundPathSpecifier("/Audio/Effects/spray.ogg"); + + // The values chosen for safety and overpressure are arbitrary, if reactions change feel free to change these to whatever is most fun! + // Generally a lower overpressure value and lower safety pressure mean weaker bombs with shorter fuses. + [DataField] + public float SafetyPressure { get; set; } = 15 * Atmospherics.OneAtmosphere; + + [DataField] + public float Overpressure { get; set; } = 20 * Atmospherics.OneAtmosphere; + + [DataField] + public LocId? SafetyAlert { get; set; } = "gas-max-pressure-alert"; + + [DataField] + public float MaxIntegrity { get; set; } = DefaultIntegrity; + + [DataField] + public float Integrity { get; set; } = DefaultIntegrity; +} diff --git a/Content.Shared/Atmos/Components/GasTankComponent.cs b/Content.Shared/Atmos/Components/GasTankComponent.cs index 7ca60985557..31fa3a0d5a9 100644 --- a/Content.Shared/Atmos/Components/GasTankComponent.cs +++ b/Content.Shared/Atmos/Components/GasTankComponent.cs @@ -5,18 +5,12 @@ namespace Content.Shared.Atmos.Components; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] -public sealed partial class GasTankComponent : Component, IGasMixtureHolder +public sealed partial class GasTankComponent : GasMaxPressureHolderComponent { - public const float MaxExplosionRange = 26f; - private const float DefaultLowPressure = 0f; - private const float DefaultOutputPressure = Atmospherics.OneAtmosphere; + private const float DefaultLowPressure = Atmospherics.OneAtmosphere; - public int Integrity = 3; public bool IsLowPressure => Air.Pressure <= TankLowPressure; - [DataField] - public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg"); - [DataField] public SoundSpecifier? ConnectSound = new SoundPathSpecifier("/Audio/Effects/internals.ogg") @@ -32,27 +26,12 @@ public sealed partial class GasTankComponent : Component, IGasMixtureHolder public EntityUid? ConnectStream; public EntityUid? DisconnectStream; - [DataField] - public GasMixture Air { get; set; } = new(); - /// /// Pressure at which tank should be considered 'low' such as for internals. /// [DataField] public float TankLowPressure = DefaultLowPressure; - /// - /// Distributed pressure. - /// - [DataField, AutoNetworkedField] - public float OutputPressure = DefaultOutputPressure; - - /// - /// The maximum allowed output pressure. - /// - [DataField] - public float MaxOutputPressure = 3 * DefaultOutputPressure; - /// /// Tank is connected to internals. /// @@ -69,52 +48,9 @@ public sealed partial class GasTankComponent : Component, IGasMixtureHolder [ViewVariables] public bool CheckUser; - /// - /// Pressure at which tanks start leaking. - /// - [DataField] - public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere; - - /// - /// Pressure at which tank spills all contents into atmosphere. - /// - [DataField] - public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere; - - /// - /// Base 3x3 explosion. - /// - [DataField] - public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere; - - /// - /// Increases explosion for each scale kPa above threshold. - /// - [DataField] - public float TankFragmentScale = 2.25f * Atmospherics.OneAtmosphere; - [DataField] public EntProtoId ToggleAction = "ActionToggleInternals"; [DataField, AutoNetworkedField] public EntityUid? ToggleActionEntity; - - /// - /// Valve to release gas from tank - /// - [DataField, AutoNetworkedField] - public bool IsValveOpen; - - /// - /// Gas release rate in L/s - /// - [DataField, AutoNetworkedField] - public float ValveOutputRate = 100f; - - [DataField] - public SoundSpecifier ValveSound = - new SoundCollectionSpecifier("valveSqueak") - { - Params = AudioParams.Default.WithVolume(-5f), - }; } diff --git a/Content.Shared/Atmos/Components/GridAtmosphereComponent.cs b/Content.Shared/Atmos/Components/GridAtmosphereComponent.cs new file mode 100644 index 00000000000..f633947eea1 --- /dev/null +++ b/Content.Shared/Atmos/Components/GridAtmosphereComponent.cs @@ -0,0 +1,155 @@ +using System.Collections.Concurrent; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Serialization; +using Content.Shared.NodeContainer.NodeGroups; + +namespace Content.Shared.Atmos.Components; + +/// +/// Internal class for storing all grid data. +/// If you need to access this data, use the API methods in the /server +/// instead of trying to scour this component or others for the data you need. +/// +[RegisterComponent, Serializable, + Access(typeof(SharedAtmosphereSystem), typeof(SharedGasTileOverlaySystem), typeof(SharedAtmosDebugOverlaySystem))] +public sealed partial class GridAtmosphereComponent : Component +{ + /// + /// Whether the grid is being updated by Atmospherics. + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool Simulated = true; + + /// + /// Indicator for if Atmospherics has delegated the processing of this + /// grid to another tick due to the time budget running out. + /// + /// If true, Atmospherics is not finished + /// processing the current stage and has yielded processing + /// to the next tick. + [ViewVariables] + public bool ProcessingPaused; + + /// + /// Timer used to delay processing for every AtmosTick. + /// No, Atmospherics cannot tick every frame. + /// + /// TODO: Replace with TimeSpan please. + [ViewVariables] + public float Timer; + + /// + /// Integer that is incremented every time the grid is processed by Atmospherics. + /// Used in multiple subsystems to prevent double-copy/processing of data. + /// + /// Do not set to zero by default. + /// You will break roundstart atmos otherwise. + [ViewVariables] + public int UpdateCounter = 1; + + [ViewVariables] + [IncludeDataField(customTypeSerializer:typeof(TileAtmosCollectionSerializer))] + public Dictionary Tiles = new(1000); + + [ViewVariables] + public HashSet MapTiles = new(1000); + + [ViewVariables] + public readonly HashSet ActiveTiles = new(1000); + + [ViewVariables] + public int ActiveTilesCount => ActiveTiles.Count; + + [ViewVariables] + public readonly HashSet ExcitedGroups = new(1000); + + [ViewVariables] + public int ExcitedGroupCount => ExcitedGroups.Count; + + [ViewVariables] + public readonly HashSet HotspotTiles = new(1000); + + [ViewVariables] + public int HotspotTilesCount => HotspotTiles.Count; + + [ViewVariables] + public readonly HashSet SuperconductivityTiles = new(1000); + + [ViewVariables] + public int SuperconductivityTilesCount => SuperconductivityTiles.Count; + + [ViewVariables] + public HashSet HighPressureDelta = new(1000); + + [ViewVariables] + public int HighPressureDeltaCount => HighPressureDelta.Count; + + /// + /// A list of entities that have a and are to + /// be processed by the , if enabled. + /// + /// To prevent massive bookkeeping overhead, this list is processed in-place, + /// with add/remove/find operations helped via a dict. + /// + /// If you want to add/remove/find entities in this list, + /// use the API methods in the Atmospherics API. + [ViewVariables] + public readonly List> DeltaPressureEntities = + new(SharedAtmosphereSystem.DeltaPressurePreAllocateLength); + + /// + /// An index lookup for the list. + /// Used for add/remove/find operations to speed up processing. + /// + public readonly Dictionary DeltaPressureEntityLookup = + new(SharedAtmosphereSystem.DeltaPressurePreAllocateLength); + + /// + /// Integer that indicates the current position in the + /// list that is being processed. + /// + [ViewVariables(VVAccess.ReadOnly)] + public int DeltaPressureCursor; + + /// + /// Queue of entities that need to have damage applied to them. + /// + [ViewVariables] + public readonly ConcurrentQueue DeltaPressureDamageResults = new(); + + [ViewVariables] + public readonly HashSet PipeNets = new(); + + [ViewVariables] + public readonly HashSet> AtmosDevices = new(); + + [ViewVariables] + public readonly Queue CurrentRunTiles = new(); + + [ViewVariables] + public readonly Queue CurrentRunExcitedGroups = new(); + + [ViewVariables] + public readonly Queue CurrentRunPipeNet = new(); + + [ViewVariables] + public readonly Queue> CurrentRunAtmosDevices = new(); + + [ViewVariables] + public readonly HashSet InvalidatedCoords = new(1000); + + [ViewVariables] + public readonly Queue CurrentRunInvalidatedTiles = new(); + + [ViewVariables] + public readonly List PossiblyDisconnectedTiles = new(100); + + [ViewVariables] + public int InvalidatedCoordsCount => InvalidatedCoords.Count; + + [ViewVariables] + public long EqualizationQueueCycleControl { get; set; } + + [ViewVariables] + public AtmosphereProcessingState State { get; set; } = AtmosphereProcessingState.Revalidate; +} diff --git a/Content.Shared/Atmos/Components/IGasMaxPressureHolder.cs b/Content.Shared/Atmos/Components/IGasMaxPressureHolder.cs new file mode 100644 index 00000000000..e05d80c96ca --- /dev/null +++ b/Content.Shared/Atmos/Components/IGasMaxPressureHolder.cs @@ -0,0 +1,44 @@ +using Robust.Shared.Audio; + +namespace Content.Shared.Atmos.Components; + +/// +/// This is an interface meant to designate an atmos device which can hold, a maximum pressurized volume of gas. +/// This device may fail if it exceeds the maximum pressure for too long. +/// +public interface IGasMaxPressureHolder : IGasMixtureHolder +{ + /// + /// Sound made when this device is destroyed from its reaching 0. + /// + SoundSpecifier? RuptureSound { get; set; } + + /// + /// Maximum pressure at which this atmos device will activate any emergency safety features, if it has any. + /// + float SafetyPressure { get; set; } + + /// + /// Maximum pressure this device can handle before it starts losing . + /// + float Overpressure { get; set; } + + /// + /// Popup alert for when this entity's pressure exceeds max pressure. + /// + LocId? SafetyAlert { get; set; } + + /// + /// How many over-pressures until this gas tank detonates. + /// An overpressure is defined as pressure exceeding + /// This determines the maximum value + /// + float MaxIntegrity { get; set; } + + /// + /// How many over-pressures until this gas tank detonates. + /// An overpressure is defined as pressure exceeding + /// This determines the current value + /// + float Integrity { get; set; } +} diff --git a/Content.Shared/Atmos/Components/MapAtmosphereComponent.cs b/Content.Shared/Atmos/Components/MapAtmosphereComponent.cs new file mode 100644 index 00000000000..37014ebfddb --- /dev/null +++ b/Content.Shared/Atmos/Components/MapAtmosphereComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Atmos.EntitySystems; + +namespace Content.Shared.Atmos.Components; + +/// +/// Component that defines the default GasMixture for a map. +/// +[RegisterComponent, Access(typeof(SharedAtmosphereSystem))] +public sealed partial class MapAtmosphereComponent : SharedMapAtmosphereComponent +{ + /// + /// The default GasMixture a map will have. Space mixture by default. + /// + [DataField] + public GasMixture Mixture = GasMixture.SpaceGas; + + /// + /// Whether empty tiles will be considered space or not. + /// + [DataField] + public bool Space = true; + + public SharedGasTileOverlaySystem.GasOverlayData Overlay; +} diff --git a/Content.Shared/Atmos/EntitySystems/GasMaxPressureSystem.cs b/Content.Shared/Atmos/EntitySystems/GasMaxPressureSystem.cs new file mode 100644 index 00000000000..31e6a4f6c42 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/GasMaxPressureSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.CCVar; +using Content.Shared.Destructible; +using Content.Shared.Explosion.EntitySystems; +using Content.Shared.Jittering; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.Serialization; + +namespace Content.Shared.Atmos.EntitySystems; + +/// +/// This handles gas volumes that have a maximum pressure, and the destructive results of them exceeding that pressure. +/// You may call it the "MaxCapSystem" if you so desire. +/// +public abstract class GasMaxPressureSystem : EntitySystem where T : IGasMaxPressureHolder, IComponent +{ + private float _maxExplosivePower; + + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] protected readonly SharedAtmosphereSystem Atmos = default!; + [Dependency] private readonly SharedDestructibleSystem _destructible = default!; + [Dependency] private readonly SharedExplosionSystem _explosions = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDeviceUpdated); + + Subs.CVar(_cfg, CCVars.AtmosTankFragment, value => _maxExplosivePower = value, true); + } + + private void OnDeviceUpdated(Entity entity, ref AtmosDeviceUpdateEvent args) + { + // We don't update our atmos device if it's in the process of being deleted. + if (CheckStatus(entity, args.dt)) + DeviceUpdated(entity, ref args); + } + + /// + /// Handler for our atmos device being updated. + /// + /// Gas holding atmos device. + /// + protected abstract void DeviceUpdated(Entity entity, ref AtmosDeviceUpdateEvent args); + + /// + /// Handler for when this atmos device is about to break due to exceeding its maximum pressure too many times + /// + /// Gas holding atmos device. + protected virtual void BeforeDeviceFailure(Entity entity) + { + + } + + /// + /// Handler for when this atmos device loses integrity due to overpressure + /// + /// Gas holding atmos device. + protected virtual void AfterDeviceFailure(Entity entity) + { + + } + + /// + /// Handler for when this atmos device loses integrity due to overpressure + /// + /// Gas holding atmos device. + protected virtual void IntegrityLost(Entity entity) + { + + } + + /// + /// Handler for when this atmos device exceeds its safety parameters + /// + /// Gas holding atmos device. + protected virtual void SafetyMeasures(Entity entity) + { + + } + + /// + /// Checks the status of an atmos device that has a specified max pressure, and handles overpressure issues. + /// + /// Gas holding atmos device. + /// Time since the last status update. + /// True if the device hasn't failed. False if the device has failed and been destroyed. + protected bool CheckStatus(Entity entity, float dt) + { + var pressure = entity.Comp.Air.Pressure; + + // Better mixes mean bigger and faster explosions! + if (pressure > entity.Comp.Overpressure * (entity.Comp.Integrity + 1)) + { + Atmos.MergeContainingMixture(entity.Owner, entity.Comp.Air, excite: true); + Audio.PlayPvs(entity.Comp.RuptureSound, Transform(entity).Coordinates, AudioParams.Default.WithVariation(0.125f)); + + // Integrity failure, destroy ourselves! + _destructible.DestroyEntity(entity); + + var totalIntensity = (float)Math.Sqrt(Atmos.GetOverPressure(entity.Comp.Air)); + if (_maxExplosivePower > 0 && _maxExplosivePower < totalIntensity) + totalIntensity = _maxExplosivePower; + + _explosions.TriggerExplosive(entity, totalIntensity: totalIntensity); + + Dirty(entity); + return false; + } + + // Device begins to fail. + if (pressure > entity.Comp.Overpressure) + { + IntegrityLost(entity); + entity.Comp.Integrity -= dt; + Appearance.SetData(entity.Owner, GasIntegrity.Integrity, entity.Comp.Integrity); + Appearance.SetData(entity.Owner, GasIntegrity.MaxIntegrity, entity.Comp.MaxIntegrity); + } + else if (entity.Comp.Integrity < entity.Comp.MaxIntegrity) + { + entity.Comp.Integrity = Math.Min(entity.Comp.Integrity + dt, entity.Comp.MaxIntegrity); + Appearance.SetData(entity.Owner, GasIntegrity.Integrity, entity.Comp.Integrity); + Appearance.SetData(entity.Owner, GasIntegrity.MaxIntegrity, entity.Comp.MaxIntegrity); + } + + // Device tries to prevent failure. + if (pressure > entity.Comp.SafetyPressure) + SafetyMeasures(entity); + + return true; + } +} + +[Serializable, NetSerializable, Flags] +public enum GasIntegrity +{ + Integrity, + MaxIntegrity +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.API.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.API.cs new file mode 100644 index 00000000000..5b953e6df4e --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.API.cs @@ -0,0 +1,104 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Content.Shared.Atmos.EntitySystems; + +public abstract partial class SharedAtmosphereSystem +{ + /// + /// Merges a given into this entity's containing . + /// + /// Entity who's containing + /// we're merging a given into. + /// The gas + /// we're merging into the containing + /// Whether we should ignore non-tile s. + /// Whether we should excite the gas upon merging. + [PublicAPI] + public virtual void MergeContainingMixture(Entity entity, GasMixture mixture, bool ignoreExposed = false, bool excite = false) + { + // Handled by server + } + + /// + /// Merges a given gas into this entity's tile . + /// + /// Entity who's containing + /// we're merging a given into. + /// The gas + /// we're merging into the containing . + /// Whether we should excite the gas upon merging. + [PublicAPI] + public virtual void MergeTileMixture(Entity entity, GasMixture mixture, bool excite = false) + { + // Handled by server + } + + /// + /// Adjusts a given gas in this entity's containing . + /// + /// Entity who's containing + /// we're merging a given into. + /// The gas in our given we're adjusting the mols of. + /// The amount of mols we're adjusting the gas by. + /// Whether we should ignore non-tile s. + /// Whether we should excite the gas upon merging. + [PublicAPI] + public virtual void AdjustContainingMixture(Entity entity, Gas gas, float mols, bool ignoreExposed = false, bool excite = false) + { + // Handled by server + } + + /// + /// Adjusts a given gas in this entity's tile . + /// + /// Entity who's containing + /// we're merging a given into. + /// The gas in our given we're adjusting the mols of. + /// The amount of mols we're adjusting the gas by. + /// Whether we should excite the gas upon merging. + [PublicAPI] + public virtual void AdjustTileMixture(Entity entity, Gas gas, float mols, bool excite = false) + { + // Handled by server + } + + /// + /// Tries to get the of a containing entity (ex. lockers and cryopods), + /// does not return tile s. + /// + /// Exposed entity that is in some . + /// The found gas . + /// Returns true if this entity is in an exposed , false otherwise. + [PublicAPI] + public bool TryGetExposedMixture(Entity entity, [NotNullWhen(true)] out GasMixture? mixture) + { + mixture = null; + if (!Resolve(entity, ref entity.Comp) || entity.Comp.Anchored) + return false; + + // TODO ATMOS: recursively iterate up through parents + // This really needs recursive InContainer metadata flag for performance + // And ideally some fast way to get the innermost airtight container. + var ev = new AtmosExposedGetAirEvent((entity, entity.Comp)); + RaiseLocalEvent(entity, ref ev); + mixture = ev.Gas; + + return ev.Handled; + } + + /// + /// Gets the potential energy from overpressure between two gas mixtures. + /// + /// + /// Returns the potential energy of the overpressure in Joules. + /// Value will be positive if the potential energy is outward (mix1 -> mix2) + /// Value will be negative if potential energy is inward (mix2 -> mix1) + /// + [PublicAPI] + public float GetOverPressure(GasMixture mix1, GasMixture? mix2 = null) + { + return (mix1.Pressure - (mix2?.Pressure ?? 0)) * mix1.Volume; + } + +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs new file mode 100644 index 00000000000..852af702199 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.CCVar; + +namespace Content.Shared.Atmos.EntitySystems; + +public abstract partial class SharedAtmosphereSystem +{ + /* + Partial class for storing shared configuration values. + */ + + public float HeatScale { get; private set; } + + [SuppressMessage("ReSharper", "BadExpressionBracesLineBreaks")] + [SuppressMessage("ReSharper", "MultipleStatementsOnOneLine")] + private void InitializeCVars() + { + Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true); + } +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs new file mode 100644 index 00000000000..240711f048a --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs @@ -0,0 +1,705 @@ +using System.Runtime.CompilerServices; +using Content.Shared.Atmos.Prototypes; +using Content.Shared.Atmos.Reactions; +using Content.Shared.CCVar; +using JetBrains.Annotations; + +namespace Content.Shared.Atmos.EntitySystems; + +public abstract partial class SharedAtmosphereSystem +{ + /* + Partial class for operations involving GasMixtures. + + Sometimes methods here are abstract because they need different client/server implementations + due to sandboxing. + */ + + /// + /// Cached array of molar heat capacities of the gases. + /// + public float[] GasMolarHeatCapacities => _gasMolarHeatCapacities; + + private float[] _gasMolarHeatCapacities = new float[Atmospherics.AdjustedNumberOfGases]; + + /// + /// Cached array of gas specific mols + /// + public float[] GasMolarMasses => _gasMolarMasses; + + private float[] _gasMolarMasses = new float[Atmospherics.AdjustedNumberOfGases]; + + /// + /// Mask used to determine if a gas is flammable or not. + /// + /// This is used to quickly determine if a contains any flammable gas. + /// When determining flammability, the float is multiplied with the mask and then + /// added to see if the mixture is flammable, and how many moles are considered flammable. + /// This is done instead of a massive if statement of doom everywhere. + /// Say Plasma has the bool set to true. + /// Atmospherics will place a 1 in the spot where plasma goes in the masking array. + /// Whenever we need to determine if a GasMixture contains fuel gases, we multiply the + /// gas array by the mask. Fuel gases will keep their value (being multiplied by one) + /// whereas non-fuel gases will be multiplied by zero and be zeroed out. + /// The resulting array can be HorizontalAdded, with any value above zero indicating fuel gases. + /// This works for multiple fuel gases at the same time, so it's a fairly quick way + /// to determine if a mixture has the gases we care about. + protected readonly float[] GasFuelMask = new float[Atmospherics.AdjustedNumberOfGases]; + + /// + /// Mask used to determine if a gas is an oxidizer or not. + /// Used in the same way as . + /// Nothing really super special. + /// + protected readonly float[] GasOxidizerMask = new float[Atmospherics.AdjustedNumberOfGases]; + + /// + /// Mask used to determine both fuel and oxidizer properties of a gas at the same time. + /// Primarily used to quickly report the specific moles in a mixture that caused a flammable reaction to occur. + /// + protected readonly float[] GasOxidiserFuelMask = new float[Atmospherics.TotalNumberOfGases]; + + public string?[] GasReagents = new string[Atmospherics.TotalNumberOfGases]; + protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases]; + + public virtual void InitializeGases() + { + foreach (var gas in Enum.GetValues()) + { + var idx = (int)gas; + // Log an error if the corresponding prototype isn't found + if (!ProtoMan.TryIndex(gas.ToString(), out var gasPrototype)) + { + Log.Error($"Failed to find corresponding {nameof(GasPrototype)} for gas ID {(int)gas} ({gas}) with expected ID \"{gas.ToString()}\". Is your prototype named correctly?"); + continue; + } + GasPrototypes[idx] = gasPrototype; + GasReagents[idx] = gasPrototype.Reagent; + } + + for (var i = 0; i < GasPrototypes.Length; i++) + { + /* + As an optimization routine we pre-divide the specific heat by the heat scale here, + so we don't have to do it every time we calculate heat capacity. + Most usages are going to want the scaled value anyway. + + If you would like the unscaled specific heat, you'd need to multiply by HeatScale again. + TODO ATMOS: please just make this 2 separate arrays instead of invoking multiplication every time. + */ + _gasMolarHeatCapacities[i] = GasPrototypes[i].MolarHeatCapacity / HeatScale; + _gasMolarMasses[i] = GasPrototypes[i].MolarMass; + + // """Mask""" built here. Used to determine if a gas is fuel/oxidizer or not decently quickly and clearly. + GasFuelMask[i] = GasPrototypes[i].IsFuel ? 1 : 0; + + // Same for oxidizer mask. + GasOxidizerMask[i] = GasPrototypes[i].IsOxidizer ? 1 : 0; + + // OxidiserFuel mask is just fuel and oxidizer combined, because both are required for a reaction to occur. + GasOxidiserFuelMask[i] = GasFuelMask[i] * GasOxidizerMask[i]; + } + } + + /// + /// Gets only the moles that are considered a fuel and an oxidizer in a . + /// + /// The to get the flammable moles for. + /// A buffer to write the flammable moles into. Must be the same length as the number of gases. + /// A of moles where only the flammable and oxidizer moles are returned, and the rest are 0. + [PublicAPI] + public void GetFlammableMoles(GasMixture mixture, float[] buffer) + { + NumericsHelpers.Multiply(mixture.Moles, GasOxidiserFuelMask, buffer); + } + + /// + /// Determines if a is ignitable or not. + /// This is a combination of determining if a mixture both has oxidizer and fuel. + /// + /// The to determine. + /// The minimum amount of moles at which a is + /// considered ignitable, for both oxidizer and fuel. + /// True if the is ignitable, otherwise, false. + [PublicAPI] + public bool IsMixtureIgnitable(GasMixture mixture, float epsilon = Atmospherics.Epsilon) + { + return IsMixtureFuel(mixture, epsilon) && IsMixtureOxidizer(mixture, epsilon); + } + + /// + /// Determines if a has fuel gases in it or not. + /// + /// The to determine. + /// The minimum amount of moles at which a + /// is considered fuel. + /// True if the is fuel, otherwise, false. + [PublicAPI] + public abstract bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon); + + /// + /// Determines if a has oxidizer gases in it or not. + /// + /// The to determine. + /// The minimum amount of moles at which a + /// is considered an oxidizer. + /// True if the is an oxidizer, otherwise, false. + [PublicAPI] + public abstract bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon); + + /// + /// Calculates the heat capacity for a . + /// + /// The to calculate the heat capacity for. + /// Whether to apply the heat capacity scaling factor. + /// This is an extremely important boolean to consider or else you will get heat transfer wrong. + /// See for more info. + /// The heat capacity of the . + [PublicAPI] + public float GetHeatCapacity(GasMixture mixture, bool applyScaling) + { + var scale = GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable); + + // By default GetHeatCapacityCalculation() has the heat-scale divisor pre-applied. + // So if we want the un-scaled heat capacity, we have to multiply by the scale. + return applyScaling ? scale : scale * HeatScale; + } + + /// + /// Calculates the thermal energy for a . + /// + /// The to calculate the thermal + /// energy of. + /// The 's thermal energy in joules. + [PublicAPI] + public float GetThermalEnergy(GasMixture mixture) + { + return mixture.Temperature * GetHeatCapacity(mixture); + } + + /// + /// Calculates the thermal energy for a gas mixture, + /// using a provided cached heat capacity value. + /// + /// The to calculate the thermal energy of. + /// A cached heat capacity value for the gas mixture, + /// to avoid redundant heat capacity calculations. + /// The 's thermal energy in joules. + [PublicAPI] + public float GetThermalEnergy(GasMixture mixture, float cachedHeatCapacity) + { + return mixture.Temperature * cachedHeatCapacity; + } + + /// + /// Merges one into another, modifying the receiver. + /// + /// The to merge into. This will be modified. + /// The to merge from. This will not be modified. + [PublicAPI] + public void Merge(GasMixture receiver, GasMixture giver) + { + if (receiver.Immutable) + return; + + if (MathF.Abs(receiver.Temperature - giver.Temperature) > Atmospherics.MinimumTemperatureDeltaToConsider) + { + var receiverHeatCapacity = GetHeatCapacity(receiver); + var giverHeatCapacity = GetHeatCapacity(giver); + var combinedHeatCapacity = receiverHeatCapacity + giverHeatCapacity; + if (combinedHeatCapacity > Atmospherics.MinimumHeatCapacity) + { + receiver.Temperature = (GetThermalEnergy(giver, giverHeatCapacity) + GetThermalEnergy(receiver, receiverHeatCapacity)) / combinedHeatCapacity; + } + } + + NumericsHelpers.Add(receiver.Moles, giver.Moles); + } + + /// + /// Performs reactions for a given gas mixture on an optional holder. + /// + /// The to perform reactions on. + /// that holds the . + /// used by Atmospherics to determine locality for certain reaction effects. + /// The of the reactions performed. + [PublicAPI] + public abstract ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder); + + /// + /// Gets the heat capacity for a . + /// + /// The to calculate the heat capacity for. + /// The heat capacity of the . + /// Note that the heat capacity of the mixture may be slightly different from + /// "real life" as we intentionally fake a heat capacity for space in + /// in order to allow Atmospherics to cool down space. + protected float GetHeatCapacity(GasMixture mixture) + { + return GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable); + } + + /// + /// Gets the mass of a given + /// + /// The in question + /// Returns the volume in kilograms. + [PublicAPI] + public abstract float GetMass(GasMixture mix); + + /// + [PublicAPI] + public abstract float GetMass(float[] moles); + + /// + /// Calculates the amount of volume transferred from one gas mixture to another over time based on flow rate. + /// + /// + /// A + /// Another + /// The area of transfer, in square meters. One tile of movement is about one square meter. + /// delta time, or how much time is passing/has passed. + /// Discharge coefficient. An abstract modifier for friction and turbulence. + /// + /// The volume of gas being moved over dt in Litres. + /// If the value is positive it's in the direction of mix1->mix2, + /// If it's negative it's in the direction of mix2 -> mix1 + /// + /// I'm assuming C is always 1 because I'm lazy, you can precalculate it and pass it with the area if you really care. + [PublicAPI] + public double GetFlowVolume(GasMixture mix1, GasMixture mix2, float area, float dt, float c = 1f) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dt); + return dt * GetFlowRate(mix1, mix2, area, c); + } + + /// + [PublicAPI] + public double GetFlowVolume(GasMixture mix1, float deltaP, float area, float dt, float c = 1f) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dt); + return dt * GetFlowRate(mix1, deltaP, area, c); + } + + /// + /// Calculates the volumetric flow rate between two gas mixtures. + /// + /// A + /// Another + /// The area of transfer, in square meters. One tile of movement is about one square meter. + /// Discharge coefficient. An abstract modifier for friction and turbulence. + /// + /// The volume of gas being moved in Litres / Second. + /// If the value is positive it's in the direction of mix1->mix2, + /// If it's negative it's in the direction of mix2 -> mix1 + /// + /// I'm assuming C is always 1 because I'm lazy, you can precalculate it and pass it with the area if you really care. + [PublicAPI] + public double GetFlowRate(GasMixture mix1, GasMixture mix2, float area, float c = 1f) + { + /* + Q = C × A × √(2 × ΔP / ρ) + Q is the volumetric airflow rate + C is the discharge coefficient + A is the cross-sectional area + ΔP is the measured pressure difference + ρ is the air density, adjusted for environmental conditions. + We can break this up into Q = A × V where V is the velocity of the gas. + */ + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(area); + return area * GetFlowVelocity(mix1, mix2, c); + } + + /// + [PublicAPI] + public double GetFlowRate(GasMixture mix1, float deltaP, float area, float c = 1f) + { + /* + Q = C × A × √(2 × ΔP / ρ) + Q is the volumetric airflow rate + C is the discharge coefficient + A is the cross-sectional area + ΔP is the measured pressure difference + ρ is the air density, adjusted for environmental conditions. + We can break this up into Q = A × V where V is the velocity of the gas. + */ + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(area); + return area * GetFlowVelocity(mix1, deltaP, c); + } + + /// + /// Calculates the flow velocity between two gas mixtures using Q = C × A × √(2 × ΔP / ρ) but without the A (area) + /// Useful for determining flow rate, or how fast a gas is moving. + /// + /// A + /// Another + /// Discharge coefficient. An abstract modifier for friction and turbulence. + /// + /// The velocity of gas movement between two mixtures in Meters / Second. + /// If the value is positive it's in the direction of mix1->mix2, + /// If it's negative it's in the direction of mix2 -> mix1 + /// + [PublicAPI] + public double GetFlowVelocity(GasMixture mix1, GasMixture mix2, float c = 1f) + { + if (mix1.Pressure > mix2.Pressure) + return GetFlowVelocity(mix1, mix1.Pressure - mix2.Pressure, c); + + return -GetFlowVelocity(mix2, mix2.Pressure - mix1.Pressure, c); + } + + /// + /// Calculates the flow velocity between a gas mixture given a pressure differential. + /// + /// The mixture which is being allowed to flow + /// The difference in pressure between this mixture and where it's flowing to + /// Discharge coefficient. An abstract modifier for friction and turbulence. + /// + /// The velocity of the gas leaving our mixture in Meters / Second. + /// + [PublicAPI] + public double GetFlowVelocity(GasMixture mix1, float deltaP, float c = 1f) + { + /* + V = C × √(2 × ΔP / ρ) + V is the velocity of our gas + C is the discharge coefficient + ΔP is the measured pressure difference + ρ is the air density, adjusted for environmental conditions. + Density is equivalent to Mass / Volume, so we invert that to divide by density. + */ + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(deltaP); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(c); + return c * Math.Sqrt(2 * deltaP * mix1.Volume / GetMass(mix1)); + } + + /// + /// Lets a volume of gas flow throw a constrained area into another volume of gas over a period of time. + /// + /// Gas volume that is discharging some of its gas. + /// Gas volume that is receiving the discharge. + /// Time that the discharge occurs in seconds, should be as small as possible since it doesn't use calculus + /// Area that our gas is traveling through in m^2, the larger the area the bigger the transfer. + /// Default of 2m^2 since that's the area of a single face of an atmos tile. + [PublicAPI] + public void FlowGas(GasMixture mixture, GasMixture? output, float dt, float area) + { + FlowGas(mixture, output, mixture.Pressure, dt, area); + } + + /// + [PublicAPI] + public void FlowGas(GasMixture mixture, GasMixture? output, float pressure, float dt, float area) + { + if (output == null) + { + FlowGas(mixture, pressure, dt, area); + return; + } + + pressure = Math.Min(pressure, mixture.Pressure - output.Pressure); + var removed = FlowGas(mixture, pressure, dt, area); + + if (removed == null) + return; + + Merge(output, removed); + } + + /// + /// Lets a volume of gas flow through constrained area at a constrained pressure delta. + /// + /// Mixture of gas that is currently flowing + /// Pressure our gas is able to flow at. + /// Time that the discharge occurs in seconds, should be as small as possible since it doesn't use calculus + /// Area that our gas is traveling through in m^2, the larger the area the bigger the transfer. + /// Default of 2m^2 since that's the area of a single face of an atmos tile. + /// + [PublicAPI] + public GasMixture? FlowGas(GasMixture mixture, float deltaP, float dt, float area = 2f) + { + if (deltaP <= 0) + return null; + + return ReleaseGasAt(mixture, (float)GetFlowVolume(mixture, deltaP, area, dt), mixture.Pressure); + } + + /// + /// Releases some volume of a gas mixture at a specified pressure. + /// + /// Mixture which is releasing gas. + /// Optional Mixture to receive gas + /// Volume we are releasing + /// Pressure of the released volume. + [PublicAPI] + public void ReleaseGasAt(GasMixture mixture, GasMixture? output, float volume, float targetPressure) + { + if (output == null) + { + ReleaseGasAt(mixture, volume, targetPressure); + return; + } + + targetPressure = Math.Min(targetPressure, mixture.Pressure - output.Pressure); + + if (targetPressure <= 0) + return; + + var molesNeeded = Math.Min(targetPressure * volume / (Atmospherics.R * mixture.Temperature), + MolesToEqualizePressure(mixture, output)); + + var removed = mixture.Remove(molesNeeded); + + Merge(mixture, removed); + } + + /// + [PublicAPI] + public GasMixture? ReleaseGasAt(GasMixture mixture, float volume, float targetPressure) + { + if (targetPressure <= 0) + return null; + + targetPressure = Math.Min(targetPressure, mixture.Pressure); + + return RemoveVolumeAtPressure(mixture, volume, targetPressure); + } + + /// + /// Removes a specified volume of gas from a mixture, at a specific pressure. + /// + /// mixture of gas + /// volume we're attempting to remove + /// pressure that volume will be removed at. + public GasMixture RemoveVolumeAtPressure(GasMixture mixture, float volume, float pressure) + { + var molesNeeded = pressure * volume / (Atmospherics.R * mixture.Temperature); + return mixture.Remove(molesNeeded); + } + + /// + /// Gets the heat capacity for a . + /// + /// The moles array of the + /// Whether this represents space, + /// and thus experiences space-specific mechanics (we cheat and make it a bit cooler). + /// See . + /// The heat capacity of the . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected abstract float GetHeatCapacityCalculation(float[] moles, bool space); + + + /// + /// Calculates the moles that must be transferred from + /// to to equalize pressure. + /// + public float MolesToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2) + { + return gasMixture1.TotalMoles * FractionToEqualizePressure(gasMixture1, gasMixture2); + } + + /// + /// Calculates the dimensionless fraction of gas required to equalize pressure between two gas mixtures. + /// + /// The first gas mixture involved in the pressure equalization. + /// This mixture should be the one you always expect to be the highest pressure. + /// The second gas mixture involved in the pressure equalization. + /// A float (from 0 to 1) representing the dimensionless fraction of gas that needs to be transferred from the + /// mixture of higher pressure to the mixture of lower pressure. + /// + /// + /// This properly takes into account the effect + /// of gas merging from inlet to outlet affecting the temperature + /// (and possibly increasing the pressure) in the outlet. + /// + /// + /// The gas is assumed to expand freely, + /// so the temperature of the gas with the greater pressure is not changing. + /// + /// + /// + /// If you want to calculate the moles required to equalize pressure between an inlet and an outlet, + /// multiply the fraction returned by the source moles. + /// + public float FractionToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2) + { + /* + Problem: the gas being merged from the inlet to the outlet could affect the + temp. of the gas and cause a pressure rise. + We want the pressure to be equalized, so we have to account for this. + + For clarity, let's assume that gasMixture1 is the inlet and gasMixture2 is the outlet. + + We require mechanical equilibrium, so \( P_1' = P_2' \) + + Before the transfer, we have: + \( P_1 = \frac{n_1 R T_1}{V_1} \) + \( P_2 = \frac{n_2 R T_2}{V_2} \) + + After removing fraction \( x \) moles from the inlet, we have: + \( P_1' = \frac{(1 - x) n_1 R T_1}{V_1} \) + + The outlet will gain the same \( x n_1 \) moles of gas. + So \( n_2' = n_2 + x n_1 \) + + After mixing, the outlet temperature will be changed. + Denote the new mixture temperature as \( T_2' \). + Volume is constant. + So we have: + \( P_2' = \frac{(n_2 + x n_1) R T_2}{V_2} \) + + The total energy of the incoming inlet to outlet gas at \( T_1 \) plus the existing energy of the outlet gas at \( T_2 \) + will be equal to the energy of the new outlet gas at \( T_2' \). + This leads to the following derivation: + \( x n_1 C_1 T_1 + n_2 C_2 T_2 = (x n_1 C_1 + n_2 C_2) T_2' \) + + Where \( C_1 \) and \( C_2 \) are the heat capacities of the inlet and outlet gases, respectively. + + Solving for \( T_2' \) gives us: + \( T_2' = \frac{x n_1 C_1 T_1 + n_2 C_2 T_2}{x n_1 C_1 + n_2 C_2} \) + + Once again, we require mechanical equilibrium (\( P_1' = P_2' \)), + so we can substitute \( T_2' \) into the pressure equation: + + \( \frac{(1 - x) n_1 R T_1}{V_1} = + \frac{(n_2 + x n_1) R}{V_2} \cdot + \frac{x n_1 C_1 T_1 + n_2 C_2 T_2} + {x n_1 C_1 + n_2 C_2} \) + + Now it's a matter of solving for \( x \). + Not going to show the full derivation here, just steps. + 1. Cancel common factor \( R \). + 2. Multiply both sides by \( x n_1 C_1 + n_2 C_2 \), so that everything + becomes a polynomial in terms of \( x \). + 3. Expand both sides. + 4. Collect like powers of \( x \). + 5. After collecting, you should end up with a polynomial of the form: + + \( (-n_1 C_1 T_1 (1 + \frac{V_2}{V_1})) x^2 + + (n_1 T_1 \frac{V_2}{V_1} (C_1 - C_2) - n_2 C_1 T_1 - n_1 C_2 T_2) x + + (n_1 T_1 \frac{V_2}{V_1} C_2 - n_2 C_2 T_2) = 0 \) + + Divide through by \( n_1 C_1 T_1 \) and replace each ratio with a symbol for clarity: + \( k_V = \frac{V_2}{V_1} \) + \( k_n = \frac{n_2}{n_1} \) + \( k_T = \frac{T_2}{T_1} \) + \( k_C = \frac{C_2}{C_1} \) + */ + + // Ensure that P_1 > P_2 so the quadratic works out. + if (gasMixture1.Pressure < gasMixture2.Pressure) + { + (gasMixture1, gasMixture2) = (gasMixture2, gasMixture1); + } + + // Establish the dimensionless ratios. + var volumeRatio = gasMixture2.Volume / gasMixture1.Volume; + var molesRatio = gasMixture2.TotalMoles / gasMixture1.TotalMoles; + var temperatureRatio = gasMixture2.Temperature / gasMixture1.Temperature; + var heatCapacityRatio = GetHeatCapacity(gasMixture2) / GetHeatCapacity(gasMixture1); + + // The quadratic equation is solved for the transfer fraction. + var quadraticA = 1 + volumeRatio; + var quadraticB = molesRatio - volumeRatio + heatCapacityRatio * (temperatureRatio + volumeRatio); + var quadraticC = heatCapacityRatio * (molesRatio * temperatureRatio - volumeRatio); + + return (-quadraticB + MathF.Sqrt(quadraticB * quadraticB - 4 * quadraticA * quadraticC)) / (2 * quadraticA); + } + + /// + /// Determines the fraction of gas to be removed and transferred from a source + /// to a target to reach a target pressure + /// in the target . + /// + /// The source that gas will be removed from. + /// This should always be of higher pressure than the second . + /// The target that will increase in pressure + /// to the target pressure. + /// The target mixture's desired pressure to target. + /// A float representing the dimensionless fraction of gas to transfer from the source + /// to the target. This may return negative if you have your mixtures swapped. + /// Note that this method doesn't take into account the heat capacity of the + /// transferred volume causing a pressure rise in the target . + [PublicAPI] + public static float FractionToMaxPressure(GasMixture mix1, GasMixture mix2, float targetPressure) + { + var molesToTransfer = MolesToMaxPressure(mix1, mix2, targetPressure); + return molesToTransfer / mix1.TotalMoles; + } + + /// + /// Determines the number of moles to be removed and transferred from a source + /// to a target to reach a target pressure + /// in the target . + /// + /// The source that gas will be removed from. + /// This should always be of higher pressure than the second . + /// The target that will increase in pressure + /// to the target pressure. + /// The target mixture's desired pressure to target. + /// The difference in moles required to reach the target pressure. + /// Note that this method doesn't take into account the heat capacity of the + /// transferred volume causing a pressure rise in the target . + [PublicAPI] + public static float MolesToMaxPressure(GasMixture mix1, GasMixture mix2, float targetPressure) + { + /* + Calculate the moles required to reach the target pressure. + The formula is derived from the ideal gas law and the + general Richman's law, under the simplification that all the specific heat capacities are equal. + Derivation can also be seen at + https://github.com/space-wizards/space-station-14/pull/35211/files/a0ae787fe07a4e792570f55b49d9dd8038eb6e4d#r1961183456 + TODO ATMOS Make this properly obey the heat capacity change on the target mixture. + + Derivation is as follows. + Assume A is mix1, B is mix2, C is the combined mixture after transfer. + We can express the number of moles in C: + n_C = n_A + n_B + + We can then determine the temperature of C: + T_C = \frac{T_A n_A c_A + T_B n_B c_B}{n_A c_A + n_B c_B} + + Where c_A and c_B are the specific heats of mixtures A and B, respectively. + We can then express the pressure of C: + P_C = \frac{n_C R T_C}{V_C} + + Using the above equations, we can express P_C as follows: + P_C = \frac{(n_A + n_B) R (\frac{T_a n_A + T_B n_B}{n_A + n_B}}{V_C} + + Which can be reduced to: + P_C = \frac{R (T_A n_A + T_B n_B)}{V_C} + + Solving for n_A gives: + n_A = \frac{P_C V_C - R T_B n_B}{R T_A} + + Using the ideal gas law to substitute: + n_A = \frac{P_C V_C - P_B V_B}{R T_A} + + The output volume doesn't change: + V_B = V_C + + So: + n_A = \frac{(P_C - P_B) V_B}{R T_A} + */ + + var delta = targetPressure - mix2.Pressure; + var requiredMoles = (delta * mix2.Volume) / (mix1.Temperature * Atmospherics.R); + + // Return the fraction of moles to transfer. + return requiredMoles; + } + + /// + /// Determines the number of moles that need to be removed from a to reach a target pressure threshold. + /// + /// The gas mixture whose moles and properties will be used in the calculation. + /// The target pressure threshold to calculate against. + /// The difference in moles required to reach the target pressure threshold. + /// The temperature of the gas is assumed to be not changing due to a free expansion. + public static float MolesToPressureThreshold(GasMixture gasMixture, float targetPressure) + { + // Kid named PV = nRT. + return gasMixture.TotalMoles - + targetPressure * gasMixture.Volume / (Atmospherics.R * gasMixture.Temperature); + } +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs index 46989397347..47e3219efc4 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs @@ -2,37 +2,105 @@ using Content.Shared.Atmos.Prototypes; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Robust.Shared.Configuration; using Robust.Shared.Prototypes; -namespace Content.Shared.Atmos.EntitySystems -{ - public abstract partial class SharedAtmosphereSystem : EntitySystem - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SharedInternalsSystem _internals = default!; +namespace Content.Shared.Atmos.EntitySystems; - private EntityQuery _internalsQuery; +public abstract partial class SharedAtmosphereSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + [Dependency] private readonly SharedInternalsSystem _internals = default!; + [Dependency] protected readonly SharedTransformSystem XformSystem = default!; - protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases]; + [Dependency] private readonly EntityQuery _internalsQuery = default!; - public override void Initialize() - { - base.Initialize(); + /// + /// The length to pre-allocate list/dicts of delta pressure entities on a . + /// + public const int DeltaPressurePreAllocateLength = 1000; - _internalsQuery = GetEntityQuery(); + public override void Initialize() + { + base.Initialize(); - InitializeBreathTool(); + InitializeBreathTool(); + InitializeGases(); + InitializeCVars(); + } - for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) - { - GasPrototypes[i] = _prototypeManager.Index(i.ToString()); - } - } + /// + /// Gets the of a given gas ID. + /// + /// The gas ID to get the prototype of. + /// The of the given gas ID. + public GasPrototype GetGas(int gasId) + { + return GasPrototypes[gasId]; + } - public GasPrototype GetGas(int gasId) => GasPrototypes[gasId]; + /// + /// Gets the of a given gas ID. + /// + /// The gas ID to get the prototype of. + /// The of the given gas ID. + public GasPrototype GetGas(Gas gasId) + { + return GasPrototypes[(int)gasId]; + } - public GasPrototype GetGas(Gas gasId) => GasPrototypes[(int) gasId]; + /// + /// Gets an enumerable of all the s. + /// + public IEnumerable Gases => GasPrototypes; +} - public IEnumerable Gases => GasPrototypes; - } +/// +/// Enum that represents the current processing state of a +/// . +/// +public enum AtmosphereProcessingState : byte +{ + Revalidate, + TileEqualize, + ActiveTiles, + ExcitedGroups, + HighPressureDelta, + DeltaPressure, + Hotspots, + Superconductivity, + PipeNet, + AtmosDevices, + NumStates, } + +/// +/// Data on the airtightness of a . +/// Cached on the and updated during +/// if it was invalidated. +/// +/// The current directions blocked on this tile. +/// This is where air cannot flow to. +/// Whether the tile can have air when blocking directions. +/// Common for entities like thin windows which only block one face but can still have air in the residing tile. +/// If true, Atmospherics will generate air (yes, creating matter from nothing) +/// using the adjacent tiles as a seed if the airtightness is removed and the tile has no air. +/// This allows stuff like airlocks that void air when becoming airtight to keep opening/closing without +/// draining a room by continuously voiding air. +public readonly record struct AirtightData( + AtmosDirection BlockedDirections, + bool NoAirWhenBlocked, + bool FixVacuum); + +/// +/// Struct that holds the result of delta pressure damage processing for an entity. +/// This is only created and enqueued when the entity needs to take damage. +/// +/// The entity to deal damage to. +/// The current absolute pressure the entity is experiencing. +/// The current delta pressure the entity is experiencing. +public readonly record struct DeltaPressureDamageResult( + Entity Ent, + float Pressure, + float DeltaPressure); diff --git a/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs new file mode 100644 index 00000000000..4ea9880707f --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedDeltaPressureSystem.cs @@ -0,0 +1,20 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Examine; + +namespace Content.Shared.Atmos.EntitySystems; + +public abstract partial class SharedDeltaPressureSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExaminedEvent); + } + + private void OnExaminedEvent(Entity ent, ref ExaminedEvent args) + { + if (ent.Comp.IsTakingDamage) + args.PushMarkup(Loc.GetString("window-taking-damage")); + } +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs index 8c7ebe83322..ffb7d9f865a 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTankSystem.cs @@ -6,16 +6,14 @@ using Content.Shared.Toggleable; using Content.Shared.UserInterface; using Content.Shared.Verbs; -using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using InternalsComponent = Content.Shared.Body.Components.InternalsComponent; namespace Content.Shared.Atmos.EntitySystems; -public abstract class SharedGasTankSystem : EntitySystem +public abstract class SharedGasTankSystem : GasMaxPressureSystem { [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly SharedInternalsSystem _internals = default!; [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; @@ -48,9 +46,9 @@ private void OnGasTankToggleInternals(Entity ent, ref GasTankT private void OnGasTankSetPressure(Entity ent, ref GasTankSetPressureMessage args) { - var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure); + var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxReleasePressure); - ent.Comp.OutputPressure = pressure; + ent.Comp.ReleasePressure = pressure; Dirty(ent); UpdateUserInterface(ent); } @@ -81,7 +79,7 @@ private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent if (component.IsConnected) args.PushMarkup(Loc.GetString("comp-gas-tank-connected")); - args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve")); + args.PushMarkup(Loc.GetString(component.ReleaseValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve")); } private void OnActionToggle(Entity gasTank, ref ToggleActionEvent args) @@ -93,28 +91,46 @@ private void OnActionToggle(Entity gasTank, ref ToggleActionEv args.Handled = true; } - private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent args) + private void OnGetAlternativeVerb(Entity entity, ref GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || args.Hands == null) return; + var user = args.User; args.Verbs.Add(new AlternativeVerb() { - Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"), + Text = entity.Comp.ReleaseValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"), Act = () => { - component.IsValveOpen = !component.IsValveOpen; - _audio.PlayPredicted(component.ValveSound, uid, args.User); - Dirty(uid, component); + ToggleValve(entity, user: user); }, - Disabled = component.IsConnected, + Disabled = entity.Comp.IsConnected, }); } + /// > + public void ToggleValve(Entity entity, EntityUid? user = null) + { + ToggleValve(entity, !entity.Comp.ReleaseValveOpen, user); + } + + /// + /// Toggles the release valve for this open or closed + /// + /// Entity whose valve we're toggling + /// Whether we're opening or closing the valve + /// Optional user who is performing the action. + public void ToggleValve(Entity entity, bool open, EntityUid? user = null) + { + entity.Comp.ReleaseValveOpen = open; + Audio.PlayPredicted(entity.Comp.ValveSound, entity, user); + Dirty(entity); + } + public bool CanConnectToInternals(Entity ent) { TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User); - return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen; + return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.ReleaseValveOpen; } public bool ConnectToInternals(Entity ent, EntityUid? user = null) @@ -141,8 +157,8 @@ public bool ConnectToInternals(Entity ent, EntityUid? user = n if (!component.IsConnected) return false; - component.ConnectStream = _audio.Stop(component.ConnectStream); - component.ConnectStream = _audio.PlayPredicted(component.ConnectSound, owner, user)?.Entity; + component.DisconnectStream = Audio.Stop(component.DisconnectStream); + component.ConnectStream = Audio.PlayPredicted(component.ConnectSound, owner, user)?.Entity; UpdateUserInterface(ent); return true; } @@ -210,8 +226,8 @@ public bool DisconnectFromInternals(Entity ent, EntityUid? use if (internalsUid != null && internalsComp != null) _internals.DisconnectTank((internalsUid.Value, internalsComp), forced: forced); - component.DisconnectStream = _audio.Stop(component.DisconnectStream); - component.DisconnectStream = _audio.PlayPredicted(component.DisconnectSound, owner, user)?.Entity; + component.ConnectStream = Audio.Stop(component.ConnectStream); + component.DisconnectStream = Audio.PlayPredicted(component.DisconnectSound, owner, user)?.Entity; UpdateUserInterface(ent); return true; } diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index 8e7dfdedaf9..e9c697c53c9 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -1,113 +1,256 @@ using Content.Shared.Atmos.Components; -using Content.Shared.Atmos.Prototypes; +using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -namespace Content.Shared.Atmos.EntitySystems +namespace Content.Shared.Atmos.EntitySystems; + +public abstract class SharedGasTileOverlaySystem : EntitySystem { - public abstract class SharedGasTileOverlaySystem : EntitySystem + public const byte ChunkSize = 8; + protected float AccumulatedFrameTime; + protected bool PvsEnabled; + + [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + [Dependency] protected readonly IConfigurationManager ConfMan = default!; + [Dependency] private readonly SharedAtmosphereSystem _atmosphere = default!; + + /// + /// array of the ids of all visible gases. + /// + public int[] VisibleGasId = default!; + + public override void Initialize() { - public const byte ChunkSize = 8; - protected float AccumulatedFrameTime; - protected bool PvsEnabled; + base.Initialize(); + SubscribeLocalEvent(OnGetState); - [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + List visibleGases = new(); - /// - /// array of the ids of all visible gases. - /// - public int[] VisibleGasId = default!; + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gasPrototype = _atmosphere.GetGas(i); + if (gasPrototype.GasOverlaySprite != null) + visibleGases.Add(i); + } + VisibleGasId = visibleGases.ToArray(); + } + + private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args) + { + if (PvsEnabled && !args.ReplayState) + return; - public override void Initialize() + // Should this be a full component state or a delta-state? + if (args.FromTick <= component.CreationTick || args.FromTick <= component.ForceTick) { - base.Initialize(); - SubscribeLocalEvent(OnGetState); + args.State = new GasTileOverlayState(component.Chunks); + return; + } - List visibleGases = new(); + var data = new Dictionary(); + foreach (var (index, chunk) in component.Chunks) + { + if (chunk.LastUpdate >= args.FromTick) + data[index] = chunk; + } - for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) - { - var gasPrototype = ProtoMan.Index(i.ToString()); - if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture) || !string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) - visibleGases.Add(i); - } + args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys)); + } + + public static Vector2i GetGasChunkIndices(Vector2i indices) + { + return new Vector2i((int)MathF.Floor((float)indices.X / ChunkSize), (int)MathF.Floor((float)indices.Y / ChunkSize)); + } - VisibleGasId = visibleGases.ToArray(); + [Serializable, NetSerializable] + public readonly struct GasOverlayData : IEquatable + { + [ViewVariables] public readonly byte FireState; + [ViewVariables] public readonly byte[] Opacity; + // TODO change fire color based on ByteTemp + + /// + /// Network-synced air temperature, compressed to a single byte per tile for bandwidth optimization. + /// Note: Values are approximate and may deviate even ~10°C from the precise server side only temperature. + /// + [ViewVariables] + public readonly ThermalByte ByteGasTemperature; + + + public GasOverlayData(byte fireState, byte[] opacity, ThermalByte byteTemp) + { + FireState = fireState; + Opacity = opacity; + ByteGasTemperature = byteTemp; } - private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args) + public bool Equals(GasOverlayData other) { - if (PvsEnabled && !args.ReplayState) - return; + if (FireState != other.FireState) + return false; - // Should this be a full component state or a delta-state? - if (args.FromTick <= component.CreationTick || args.FromTick <= component.ForceTick) - { - args.State = new GasTileOverlayState(component.Chunks); - return; - } + if (Opacity?.Length != other.Opacity?.Length) + return false; - var data = new Dictionary(); - foreach (var (index, chunk) in component.Chunks) + if (Opacity != null && other.Opacity != null) { - if (chunk.LastUpdate >= args.FromTick) - data[index] = chunk; + for (var i = 0; i < Opacity.Length; i++) + { + if (Opacity[i] != other.Opacity[i]) + return false; + } } - args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys)); - } + if (ByteGasTemperature != other.ByteGasTemperature) + return false; - public static Vector2i GetGasChunkIndices(Vector2i indices) - { - return new((int) MathF.Floor((float) indices.X / ChunkSize), (int) MathF.Floor((float) indices.Y / ChunkSize)); + return true; } + } - [Serializable, NetSerializable] - public readonly struct GasOverlayData : IEquatable - { - [ViewVariables] - public readonly byte FireState; + [Serializable, NetSerializable] + public sealed class GasOverlayUpdateEvent : EntityEventArgs + { + public Dictionary> UpdatedChunks = new(); + public Dictionary> RemovedChunks = new(); + } +} + +/// +/// Struct for networking gas temperatures to all clients using a single struct(byte) per tile. +/// +/// +/// +/// This struct compresses the gas temperature into a 1-byte value (0-255). +/// It clamps the temperature to a maximum of 1000K and divides it by 4, creating a range of 0-250. +/// This provides a resolution of 4 degrees Kelvin. +/// +/// +/// The remaining bytes are used as special flags: +/// +/// 255: Represents a Wall (block cannot hold atmosphere). +/// 254: Represents a Vacuum. +/// 251-253: Reserved for future use. +/// +/// +/// +/// Dirtying Logic: The value is only dirtied and networked if the difference between the +/// networked byte and the real atmosphere byte is greater than 1. This prevents network spam +/// from minor temperature fluctuations (e.g., heating from 1K to 8K will not trigger an update, +/// but hitting 9K moves the byte index enough to sync). +/// +/// +/// Currently, the conversion is linear. Future improvements might involve a quadratic scale +/// or pre-defined resolution points to offer higher precision at room temperatures +/// and lower precision at extreme temperatures (1000K). +/// +/// +[Serializable, NetSerializable] +public struct ThermalByte : IEquatable +{ + public const float TempMinimum = 0f; + public const float TempMaximum = 1000f; + public const int TempResolution = 250; - [ViewVariables] - public readonly byte[] Opacity; + public const byte ReservedFuture0 = 251; + public const byte ReservedFuture1 = 252; + public const byte ReservedFuture2 = 253; + public const byte StateVacuum = 254; + public const byte AtmosImpossible = 255; - // TODO change fire color based on temps - // But also: dont dirty on a 0.01 kelvin change in temperatures. - // Either have a temp tolerance, or map temperature -> byte levels + public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution; + public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum); - public GasOverlayData(byte fireState, byte[] opacity) - { - FireState = fireState; - Opacity = opacity; - } + private byte _coreValue; - public bool Equals(GasOverlayData other) - { - if (FireState != other.FireState) - return false; + public ThermalByte(float temperatureKelvin) + { + SetTemperature(temperatureKelvin); + } - if (Opacity?.Length != other.Opacity?.Length) - return false; + public ThermalByte() + { + _coreValue = AtmosImpossible; + } - if (Opacity != null && other.Opacity != null) - { - for (var i = 0; i < Opacity.Length; i++) - { - if (Opacity[i] != other.Opacity[i]) - return false; - } - } + /// + /// Set temperature of air in this in Kelvin. + /// + public void SetTemperature(float temperatureKelvin) + { + var clampedTemp = Math.Clamp(temperatureKelvin, TempMinimum, TempMaximum); + _coreValue = (byte)((clampedTemp - TempMinimum) * TempResolution / (TempMaximum - TempMinimum)); + } - return true; - } - } + public void SetAtmosIsImpossible() + { + _coreValue = AtmosImpossible; + } + + public void SetVacuum() + { + _coreValue = StateVacuum; + } - [Serializable, NetSerializable] - public sealed class GasOverlayUpdateEvent : EntityEventArgs + public bool IsAtmosImpossible => _coreValue == AtmosImpossible; // Cold space, solid walls + public bool IsVacuum => _coreValue == StateVacuum; + public byte Value => _coreValue; + + /// + /// Attempts to get the air temperature in Kelvin. + /// + /// The temperature in Kelvin, if the tile has a valid temperature. + /// + /// If true and the tile is a vacuum, will be set to + /// and the method will return . + /// + /// + /// if the tile contains a valid temperature (including vacuum if is set); + /// otherwise (e.g., walls). + /// + public readonly bool TryGetTemperature(out float temperature, bool onVacuumReturnTcmb = true) + { + switch (_coreValue) { - public Dictionary> UpdatedChunks = new(); - public Dictionary> RemovedChunks = new(); + case AtmosImpossible: + temperature = 0f; + return false; + case StateVacuum when onVacuumReturnTcmb: + temperature = Atmospherics.TCMB; + return true; + case StateVacuum: + temperature = 0f; + return false; + default: + temperature = (_coreValue * TempDegreeResolution) + TempMinimum; + return true; } } + + public bool Equals(ThermalByte other) + { + return _coreValue == other._coreValue; + } + + public static bool operator ==(ThermalByte left, ThermalByte right) + { + return left.Equals(right); + } + + public static bool operator !=(ThermalByte left, ThermalByte right) + { + return !left.Equals(right); + } + + public override bool Equals(object? obj) + { + return obj is ThermalByte other && Equals(other); + } + + public override int GetHashCode() + { + return _coreValue.GetHashCode(); + } } diff --git a/Content.Shared/Atmos/ExcitedGroup.cs b/Content.Shared/Atmos/ExcitedGroup.cs new file mode 100644 index 00000000000..837763f9ba6 --- /dev/null +++ b/Content.Shared/Atmos/ExcitedGroup.cs @@ -0,0 +1,44 @@ +namespace Content.Shared.Atmos; + +/// +/// Internal Atmospherics class that stores data about a group of s +/// that are excited and need to be processed. +/// +/// Excited Groups is an optimization routine executed during LINDA +/// that bunches small groups of active s +/// together and performs equalization processing on the entire group when the group dissolves. +/// Dissolution happens when LINDA operations between the tiles decrease to very low mole deltas. +/// +public sealed class ExcitedGroup +{ + /// + /// Whether this Active Group has been disposed of. + /// Used to make sure we don't perform operations on active groups that + /// we've already dissolved. + /// + [ViewVariables] + public bool Disposed = false; + + /// + /// List of tiles that belong to this excited group. + /// + [ViewVariables] + public readonly List Tiles = new(100); + + /// + /// Cycles before this excited group will be queued for dismantling. + /// Dismantling is the process of equalizing the atmosphere + /// across all tiles in the excited group and removing the group. + /// + [ViewVariables] + public int DismantleCooldown = 0; + + /// + /// Cycles before this excited group will be allowed to break down and deactivate. + /// Breakdown occurs when the excited group is small enough and inactive enough + /// to be safely removed without equalization. Used where the mole deltas across + /// the group are very low but not high enough for an equalization to occur. + /// + [ViewVariables] + public int BreakdownCooldown = 0; +} diff --git a/Content.Shared/Atmos/GasMixtureStringRepresentation.cs b/Content.Shared/Atmos/GasMixtureStringRepresentation.cs index 942b2bdc672..33b9d9e181c 100644 --- a/Content.Shared/Atmos/GasMixtureStringRepresentation.cs +++ b/Content.Shared/Atmos/GasMixtureStringRepresentation.cs @@ -1,10 +1,19 @@ -namespace Content.Shared.Atmos; +using System.Text; + +namespace Content.Shared.Atmos; public readonly record struct GasMixtureStringRepresentation(float TotalMoles, float Temperature, float Pressure, Dictionary MolesPerGas) : IFormattable { public override string ToString() { - return $"{Temperature}K {Pressure} kPa"; + var stringBuilder = new StringBuilder(); + foreach (var (gas, moles) in MolesPerGas) + { + stringBuilder.Append($"{gas}: {moles}, "); + } + var result = stringBuilder.ToString(); + + return $"{Temperature} K, {Pressure} kPa, {result}Total Moles: {TotalMoles}"; } public string ToString(string? format, IFormatProvider? formatProvider) diff --git a/Content.Shared/Atmos/Hotspot.cs b/Content.Shared/Atmos/Hotspot.cs new file mode 100644 index 00000000000..5dad064df4b --- /dev/null +++ b/Content.Shared/Atmos/Hotspot.cs @@ -0,0 +1,57 @@ +namespace Content.Shared.Atmos; + +/// +/// Internal Atmospherics struct that stores data about a hotspot in a tile. +/// Hotspots are used to model (slow-spreading) fires and firestarters. +/// +public struct Hotspot +{ + /// + /// Whether this hotspot is currently representing fire and needs to be processed. + /// Set when the hotspot "becomes alight". This is never set to false + /// because Atmospherics will just assign + /// a new struct when the fire goes out. + /// + [ViewVariables] + public bool Valid; + + /// + /// Whether this hotspot has skipped its first process cycle. + /// AtmosphereSystem.Hotspot skips processing a hotspot beyond + /// setting it to active (for LINDA processing) the first + /// time it is processed. + /// + [ViewVariables] + public bool SkippedFirstProcess; + + /// + /// Whether this hotspot is currently using the tile for reacting and fire processing + /// instead of a fraction of the tile's air. + /// + /// When a tile is considered a hotspot, Hotspot will pull a fraction of that tile's + /// air out of the tile and perform a reaction on that air, merging it back afterward. + /// Bypassing triggers when the hotspot volume nears the tile's volume, making the system + /// use the tile's GasMixture instead of pulling a fraction out. + /// + [ViewVariables] + public bool Bypassing; + + /// + /// Current temperature of the hotspot's volume, in Kelvin. + /// + [ViewVariables] + public float Temperature; + + /// + /// Current volume of the hotspot, in liters. + /// You can think of this as the volume of the current fire in the tile. + /// + [ViewVariables] + public float Volume; + + /// + /// State for the fire sprite. + /// + [ViewVariables] + public byte State; +} diff --git a/Content.Shared/Atmos/MonstermosInfo.cs b/Content.Shared/Atmos/MonstermosInfo.cs new file mode 100644 index 00000000000..7b4b8b13470 --- /dev/null +++ b/Content.Shared/Atmos/MonstermosInfo.cs @@ -0,0 +1,146 @@ +namespace Content.Shared.Atmos; + +/// +/// Atmospherics class that stores data on tiles for Monstermos calculations and operations. +/// +public struct MonstermosInfo +{ + /// + /// The last cycle this tile was processed for monstermos calculations. + /// Used to determine if Monstermos has already processed this tile in the + /// current tick's processing run. + /// + [ViewVariables] + public int LastCycle; + + /// + /// The last global cycle (on the GridAtmosphereComponent) this tile was processed for + /// monstermos calculations. + /// Monstermos can process multiple groups, and these groups may intersect with each other. + /// This allows Monstermos to check if a tile belongs to another group that has already been processed, + /// and skip processing it again. + /// + /// Used for exploring the current area for determining tiles that should be equalized + /// using a BFS fill (see https://en.wikipedia.org/wiki/Breadth-first_search) + /// + [ViewVariables] + public long LastQueueCycle; + + /// + /// Similar to . Monstermos performs a second slow pass after the main + /// BFS fill in order to build a gradient map to determine transfer directions and amounts. + /// This field also tracks if we've already processed this tile in that slow pass so we don't re-queue it. + /// + [ViewVariables] + public long LastSlowQueueCycle; + + /// + /// Difference in the amount of moles in this tile compared to the tile's neighbors. + /// Used to determine "how strongly" air wants to flow in/out of this tile from/to its neighbors. + /// + [ViewVariables] + public float MoleDelta; + + /// + /// Number of moles that are going to be transferred in this direction during final equalization. + /// + [ViewVariables] + public float TransferDirectionEast; + + /// + /// Number of moles that are going to be transferred in this direction during final equalization. + /// + [ViewVariables] + public float TransferDirectionWest; + + /// + /// Number of moles that are going to be transferred in this direction during final equalization. + /// + [ViewVariables] + public float TransferDirectionNorth; + + /// + /// Number of moles that are going to be transferred in this direction during final equalization. + /// + [ViewVariables] + public float TransferDirectionSouth; + + /// + /// Number of moles that are going to be transferred to this tile during final equalization. + /// You can think of this as molar flow rate, or the amount of air currently flowing through this tile. + /// Used for space wind and airflow sounds during explosive decompression or big movements. + /// + /// During equalization calculations, Monstermos determines how much air is going to be transferred + /// between tiles, and sums that up into this field. It then either + /// + /// determines how many moles to transfer in the direction of , or + /// + /// determines how many moles to move in each direction using , + /// setting the TransferDirection fields accordingly based on the ratio obtained + /// from . + /// + [ViewVariables] + public float CurrentTransferAmount; + + /// + /// A pointer from the current tile to the direction in which air is being transferred the most. + /// + [ViewVariables] + public AtmosDirection CurrentTransferDirection; + + /// + /// Marks this tile as being equalized using the O(n log n) algorithm. + /// + [ViewVariables] + public bool FastDone; + + /// + /// Gets or sets the TransferDirection in the given direction. + /// + /// + /// Thrown when an invalid direction is given + /// (a non-cardinal direction) + public float this[AtmosDirection direction] + { + get => + direction switch + { + AtmosDirection.East => TransferDirectionEast, + AtmosDirection.West => TransferDirectionWest, + AtmosDirection.North => TransferDirectionNorth, + AtmosDirection.South => TransferDirectionSouth, + _ => throw new ArgumentOutOfRangeException(nameof(direction)) + }; + + set + { + switch (direction) + { + case AtmosDirection.East: + TransferDirectionEast = value; + break; + case AtmosDirection.West: + TransferDirectionWest = value; + break; + case AtmosDirection.North: + TransferDirectionNorth = value; + break; + case AtmosDirection.South: + TransferDirectionSouth = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(direction)); + } + } + } + + /// + /// Gets or sets the TransferDirection by index. + /// + /// The index of the direction + public float this[int index] + { + get => this[(AtmosDirection) (1 << index)]; + set => this[(AtmosDirection) (1 << index)] = value; + } +} diff --git a/Content.Shared/Atmos/Piping/Components/AtmosDeviceDisabledEvent.cs b/Content.Shared/Atmos/Piping/Components/AtmosDeviceDisabledEvent.cs index 6fe5d86df2b..66aa9b0bfac 100644 --- a/Content.Shared/Atmos/Piping/Components/AtmosDeviceDisabledEvent.cs +++ b/Content.Shared/Atmos/Piping/Components/AtmosDeviceDisabledEvent.cs @@ -1,7 +1,12 @@ namespace Content.Shared.Atmos.Piping.Components; /// -/// Raised directed on an atmos device when it is enabled. +/// Raised directed on an AtmosDeviceComponent when it has been removed from +/// the GridAtmosphereComponent it was attached to. +/// This can occur when it has been requested manually or when the device has been unanchored. +/// +/// Any information that you were tracking about the grid should +/// probably be cleared out here. /// [ByRefEvent] -public readonly record struct AtmosDeviceDisabledEvent; \ No newline at end of file +public readonly record struct AtmosDeviceDisabledEvent; diff --git a/Content.Shared/Atmos/Piping/Components/AtmosDeviceEnabledEvent.cs b/Content.Shared/Atmos/Piping/Components/AtmosDeviceEnabledEvent.cs index d656367245e..5f829c7d597 100644 --- a/Content.Shared/Atmos/Piping/Components/AtmosDeviceEnabledEvent.cs +++ b/Content.Shared/Atmos/Piping/Components/AtmosDeviceEnabledEvent.cs @@ -1,7 +1,13 @@ namespace Content.Shared.Atmos.Piping.Components; /// -/// Raised directed on an atmos device when it is enabled. +/// Raised directed on an AtmosDeviceComponent when it has been added to +/// a GridAtmosphereComponent. +/// This can occur when it has been requested manually or when the device has been anchored. +/// +/// You can use this to cache specific information about the grid the device is on, +/// though be careful with caching references to objects that can be removed or changed at any time +/// (such as s). /// [ByRefEvent] -public readonly record struct AtmosDeviceEnabledEvent; \ No newline at end of file +public readonly record struct AtmosDeviceEnabledEvent; diff --git a/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs b/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs new file mode 100644 index 00000000000..daedcfdaa52 --- /dev/null +++ b/Content.Shared/Atmos/Piping/Components/AtmosDeviceTileChangedEvent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Atmos.Piping.Components; + +/// +/// Raised directed on entities when the tile that they reside in has had their +/// associated TileAtmosphere changed significantly, i.e. a tile/ being added, removed, +/// or replaced. Important when atmos devices need to update any stored references to their tile's atmosphere. +/// +[ByRefEvent] +public readonly record struct AtmosDeviceTileChangedEvent; diff --git a/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs index 947ad01e1bd..e6e1f85a389 100644 --- a/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs +++ b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs @@ -1,4 +1,6 @@ -using Robust.Shared.Serialization; +using Content.Shared.Atmos.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.Atmos.Piping.Trinary.Components { @@ -48,13 +50,8 @@ public GasFilterChangeRateMessage(float rate) } [Serializable, NetSerializable] - public sealed class GasFilterSelectGasMessage : BoundUserInterfaceMessage + public sealed class GasFilterSelectGasMessage(Gas? gas) : BoundUserInterfaceMessage { - public int? ID { get; } - - public GasFilterSelectGasMessage(int? id) - { - ID = id; - } + public readonly Gas? Gas = gas; } } diff --git a/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs index 40d76684ee8..d4e55e4b572 100644 --- a/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs +++ b/Content.Shared/Atmos/Piping/Unary/Components/GasCanisterComponent.cs @@ -1,13 +1,12 @@ -using Content.Shared.Atmos.Piping.Binary.Components; +using Content.Shared.Atmos.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Guidebook; -using Robust.Shared.Audio; using Robust.Shared.GameStates; namespace Content.Shared.Atmos.Piping.Unary.Components; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] -public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder +public sealed partial class GasCanisterComponent : GasMaxPressureHolderComponent { [DataField("port")] public string PortName { get; set; } = "port"; @@ -21,38 +20,18 @@ public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder [DataField] public ItemSlot GasTankSlot = new(); - [DataField("gasMixture")] - public GasMixture Air { get; set; } = new(); - - /// - /// Last recorded pressure, for appearance-updating purposes. - /// - public float LastPressure = 0f; - /// - /// Minimum release pressure possible for the release valve. + /// The safety release valve on this gas canister. Automatically opens + /// when is reached. /// - [DataField, AutoNetworkedField] - public float MinReleasePressure = Atmospherics.OneAtmosphere / 10; - - /// - /// Maximum release pressure possible for the release valve. - /// - [DataField, AutoNetworkedField] - public float MaxReleasePressure = Atmospherics.OneAtmosphere * 10; - - /// - /// Valve release pressure. - /// - [DataField, AutoNetworkedField] - public float ReleasePressure = Atmospherics.OneAtmosphere; + [DataField] + public bool SafetyValveOpen; /// - /// Whether the release valve is open on the canister. + /// Last recorded pressure, for appearance-updating purposes. /// - [DataField, AutoNetworkedField] - public bool ReleaseValve = false; + public float LastPressure = 0f; [GuidebookData] - public float Volume => Air.Volume; + public float Volume => Air?.Volume ?? 0f; } diff --git a/Content.Shared/Atmos/Piping/Unary/Components/SharedVentScrubberComponent.cs b/Content.Shared/Atmos/Piping/Unary/Components/SharedVentScrubberComponent.cs index 6a9223a3cb8..d16d1c7f6d5 100644 --- a/Content.Shared/Atmos/Piping/Unary/Components/SharedVentScrubberComponent.cs +++ b/Content.Shared/Atmos/Piping/Unary/Components/SharedVentScrubberComponent.cs @@ -27,7 +27,11 @@ public sealed class GasVentScrubberData : IAtmosDeviceData Gas.Thermonium, /// Imperial Added Thermonium Gas.Phazonium, /// Imperial Added Phazonium and Ozonium Gas.Ozonium, /// Imperial Added Phazonium and Ozonium - Gas.Hydrogen /// Imperial Added Hydrogen + Gas.Hydrogen, /// Imperial Added Hydrogen + Gas.Deuterium, /// Imperial Atmos Update start + Gas.HyperNoblium, + Gas.AntiNoblium, + Gas.BZ /// Imperial Atmos Update end }; // Presets for 'dumb' air alarm modes diff --git a/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs b/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs index e7a8ed51319..8fed3f0eb7b 100644 --- a/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs +++ b/Content.Shared/Atmos/Piping/Unary/Systems/SharedGasCanisterSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Administration.Logs; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Piping.Binary.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; @@ -9,7 +10,7 @@ namespace Content.Shared.Atmos.Piping.Unary.Systems; -public abstract class SharedGasCanisterSystem : EntitySystem +public abstract class SharedGasCanisterSystem : GasMaxPressureSystem { [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; [Dependency] private readonly ItemSlotsSystem _slots = default!; @@ -23,6 +24,8 @@ public override void Initialize() SubscribeLocalEvent(OnCanisterContainerModified); SubscribeLocalEvent(OnCanisterInsertAttempt); SubscribeLocalEvent(OnCanisterStartup); + SubscribeLocalEvent(OnCanisterMapInit); + SubscribeLocalEvent(OnCanisterUIOpened); // Bound UI subscriptions SubscribeLocalEvent(OnHoldingTankEjectMessage); @@ -30,6 +33,19 @@ public override void Initialize() SubscribeLocalEvent(OnCanisterChangeReleaseValve); } + private void OnCanisterUIOpened(Entity ent, ref BoundUIOpenedEvent args) + { + // Fixes all canisters not populating UI elements before MapInit. Mappers rejoice + // We still need to DirtyUI after MapInit because this has latency, bad UX for players. + DirtyUI(ent.Owner, ent); + } + + private void OnCanisterMapInit(Entity ent, ref MapInitEvent args) + { + // Fixes empty canisters not populating UI elements + DirtyUI(ent.Owner, ent); + } + private void OnCanisterStartup(Entity ent, ref ComponentStartup args) { // Ensure container @@ -58,7 +74,7 @@ private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canis var item = canister.GasTankSlot.Item; _slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor, excludeUserAudio: true); - if (canister.ReleaseValve) + if (canister.ReleaseValveOpen) { AdminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere"); } @@ -88,24 +104,35 @@ private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent DirtyUI(uid, canister); } - private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args) + private void OnCanisterChangeReleaseValve(Entity entity, ref GasCanisterChangeReleaseValveMessage args) { // filling a jetpack with plasma is less important than filling a room with it - var impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High; + var impact = entity.Comp.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High; var containedGasDict = new Dictionary(); var containedGasArray = Enum.GetValues(typeof(Gas)); for (var i = 0; i < containedGasArray.Length; i++) { - containedGasDict.Add((Gas)i, canister.Air[i]); + containedGasDict.Add((Gas)i, entity.Comp.Air[i]); } - AdminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]"); + AdminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(entity):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]"); - canister.ReleaseValve = args.Valve; - Dirty(uid, canister); - DirtyUI(uid, canister); + ToggleValve(entity, args.Valve, args.Actor); + DirtyUI(entity); + } + + protected void ToggleValve(Entity entity, EntityUid? user = null) + { + ToggleValve(entity, !entity.Comp.ReleaseValveOpen, user); + } + + protected void ToggleValve(Entity entity, bool open, EntityUid? user = null) + { + entity.Comp.ReleaseValveOpen = open; + Audio.PlayPredicted(entity.Comp.ValveSound, entity, user); + Dirty(entity); } private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args) @@ -114,7 +141,7 @@ private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent compone return; // Could whitelist but we want to check if it's open so. - if (!TryComp(args.Item, out var gasTank) || gasTank.IsValveOpen) + if (!TryComp(args.Item, out var gasTank) || gasTank.ReleaseValveOpen) { args.Cancelled = true; } diff --git a/Content.Shared/Atmos/Prototypes/GasPrototype.cs b/Content.Shared/Atmos/Prototypes/GasPrototype.cs index 6a2d4078207..bb92d0307d3 100644 --- a/Content.Shared/Atmos/Prototypes/GasPrototype.cs +++ b/Content.Shared/Atmos/Prototypes/GasPrototype.cs @@ -1,87 +1,122 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Atmos.Prototypes +using Robust.Shared.Utility; + +namespace Content.Shared.Atmos.Prototypes; + +/// +/// Prototype defining a gas for atmospherics. +/// +/// +/// The total number of gases is hardcoded in a bunch of places. +/// If you add any new ones, make sure to also adjust the constants in accordingly. +/// +[Prototype] +public sealed partial class GasPrototype : IPrototype { - [Prototype] - public sealed partial class GasPrototype : IPrototype - { - [DataField("name")] public string Name { get; set; } = ""; - - // TODO: Control gas amount necessary for overlay to appear - // TODO: Add interfaces for gas behaviours e.g. breathing, burning - - [ViewVariables] - [IdDataField] - public string ID { get; private set; } = default!; - - /// - /// Specific heat for gas. - /// - [DataField("specificHeat")] - public float SpecificHeat { get; private set; } - - /// - /// Heat capacity ratio for gas - /// - [DataField("heatCapacityRatio")] - public float HeatCapacityRatio { get; private set; } = 1.4f; - - /// - /// Molar mass of gas - /// - [DataField("molarMass")] - public float MolarMass { get; set; } = 1f; - - - /// - /// Minimum amount of moles for this gas to be visible. - /// - [DataField("gasMolesVisible")] - public float GasMolesVisible { get; private set; } = 0.25f; - - /// - /// Visibility for this gas will be max after this value. - /// - public float GasMolesVisibleMax => GasMolesVisible * GasVisibilityFactor; - - [DataField("gasVisbilityFactor")] - public float GasVisibilityFactor = Atmospherics.FactorGasVisibleMax; - - /// - /// If this reagent is in gas form, this is the path to the overlay that will be used to make the gas visible. - /// - [DataField("gasOverlayTexture")] - public string GasOverlayTexture { get; private set; } = string.Empty; - - /// - /// If this reagent is in gas form, this will be the path to the RSI sprite that will be used to make the gas visible. - /// - [DataField("gasOverlayState")] - public string GasOverlayState { get; set; } = string.Empty; - - /// - /// State for the gas RSI overlay. - /// - [DataField("gasOverlaySprite")] - public string GasOverlaySprite { get; set; } = string.Empty; - - /// - /// Path to the tile overlay used when this gas appears visible. - /// - [DataField("overlayPath")] - public string OverlayPath { get; private set; } = string.Empty; - - /// - /// The reagent that this gas will turn into when inhaled. - /// - [DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string? Reagent { get; private set; } = default!; - - [DataField("color")] public string Color { get; private set; } = string.Empty; - - [DataField("pricePerMole")] - public float PricePerMole { get; set; } = 0; - } + // TODO: Add interfaces for gas behaviours e.g. breathing, burning + + /// + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The name of the gas as shown to the player. + /// + [DataField(required: true)] + public LocId Name; + + /// + /// The abbreviation of the name. For example O₂ for Oxygen. + /// Used for UI purposes. + /// + [DataField(required: true)] + public LocId Abbreviation; + + /// + /// The molar heat capacity of this gas, in J/(K * mol). + /// Describes how much heat energy is needed to heat up this gas by one Kelvin. + /// Or in other words, the higher this number is the more energy this gas can store. + /// + /// + /// This will be divided by the cvar. + /// + [DataField] + public float MolarHeatCapacity; + + /// + /// Heat capacity ratio for gas. + /// TODO: Make gas pumps do proper adiabatic compression so that this is actually used. + /// + [DataField] + public float HeatCapacityRatio = 1.4f; + + /// + /// Molar mass of the gas. + /// TODO: This is not used anywhere, do we even need this? + /// + [DataField] + public float MolarMass = 1f; + + + /// + /// Minimum amount of moles for this gas to be visible. + /// + [DataField] + public float GasMolesVisible = 0.25f; + + /// + /// Visibility for this gas will be max after this value. + /// + [ViewVariables] + public float GasMolesVisibleMax => GasMolesVisible * GasVisibilityFactor; + + /// + /// Multiplier that decides when a gas will be at maximum visibility. + /// + [DataField] + public float GasVisibilityFactor = Atmospherics.FactorGasVisibleMax; + + /// + /// Sprite to show in the gas overlay if this gas is present on a tile. + /// If null the gas will be invisible. + /// + [DataField] + public SpriteSpecifier? GasOverlaySprite; + + /// + /// The reagent that this gas will turn into when inhaled or condensed. + /// + [DataField] + public ProtoId? Reagent; + + /// + /// The color of the gas used for UI purposes. + /// + [DataField] + public Color Color = Color.White; + + /// + /// The price per mole when this gas is sold at cargo. + /// The final price will also depend on the purity of the gas mixture. + /// + [DataField] + public float PricePerMole = 0; + + /// + /// Whether the gas is considered to be flammable. + /// This is used generically across Atmospherics to determine + /// if things like hotspots are allowed to ignite if an + /// oxidizer is present. + /// + [DataField] + public bool IsFuel; + + /// + /// Whether the gas is considered to be an oxidizer. + /// Same reasoning as but for oxidizers. + /// + [DataField] + public bool IsOxidizer; } diff --git a/Content.Shared/Atmos/Rotting/BeginRottingEvent.cs b/Content.Shared/Atmos/Rotting/BeginRottingEvent.cs new file mode 100644 index 00000000000..fc612d9b3da --- /dev/null +++ b/Content.Shared/Atmos/Rotting/BeginRottingEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Atmos.Rotting; + +/// +/// Raised when an entity starts to rot. +/// +[ByRefEvent] +public record struct BeginRottingEvent; diff --git a/Content.Shared/Atmos/Rotting/PerishableComponent.cs b/Content.Shared/Atmos/Rotting/PerishableComponent.cs index 99b30fc9069..3acda1c3ffb 100644 --- a/Content.Shared/Atmos/Rotting/PerishableComponent.cs +++ b/Content.Shared/Atmos/Rotting/PerishableComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Atmos.Rotting; /// This makes mobs eventually start rotting when they die. /// It may be expanded to food at some point, but it's just for mobs right now. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] [Access(typeof(SharedRottingSystem))] public sealed partial class PerishableComponent : Component { @@ -20,7 +20,7 @@ public sealed partial class PerishableComponent : Component /// /// How much rotting has occured /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan RotAccumulator = TimeSpan.Zero; /// diff --git a/Content.Shared/Atmos/Rotting/SharedRottingSystem.cs b/Content.Shared/Atmos/Rotting/SharedRottingSystem.cs index 60c89c012a0..3007da55df0 100644 --- a/Content.Shared/Atmos/Rotting/SharedRottingSystem.cs +++ b/Content.Shared/Atmos/Rotting/SharedRottingSystem.cs @@ -25,27 +25,27 @@ public override void Initialize() SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnPerishableExamined); - SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnRottingMobStateChanged); SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnExamined); } - private void OnPerishableMapInit(EntityUid uid, PerishableComponent component, MapInitEvent args) + private void OnPerishableMapInit(Entity ent, ref MapInitEvent args) { - component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate; + ent.Comp.RotNextUpdate = _timing.CurTime + ent.Comp.PerishUpdateRate; } - private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args) + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) { if (args.NewMobState != MobState.Dead && args.OldMobState != MobState.Dead) return; - if (HasComp(uid)) + if (HasComp(ent)) return; - component.RotAccumulator = TimeSpan.Zero; - component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate; + ent.Comp.RotAccumulator = TimeSpan.Zero; + ent.Comp.RotNextUpdate = _timing.CurTime + ent.Comp.PerishUpdateRate; + DirtyField(ent.Owner, ent.Comp, nameof(PerishableComponent.RotAccumulator)); } private void OnPerishableExamined(Entity perishable, ref ExaminedEvent args) @@ -62,18 +62,11 @@ private void OnPerishableExamined(Entity perishable, ref Ex args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(perishable, EntityManager)))); } - private void OnShutdown(EntityUid uid, RottingComponent component, ComponentShutdown args) - { - if (TryComp(uid, out var perishable)) - { - perishable.RotNextUpdate = TimeSpan.Zero; - } - } - private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead) return; + RemCompDeferred(uid, component); } @@ -148,6 +141,7 @@ public void ReduceAccumulator(EntityUid uid, TimeSpan time) if (!TryComp(uid, out var rotting)) { perishable.RotAccumulator -= time; + DirtyField(uid, perishable, nameof(PerishableComponent.RotAccumulator)); return; } var total = (rotting.TotalRotTime + perishable.RotAccumulator) - time; @@ -156,8 +150,8 @@ public void ReduceAccumulator(EntityUid uid, TimeSpan time) { RemCompDeferred(uid, rotting); perishable.RotAccumulator = total; + DirtyField(uid, perishable, nameof(PerishableComponent.RotAccumulator)); } - else rotting.TotalRotTime = total - perishable.RotAfter; } diff --git a/Content.Server/Atmos/Serialization/TileAtmosCollectionSerializer.cs b/Content.Shared/Atmos/Serialization/TileAtmosCollectionSerializer.cs similarity index 99% rename from Content.Server/Atmos/Serialization/TileAtmosCollectionSerializer.cs rename to Content.Shared/Atmos/Serialization/TileAtmosCollectionSerializer.cs index c7f2c9379a0..d99cb3d2100 100644 --- a/Content.Server/Atmos/Serialization/TileAtmosCollectionSerializer.cs +++ b/Content.Shared/Atmos/Serialization/TileAtmosCollectionSerializer.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Content.Shared.Atmos; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; @@ -10,7 +9,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; using Robust.Shared.Utility; -namespace Content.Server.Atmos.Serialization; +namespace Content.Shared.Atmos.Serialization; public sealed partial class TileAtmosCollectionSerializer : ITypeSerializer, MappingDataNode>, ITypeCopier> { diff --git a/Content.Shared/Atmos/TileAtmosphere.cs b/Content.Shared/Atmos/TileAtmosphere.cs new file mode 100644 index 00000000000..9b1b5dbd622 --- /dev/null +++ b/Content.Shared/Atmos/TileAtmosphere.cs @@ -0,0 +1,247 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; + +namespace Content.Shared.Atmos; + +/// +/// Internal Atmospherics class that stores data on an atmosphere in a single tile. +/// You should not be using these directly outside of . +/// Use the public APIs in instead. +/// +[Access(typeof(SharedAtmosphereSystem), typeof(SharedGasTileOverlaySystem), typeof(SharedAtmosDebugOverlaySystem))] +public sealed class TileAtmosphere : IGasMixtureHolder +{ + /// + /// The last cycle this tile's air was archived into . + /// See for more info on archival. + /// + [ViewVariables] + public int ArchivedCycle; + + /// + /// Current cycle this tile was processed. + /// Used to prevent double-processing in a single cycle in many processing stages. + /// + [ViewVariables] + public int CurrentCycle; + + /// + /// Current temperature of this tile, in Kelvin. + /// Used for Superconduction. + /// This is not the temperature of the attached ! + /// + [ViewVariables] + public float Temperature = Atmospherics.T20C; + + /// + /// The current target tile for pressure movement for the current cycle. + /// Gas will be moved towards this tile during pressure equalization. + /// Also see . + /// + [ViewVariables] + public TileAtmosphere? PressureSpecificTarget; + + /// + /// The current pressure difference (delta) between this tile and its pressure target. + /// If Monstermos is enabled, this value represents the quantity of moles transferred. + /// + [ViewVariables] + public float PressureDifference; + + /// + /// The current heat capacity of this tile. + /// Used for Superconduction. + /// This is not the heat capacity of the attached ! + /// + [ViewVariables(VVAccess.ReadWrite)] + public float HeatCapacity = Atmospherics.MinimumHeatCapacity; + + /// + /// The current thermal conductivity of this tile. + /// Describes how well heat moves between this tile and adjacent tiles during superconduction. + /// + [ViewVariables] + public float ThermalConductivity = 0.05f; + + /// + /// Designates whether this tile is currently excited for processing in an excited group or LINDA. + /// + [ViewVariables] + public bool Excited; + + /// + /// Whether this tile should be considered space. + /// + [ViewVariables] + public bool Space; + + /// + /// Cached adjacent tiles for this tile. + /// Ordered in the same order as + /// (should be North, South, East, West). + /// Adjacent tiles can be null if air cannot flow to them. + /// + [ViewVariables] + public readonly TileAtmosphere?[] AdjacentTiles = new TileAtmosphere[Atmospherics.Directions]; + + /// + /// Neighbouring tiles to which air can flow. This is a combination of this tile's unblocked direction, and the + /// unblocked directions on adjacent tiles. + /// + [ViewVariables] + public AtmosDirection AdjacentBits = AtmosDirection.Invalid; + + /// + /// Current information for this tile. + /// + [ViewVariables] + [Access(typeof(SharedAtmosphereSystem), Other = AccessPermissions.ReadExecute)] + public MonstermosInfo MonstermosInfo; + + /// + /// Current information for this tile. + /// + [ViewVariables] + public Hotspot Hotspot; + + /// + /// Points to the direction of the recipient tile for pressure equalization logic + /// (Monstermos or HighPressureDelta otherwise). + /// + [ViewVariables] + public AtmosDirection PressureDirection; + + /// + /// Last cycle's for debugging purposes. + /// + [ViewVariables] + public AtmosDirection LastPressureDirection; + + /// + /// Grid entity this tile belongs to. + /// + [ViewVariables] + [Access(typeof(SharedAtmosphereSystem))] + public EntityUid GridIndex; + + /// + /// The grid indices of this tile. + /// + [ViewVariables] + public Vector2i GridIndices; + + /// + /// The excited group this tile belongs to, if any. + /// + [ViewVariables] + public ExcitedGroup? ExcitedGroup; + + /// + /// The air in this tile. If null, this tile is completely air-blocked. + /// This can be immutable if the tile is spaced. + /// + [ViewVariables] + [Access(typeof(SharedAtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends + public GasMixture? Air; + + /// + /// A copy of the air in this tile from the last time it was archived at . + /// LINDA archives the air before doing any necessary processing and uses this to perform its calculations, + /// making the results of LINDA independent of the order in which tiles are processed. + /// + [ViewVariables] + public GasMixture? AirArchived; + + /// + /// The amount of gas last shared to adjacent tiles during LINDA processing. + /// Used to determine when LINDA should dismantle an excited group + /// or extend its time alive. + /// + [DataField("lastShare")] + public float LastShare; + + /// + /// Implementation of . + /// + GasMixture IGasMixtureHolder.Air + { + get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature }; + set => Air = value; + } + + /// + /// The maximum temperature this tile has sustained during hotspot fire processing. + /// Used for debugging. + /// + [ViewVariables] + public float MaxFireTemperatureSustained; + + /// + /// If true, then this tile is directly exposed to the map's atmosphere, either because the grid has no tile at + /// this position, or because the tile type is not airtight. + /// + [ViewVariables] + public bool MapAtmosphere; + + /// + /// If true, this tile does not actually exist on the grid, it only exists to represent the map's atmosphere for + /// adjacent grid tiles. + /// This tile often has immutable air and is sitting off the edge of the grid, where there is no grid. + /// + [ViewVariables] + public bool NoGridTile; + + /// + /// If true, this tile is queued for processing in + /// + [ViewVariables] + public bool TrimQueued; + + /// + /// Cached information about airtight entities on this tile. This gets updated anytime a tile gets invalidated + /// (i.e., gets added to ). + /// + public AirtightData AirtightData; + + /// + /// Creates a new TileAtmosphere. + /// + /// The grid entity this tile belongs to. + /// >The grid indices of this tile. + /// The gas mixture of this tile. + /// If true, the gas mixture will be marked immutable. + /// If true, this tile is considered space. + public TileAtmosphere(EntityUid gridIndex, Vector2i gridIndices, GasMixture? mixture = null, bool immutable = false, bool space = false) + { + GridIndex = gridIndex; + GridIndices = gridIndices; + Air = mixture; + AirArchived = Air?.Clone(); + Space = space; + + if(immutable) + Air?.MarkImmutable(); + } + + /// + /// Creates a copy of another TileAtmosphere. + /// + /// The TileAtmosphere to copy. + public TileAtmosphere(TileAtmosphere other) + { + GridIndex = other.GridIndex; + GridIndices = other.GridIndices; + Space = other.Space; + NoGridTile = other.NoGridTile; + MapAtmosphere = other.MapAtmosphere; + Air = other.Air?.Clone(); + AirArchived = Air != null ? Air.Clone() : null; + } + + /// + /// Creates a new empty TileAtmosphere. + /// + public TileAtmosphere() + { + } +} diff --git a/Content.Shared/Atmos/TileFireEvent.cs b/Content.Shared/Atmos/TileFireEvent.cs new file mode 100644 index 00000000000..f0045f58fdc --- /dev/null +++ b/Content.Shared/Atmos/TileFireEvent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Atmos; + +/// +/// Event raised on an entity when it is standing on a tile that's on fire. +/// +/// Current temperature of the hotspot this entity is exposed to. +/// Current volume of the hotspot this entity is exposed to. +/// This is not the volume of the tile this entity is on. +[ByRefEvent] +public readonly record struct TileFireEvent(float Temperature, float Volume); diff --git a/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs b/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs index 1a8f9cb3bb8..aa37e604087 100644 --- a/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs +++ b/Content.Shared/Audio/Jukebox/SharedJukeboxSystem.cs @@ -5,4 +5,15 @@ namespace Content.Shared.Audio.Jukebox; public abstract class SharedJukeboxSystem : EntitySystem { [Dependency] protected readonly SharedAudioSystem Audio = default!; + + /// + /// Returns whether or not the given jukebox is currently playing a song. + /// + public bool IsPlaying(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + return entity.Comp.AudioStream is { } audio && Audio.IsPlaying(audio); + } } diff --git a/Content.Shared/Audio/SharedAmbientSoundSystem.cs b/Content.Shared/Audio/SharedAmbientSoundSystem.cs index 0a52b7c58e2..fd5a474a70a 100644 --- a/Content.Shared/Audio/SharedAmbientSoundSystem.cs +++ b/Content.Shared/Audio/SharedAmbientSoundSystem.cs @@ -6,15 +6,13 @@ namespace Content.Shared.Audio; public abstract class SharedAmbientSoundSystem : EntitySystem { - private EntityQuery _query; + [Dependency] private readonly EntityQuery _query = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(GetCompState); SubscribeLocalEvent(HandleCompState); - - _query = GetEntityQuery(); } public virtual void SetAmbience(EntityUid uid, bool value, AmbientSoundComponent? ambience = null) diff --git a/Content.Shared/BarSign/BarSignComponent.cs b/Content.Shared/BarSign/BarSignComponent.cs index 98c6e815cc8..a3a6cd71a46 100644 --- a/Content.Shared/BarSign/BarSignComponent.cs +++ b/Content.Shared/BarSign/BarSignComponent.cs @@ -4,6 +4,10 @@ namespace Content.Shared.BarSign; +/// +/// Makes it possible to switch this entity's sprite and name using a BUI. +/// +/// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class BarSignComponent : Component { @@ -12,16 +16,40 @@ public sealed partial class BarSignComponent : Component /// [DataField, AutoNetworkedField] public ProtoId? Current; + + /// + /// The bar sign fallback state upon being EMPed. + /// + [DataField, AutoNetworkedField] + public ProtoId? Emped = "EmpBarSign"; } +/// +/// The key for the BoundUserInterface. +/// [Serializable, NetSerializable] public enum BarSignUiKey : byte { Key } +/// +/// The enum to be used for appearance data of the bar sign. +/// +[Serializable, NetSerializable] +public enum BarSignVisuals : byte +{ + BarSignPrototype, +} + +/// +/// Send from the client when setting the bar sign. +/// [Serializable, NetSerializable] public sealed class SetBarSignMessage(ProtoId sign) : BoundUserInterfaceMessage { + /// + /// The new prototype to use. + /// public ProtoId Sign = sign; } diff --git a/Content.Shared/BarSign/BarSignSystem.cs b/Content.Shared/BarSign/BarSignSystem.cs index 0c9770bfe98..042120663e1 100644 --- a/Content.Shared/BarSign/BarSignSystem.cs +++ b/Content.Shared/BarSign/BarSignSystem.cs @@ -1,4 +1,6 @@ +using Content.Shared.Emp; using System.Linq; +using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -9,45 +11,94 @@ public sealed class BarSignSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + public override void Initialize() { SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnAfterAutoHandleState); Subs.BuiEvents(BarSignUiKey.Key, subs => { subs.Event(OnSetBarSignMessage); }); + + SubscribeLocalEvent(OnEmpPulse); + SubscribeLocalEvent(OnBoundUIAttempt); } private void OnMapInit(Entity ent, ref MapInitEvent args) { - if (ent.Comp.Current != null) + BarSignPrototype? newPrototype; + if (ent.Comp.Current is null) + newPrototype = _random.Pick(GetAllBarSigns(_prototypeManager)); + else if (!_prototypeManager.Resolve(ent.Comp.Current, out newPrototype)) return; - var newPrototype = _random.Pick(GetAllBarSigns(_prototypeManager)); SetBarSign(ent, newPrototype); } + private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + // Update the UI if the component was changed. + if (_ui.TryGetOpenUi(ent.Owner, BarSignUiKey.Key, out var bui)) + bui.Update(); + } + private void OnSetBarSignMessage(Entity ent, ref SetBarSignMessage args) { if (!_prototypeManager.Resolve(args.Sign, out var signPrototype)) return; + if (signPrototype.Hidden) + return; // Hidden signs cannot be selected from the BUI. + SetBarSign(ent, signPrototype); } + private void OnEmpPulse(Entity ent, ref EmpPulseEvent args) + { + if (!_prototypeManager.Resolve(ent.Comp.Emped, out var empedPrototype)) + return; + + SetBarSign(ent, empedPrototype); + args.Affected = true; + args.Disabled = true; + } + + private void OnBoundUIAttempt(Entity ent, ref BoundUserInterfaceMessageAttempt args) + { + if (HasComp(ent)) + args.Cancel(); + } + + /// + /// Set the sprite, name and description of the bar sign to a given . + /// public void SetBarSign(Entity ent, BarSignPrototype newPrototype) { + if (HasComp(ent)) + return; + var meta = MetaData(ent); var name = Loc.GetString(newPrototype.Name); _metaData.SetEntityName(ent, name, meta); _metaData.SetEntityDescription(ent, Loc.GetString(newPrototype.Description), meta); + _appearance.SetData(ent.Owner, BarSignVisuals.BarSignPrototype, newPrototype.ID); ent.Comp.Current = newPrototype.ID; Dirty(ent); + + // Predict updating the BUI if it's open. + if (_ui.TryGetOpenUi(ent.Owner, BarSignUiKey.Key, out var bui)) + bui.Update(); } + /// + /// Returns a list of all s that are not hidden. + /// public static List GetAllBarSigns(IPrototypeManager prototypeManager) { return prototypeManager diff --git a/Content.Shared/Bed/BedSystem.cs b/Content.Shared/Bed/BedSystem.cs new file mode 100644 index 00000000000..3a25b6e624a --- /dev/null +++ b/Content.Shared/Bed/BedSystem.cs @@ -0,0 +1,168 @@ +using Content.Shared.Actions; +using Content.Shared.Bed.Components; +using Content.Shared.Bed.Sleep; +using Content.Shared.Body.Events; +using Content.Shared.Buckle.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Emag.Systems; +using Content.Shared.Imperial.Medieval.Skills; +using Content.Shared.Metabolism; +using Content.Shared.Mobs.Systems; +using Content.Shared.Power; +using Content.Shared.Power.EntitySystems; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Bed; + +public sealed class BedSystem : EntitySystem +{ + [Dependency] private readonly ActionContainerSystem _actConts = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MetabolizerSystem _metabolizer = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!; + [Dependency] private readonly SleepingSystem _sleepingSystem = default!; + + [Dependency] private readonly EntityQuery _sleepingQuery = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHealMapInit); + SubscribeLocalEvent(OnStrapped); // Imperial Medieval + SubscribeLocalEvent(OnUnstrapped); + + SubscribeLocalEvent(OnStasisStrapped); + SubscribeLocalEvent(OnStasisUnstrapped); + SubscribeLocalEvent(OnStasisEmagged); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnStasisGetMetabolicMultiplier); + } + + private void OnHealMapInit(Entity ent, ref MapInitEvent args) + { + _actConts.EnsureAction(ent.Owner, ref ent.Comp.SleepAction, SleepingSystem.SleepActionId); + Dirty(ent); + } + + private void OnStrapped(Entity bed, ref StrappedEvent args) + { + EnsureComp(bed); + bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime); + _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed); + // Imperial Medieval sleep start + if (TryComp(args.Buckle, out var skills)) + _actionsSystem.SetCooldown(bed.Comp.SleepAction, TimeSpan.FromSeconds(10 - (skills.Levels[SharedSkillsSystem.VitalityId] - 10) * 0.5)); + // Imperial Medieval sleep end + Dirty(bed); + + // Single action entity, cannot strap multiple entities to the same bed. + DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1); + } + + private void OnUnstrapped(Entity bed, ref UnstrappedEvent args) + { + // If the entity being unbuckled is terminating, we shouldn't try to act upon it, as some components may be gone + if (!Terminating(args.Buckle.Owner)) + { + _actionsSystem.RemoveAction(args.Buckle.Owner, bed.Comp.SleepAction); + _sleepingSystem.TryWaking(args.Buckle.Owner); + } + + RemComp(bed); + } + + private void OnStasisStrapped(Entity ent, ref StrappedEvent args) + { + EnsureComp(args.Buckle); + _metabolizer.UpdateMetabolicMultiplier(args.Buckle); + } + + private void OnStasisUnstrapped(Entity ent, ref UnstrappedEvent args) + { + RemComp(ent); + _metabolizer.UpdateMetabolicMultiplier(args.Buckle); + } + + private void OnStasisEmagged(Entity ent, ref GotEmaggedEvent args) + { + if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) + return; + + if (_emag.CheckFlag(ent, EmagType.Interaction)) + return; + + ent.Comp.Multiplier = 1f / ent.Comp.Multiplier; + UpdateMetabolisms(ent.Owner); + Dirty(ent); + + args.Handled = true; + } + + private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) + { + UpdateMetabolisms(ent.Owner); + } + + private void OnStasisGetMetabolicMultiplier(Entity ent, ref GetMetabolicMultiplierEvent args) + { + if (!TryComp(ent, out var buckle) || buckle.BuckledTo is not { } buckledTo) + return; + + if (!TryComp(buckledTo, out var stasis)) + return; + + if (!_powerReceiver.IsPowered(buckledTo)) + return; + + args.Multiplier *= stasis.Multiplier; + } + + private void UpdateMetabolisms(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + foreach (var buckledEntity in ent.Comp.BuckledEntities) + { + _metabolizer.UpdateMetabolicMultiplier(buckledEntity); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var bedComponent, out var strapComponent)) + { + if (_timing.CurTime < bedComponent.NextHealTime) + continue; + + bedComponent.NextHealTime += TimeSpan.FromSeconds(bedComponent.HealTime); + + Dirty(uid, bedComponent); + + if (strapComponent.BuckledEntities.Count == 0) + continue; + + foreach (var healedEntity in strapComponent.BuckledEntities) + { + if (_mobStateSystem.IsDead(healedEntity)) + continue; + + var damage = bedComponent.Damage; + + if (_sleepingQuery.HasComp(healedEntity)) + damage *= bedComponent.SleepMultiplier; + + _damageableSystem.TryChangeDamage(healedEntity, damage, true, origin: uid); + } + } + } +} diff --git a/Content.Shared/Bed/Components/StasisBedBuckledComponent.cs b/Content.Shared/Bed/Components/StasisBedBuckledComponent.cs index 3a1c991b1ea..09759f71efa 100644 --- a/Content.Shared/Bed/Components/StasisBedBuckledComponent.cs +++ b/Content.Shared/Bed/Components/StasisBedBuckledComponent.cs @@ -6,5 +6,5 @@ namespace Content.Shared.Bed.Components; /// Tracking component added to entities buckled to stasis beds. /// [RegisterComponent, NetworkedComponent] -[Access(typeof(SharedBedSystem))] +[Access(typeof(BedSystem))] public sealed partial class StasisBedBuckledComponent : Component; diff --git a/Content.Shared/Bed/Components/StasisBedComponent.cs b/Content.Shared/Bed/Components/StasisBedComponent.cs index 97936261d60..fdc720c34fd 100644 --- a/Content.Shared/Bed/Components/StasisBedComponent.cs +++ b/Content.Shared/Bed/Components/StasisBedComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Bed.Components; /// A that modifies a strapped entity's metabolic rate by the given multiplier /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedBedSystem))] +[Access(typeof(BedSystem))] public sealed partial class StasisBedComponent : Component { /// diff --git a/Content.Shared/Bed/SharedBedSystem.cs b/Content.Shared/Bed/SharedBedSystem.cs deleted file mode 100644 index 4616bf177bd..00000000000 --- a/Content.Shared/Bed/SharedBedSystem.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Bed.Components; -using Content.Shared.Bed.Sleep; -using Content.Shared.Body.Events; -using Content.Shared.Body.Systems; -using Content.Shared.Buckle.Components; -using Content.Shared.Emag.Systems; -using Content.Shared.Imperial.Medieval.Skills; -using Content.Shared.Power; -using Content.Shared.Power.EntitySystems; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Shared.Bed; - -public abstract class SharedBedSystem : EntitySystem -{ - [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] private readonly ActionContainerSystem _actConts = default!; - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] private readonly EmagSystem _emag = default!; - [Dependency] private readonly SharedMetabolizerSystem _metabolizer = default!; - [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!; - [Dependency] private readonly SleepingSystem _sleepingSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnHealMapInit); - SubscribeLocalEvent(OnUnstrapped); - - SubscribeLocalEvent(OnStasisStrapped); - SubscribeLocalEvent(OnStasisUnstrapped); - SubscribeLocalEvent(OnStasisEmagged); - SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnStasisGetMetabolicMultiplier); - } - - private void OnHealMapInit(Entity ent, ref MapInitEvent args) - { - _actConts.EnsureAction(ent.Owner, ref ent.Comp.SleepAction, SleepingSystem.SleepActionId); - Dirty(ent); - } - - - private void OnUnstrapped(Entity bed, ref UnstrappedEvent args) - { - // If the entity being unbuckled is terminating, we shouldn't try to act upon it, as some components may be gone - if (!Terminating(args.Buckle.Owner)) - { - _actionsSystem.RemoveAction(args.Buckle.Owner, bed.Comp.SleepAction); - _sleepingSystem.TryWaking(args.Buckle.Owner); - } - - RemComp(bed); - } - - private void OnStasisStrapped(Entity ent, ref StrappedEvent args) - { - EnsureComp(args.Buckle); - _metabolizer.UpdateMetabolicMultiplier(args.Buckle); - } - - private void OnStasisUnstrapped(Entity ent, ref UnstrappedEvent args) - { - RemComp(ent); - _metabolizer.UpdateMetabolicMultiplier(args.Buckle); - } - - private void OnStasisEmagged(Entity ent, ref GotEmaggedEvent args) - { - if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) - return; - - if (_emag.CheckFlag(ent, EmagType.Interaction)) - return; - - ent.Comp.Multiplier = 1f / ent.Comp.Multiplier; - UpdateMetabolisms(ent.Owner); - Dirty(ent); - - args.Handled = true; - } - - private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) - { - UpdateMetabolisms(ent.Owner); - } - - private void OnStasisGetMetabolicMultiplier(Entity ent, ref GetMetabolicMultiplierEvent args) - { - if (!TryComp(ent, out var buckle) || buckle.BuckledTo is not { } buckledTo) - return; - - if (!TryComp(buckledTo, out var stasis)) - return; - - if (!_powerReceiver.IsPowered(buckledTo)) - return; - - args.Multiplier *= stasis.Multiplier; - } - - protected void UpdateMetabolisms(Entity ent) - { - if (!Resolve(ent, ref ent.Comp, false)) - return; - - foreach (var buckledEntity in ent.Comp.BuckledEntities) - { - _metabolizer.UpdateMetabolicMultiplier(buckledEntity); - } - } -} diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index a40e5a15d3f..b445e9586d2 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Damage; using Content.Shared.Damage.Events; using Content.Shared.Damage.ForceSay; +using Content.Shared.Damage.Systems; using Content.Shared.Emoting; using Content.Shared.Examine; using Content.Shared.Eye.Blinding.Systems; @@ -15,6 +16,7 @@ using Content.Shared.Mobs.Components; using Content.Shared.Pointing; using Content.Shared.Popups; +using Content.Shared.Rejuvenate; using Content.Shared.Slippery; using Content.Shared.Sound; using Content.Shared.Sound.Components; @@ -58,7 +60,9 @@ public override void Initialize() SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnZombified); SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnCompInit); + SubscribeLocalEvent(OnComponentRemoved); + SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(OnSeeAttempt); SubscribeLocalEvent(OnPointAttempt); @@ -101,6 +105,12 @@ private void OnSleepAction(Entity ent, ref SleepActionEvent a TrySleeping((ent, ent.Comp)); } + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + // WAKE UP!!! + RemComp(ent); + } + /// /// when sleeping component is added or removed, we do some stuff with other components. /// @@ -134,7 +144,7 @@ private void OnSleepStateChanged(Entity ent, ref SleepStateCh RemComp(ent); } - private void OnMapInit(Entity ent, ref MapInitEvent args) + private void OnCompInit(Entity ent, ref ComponentInit args) { var ev = new SleepStateChangedEvent(true); RaiseLocalEvent(ent, ref ev); @@ -147,6 +157,16 @@ private void OnMapInit(Entity ent, ref MapInitEvent args) } + private void OnComponentRemoved(Entity ent, ref ComponentRemove args) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.WakeAction); + + var ev = new SleepStateChangedEvent(false); + RaiseLocalEvent(ent, ref ev); + + _blindableSystem.UpdateIsBlind(ent.Owner); + } + private void OnSpeakAttempt(Entity ent, ref SpeakAttemptEvent args) { // TODO reduce duplication of this behavior with MobStateSystem somehow @@ -274,17 +294,6 @@ private void OnStatusEffectApplied(Entity e TrySleeping(args.Target); } - private void Wake(Entity ent) - { - RemComp(ent); - _actionsSystem.RemoveAction(ent.Owner, ent.Comp.WakeAction); - - var ev = new SleepStateChangedEvent(false); - RaiseLocalEvent(ent, ref ev); - - _blindableSystem.UpdateIsBlind(ent.Owner); - } - /// /// Try sleeping. Only mobs can sleep. /// @@ -344,8 +353,7 @@ public bool TryWaking(Entity ent, bool force = false, Entity _popupSystem.PopupClient(Loc.GetString("wake-other-success", ("target", Identity.Entity(ent, EntityManager))), ent, user); } - Wake((ent, ent.Comp)); - return true; + return RemComp(ent); } /// diff --git a/Content.Shared/Blocking/BlockingSystem.User.cs b/Content.Shared/Blocking/BlockingSystem.User.cs index 30965fb7a9f..2aa05ac64cc 100644 --- a/Content.Shared/Blocking/BlockingSystem.User.cs +++ b/Content.Shared/Blocking/BlockingSystem.User.cs @@ -1,6 +1,7 @@ +using System.Linq; using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Robust.Shared.Audio; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Content.Shared.Imperial.Medieval.Skills; // --- IMPERIAL MEDIEVAL @@ -43,36 +44,37 @@ private void OnAnchorChanged(EntityUid uid, BlockingUserComponent component, ref private void OnUserDamageModified(EntityUid uid, BlockingUserComponent component, DamageModifyEvent args) { - if (TryComp(component.BlockingItem, out var blocking)) + if (component.BlockingItem is not { } item || !TryComp(item, out var blocking)) + return; + + if (args.Damage.GetTotal() <= 0) + return; + + // A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it. + if (!TryComp(item, out var dmgComp)) + return; + + var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction; + // Imperial Medieval Start + var ev = new GetShieldBlockFractionEvent(item, uid, blockFraction); + RaiseLocalEvent(uid, ref ev); + blockFraction = ev.BlockFraction; + // Imperial Medieval End + var modifier = blocking.IsBlocking ? blocking.ActiveBlockDamageModifier : blocking.PassiveBlockDamageModifer; + blockFraction = Math.Clamp(blockFraction, 0, 1); + _damageable.TryChangeDamage((item, dmgComp), blockFraction * args.OriginalDamage); + + var modify = new DamageModifierSet(); + foreach (var key in modifier.Coefficients.Keys.Concat(modifier.FlatReduction.Keys)) { - if (args.Damage.GetTotal() <= 0) - return; - - // A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it. - if (!TryComp(component.BlockingItem, out var dmgComp)) - return; - - var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction; - // --- IMPERIAL MEDIEVAL START --- - var ev = new GetShieldBlockFractionEvent(component.BlockingItem.Value, uid, blockFraction); - RaiseLocalEvent(uid, ref ev); - blockFraction = ev.BlockFraction; - // --- IMPERIAL MEDIEVAL END -- - blockFraction = Math.Clamp(blockFraction, 0, 1); - _damageable.TryChangeDamage(component.BlockingItem, blockFraction * args.OriginalDamage); - - var modify = new DamageModifierSet(); - foreach (var key in dmgComp.Damage.DamageDict.Keys) - { - modify.Coefficients.TryAdd(key, 1 - blockFraction); - } - - args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify); - - if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage)) - { - _audio.PlayPvs(blocking.BlockSound, uid); - } + modify.Coefficients.TryAdd(key, 1 - blockFraction); + } + + args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify); + + if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage)) + { + _audio.PlayPvs(blocking.BlockSound, uid); } } @@ -97,14 +99,13 @@ private void OnEntityTerminating(EntityUid uid, BlockingUserComponent component, } /// - /// Check for the shield and has the user stop blocking - /// Used where you'd like the user to stop blocking, but also don't want to remove the + /// Called when the user is somehow terminating. Used for items that are held by a user and can be blocked with. /// - /// The user blocking - /// The private void UserStopBlocking(EntityUid uid, BlockingUserComponent component) { - if (TryComp(component.BlockingItem, out var blockComp) && blockComp.IsBlocking) - StopBlocking(component.BlockingItem.Value, blockComp, uid); + if (TryComp(component.BlockingItem, out var blockingComponent) && blockingComponent.IsBlocking) + { + StopBlocking(component.BlockingItem.Value, blockingComponent, uid); + } } } diff --git a/Content.Shared/Blocking/BlockingSystem.cs b/Content.Shared/Blocking/BlockingSystem.cs index 4ee134395a0..454f7aaf314 100644 --- a/Content.Shared/Blocking/BlockingSystem.cs +++ b/Content.Shared/Blocking/BlockingSystem.cs @@ -35,6 +35,11 @@ public sealed partial class BlockingSystem : EntitySystem [Dependency] private readonly IEntitySystemManager _systems = default!; // Imperial Medieval CombatStance [Dependency] private readonly TurfSystem _turf = default!; + [Dependency] private readonly EntityQuery _blockQuery = default!; + [Dependency] private readonly EntityQuery _handQuery = default!; + [Dependency] private readonly EntityQuery _mobQuery = default!; + [Dependency] private readonly EntityQuery _userQuery = default!; + public override void Initialize() { base.Initialize(); @@ -93,10 +98,7 @@ private void OnToggleAction(EntityUid uid, BlockingComponent component, ToggleAc if (args.Handled) return; - var blockQuery = GetEntityQuery(); - var handQuery = GetEntityQuery(); - - if (!handQuery.TryGetComponent(args.Performer, out var hands)) + if (!_handQuery.TryGetComponent(args.Performer, out var hands)) return; var shields = _handsSystem.EnumerateHeld((args.Performer, hands)).ToArray(); @@ -106,7 +108,7 @@ private void OnToggleAction(EntityUid uid, BlockingComponent component, ToggleAc if (shield == uid) continue; - if (blockQuery.TryGetComponent(shield, out var otherBlockComp) && otherBlockComp.IsBlocking) + if (_blockQuery.TryGetComponent(shield, out var otherBlockComp) && otherBlockComp.IsBlocking) { CantBlockError(args.Performer); return; @@ -175,7 +177,7 @@ public bool StartBlocking(EntityUid item, BlockingComponent component, EntityUid var mobQuery = GetEntityQuery(); foreach (var uid in intersecting) { - if (uid != user && mobQuery.HasComponent(uid)) + if (uid != user && _mobQuery.HasComponent(uid)) { TooCloseError(user); return false; @@ -247,7 +249,7 @@ public bool StopBlocking(EntityUid item, BlockingComponent component, EntityUid if (TryComp(user, out var blockingUserComponent) && TryComp(user, out var physicsComponent)) { if (xform.Anchored) - _transformSystem.Unanchor(user, xform); + _transformSystem.Unanchor(user, xform, false); _actionsSystem.SetToggled(component.BlockingToggleActionEntity, false); _fixtureSystem.DestroyFixture(user, BlockingComponent.BlockFixtureID, body: physicsComponent); @@ -273,17 +275,14 @@ private void StopBlockingHelper(EntityUid uid, BlockingComponent component, Enti if (component.IsBlocking) StopBlocking(uid, component, user); - var userQuery = GetEntityQuery(); - var handQuery = GetEntityQuery(); - - if (!handQuery.TryGetComponent(user, out var hands)) + if (!_handQuery.TryGetComponent(user, out var hands)) return; var shields = _handsSystem.EnumerateHeld((user, hands)).ToArray(); foreach (var shield in shields) { - if (HasComp(shield) && userQuery.TryGetComponent(user, out var blockingUserComponent)) + if (HasComp(shield) && _userQuery.TryGetComponent(user, out var blockingUserComponent)) { blockingUserComponent.BlockingItem = shield; return; diff --git a/Content.Shared/Body/BodyComponent.cs b/Content.Shared/Body/BodyComponent.cs new file mode 100644 index 00000000000..1e0776739d8 --- /dev/null +++ b/Content.Shared/Body/BodyComponent.cs @@ -0,0 +1,46 @@ +using Robust.Shared.Containers; +using Robust.Shared.GameStates; + +namespace Content.Shared.Body; + +/// +/// Component on the entity that "has" a body, and that oversees entities with the inside it. +/// +/// +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(BodySystem))] +public sealed partial class BodyComponent : Component +{ + public const string ContainerID = "body_organs"; + + /// + /// The actual container with entities with in it + /// + [ViewVariables] + public Container? Organs; +} + +/// +/// Raised on organ entity, when it is inserted into a body +/// +[ByRefEvent] +public readonly record struct OrganGotInsertedEvent(EntityUid Target); + +/// +/// Raised on organ entity, when it is removed from a body +/// +[ByRefEvent] +public readonly record struct OrganGotRemovedEvent(EntityUid Target); + +/// +/// Raised on body entity, when an organ is inserted into it +/// +[ByRefEvent] +public readonly record struct OrganInsertedIntoEvent(EntityUid Organ); + +/// +/// Raised on body entity, when an organ is removed from it +/// +[ByRefEvent] +public readonly record struct OrganRemovedFromEvent(EntityUid Organ); diff --git a/Content.Shared/Body/BodySystem.Compatibility.cs b/Content.Shared/Body/BodySystem.Compatibility.cs new file mode 100644 index 00000000000..82d1b6a7eb4 --- /dev/null +++ b/Content.Shared/Body/BodySystem.Compatibility.cs @@ -0,0 +1,31 @@ +using JetBrains.Annotations; + +namespace Content.Shared.Body; + +public sealed partial class BodySystem +{ + /// + /// Returns a list of organs with a given component in the body. + /// This is only provided to ease migration from the older BodySystem and should not be used in new code. + /// + /// The body to query. + /// The set of organs with the given component. + /// The component to test for. + /// Whether any organs were returned. + [Obsolete("Use an event-relay based approach instead")] + [PublicAPI] + public bool TryGetOrgansWithComponent(Entity ent, out List> organs) where TComp : Component + { + organs = new(); + if (!_bodyQuery.Resolve(ent, ref ent.Comp)) + return false; + + foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + { + if (TryComp(organ, out var comp)) + organs.Add((organ, comp)); + } + + return organs.Count != 0; + } +} diff --git a/Content.Shared/Body/BodySystem.Relay.cs b/Content.Shared/Body/BodySystem.Relay.cs new file mode 100644 index 00000000000..68dbe4f22a7 --- /dev/null +++ b/Content.Shared/Body/BodySystem.Relay.cs @@ -0,0 +1,71 @@ +using Content.Shared.Body.Events; +using Content.Shared.Gibbing; +using Content.Shared.Humanoid; +using Content.Shared.Medical; +using JetBrains.Annotations; + +namespace Content.Shared.Body; + +public sealed partial class BodySystem +{ + // Refrain from adding an infinite block of relays here - consuming systems can use RelayEvent + private void InitializeRelay() + { + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + SubscribeLocalEvent(RefRelayBodyEvent); + } + + private void RefRelayBodyEvent(EntityUid uid, BodyComponent component, ref T args) where T : struct + { + RelayEvent((uid, component), ref args); + } + + private void RelayBodyEvent(EntityUid uid, BodyComponent component, T args) where T : class + { + RelayEvent((uid, component), args); + } + + /// + /// Relays the given event to organs within a body. + /// + /// The body to relay the event within + /// The event to relay + /// The type of the event + [PublicAPI] + public void RelayEvent(Entity ent, ref T args) where T : struct + { + var ev = new BodyRelayedEvent(ent, args); + foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + { + RaiseLocalEvent(organ, ref ev); + } + args = ev.Args; + } + + /// + /// Relays the given event to organs within a body. + /// + /// The body to relay the event within + /// The event to relay + /// The type of the event + [PublicAPI] + public void RelayEvent(Entity ent, T args) where T : class + { + var ev = new BodyRelayedEvent(ent, args); + foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + { + RaiseLocalEvent(organ, ref ev); + } + } +} + +/// +/// Event wrapper for events being relayed to organs within a body. +/// +[ByRefEvent] +public record struct BodyRelayedEvent(Entity Body, TEvent Args); diff --git a/Content.Shared/Body/BodySystem.cs b/Content.Shared/Body/BodySystem.cs new file mode 100644 index 00000000000..3f01b4e2ba8 --- /dev/null +++ b/Content.Shared/Body/BodySystem.cs @@ -0,0 +1,97 @@ +using Content.Shared.DragDrop; +using Robust.Shared.Containers; + +namespace Content.Shared.Body; + +/// +/// System responsible for coordinating entities with and their entities with . +/// This system is primarily responsible for event relaying and the relationships between a body and its organs. +/// It is not responsible for player-facing body features, e.g. "blood" or "breathing." +/// Such features should be implemented in systems relying on the various events raised by this class. +/// +/// +/// +/// +/// +/// +public sealed partial class BodySystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _container = default!; + + [Dependency] private readonly EntityQuery _bodyQuery = default!; + [Dependency] private readonly EntityQuery _organQuery = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBodyInit); + SubscribeLocalEvent(OnBodyShutdown); + + SubscribeLocalEvent(OnCanDrag); + + SubscribeLocalEvent(OnBodyEntInserted); + SubscribeLocalEvent(OnBodyEntRemoved); + + InitializeRelay(); + } + + private void OnBodyInit(Entity ent, ref ComponentInit args) + { + ent.Comp.Organs = + _container.EnsureContainer(ent, BodyComponent.ContainerID); + } + + private void OnBodyShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.Organs is { } organs) + _container.ShutdownContainer(organs); + } + + private void OnBodyEntInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != BodyComponent.ContainerID) + return; + + if (!_organQuery.TryComp(args.Entity, out var organ)) + return; + + var body = new OrganInsertedIntoEvent(args.Entity); + RaiseLocalEvent(ent, ref body); + + var ev = new OrganGotInsertedEvent(ent); + RaiseLocalEvent(args.Entity, ref ev); + + if (organ.Body != ent) + { + organ.Body = ent; + Dirty(args.Entity, organ); + } + } + + private void OnBodyEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != BodyComponent.ContainerID) + return; + + if (!_organQuery.TryComp(args.Entity, out var organ)) + return; + + var body = new OrganRemovedFromEvent(args.Entity); + RaiseLocalEvent(ent, ref body); + + var ev = new OrganGotRemovedEvent(ent); + RaiseLocalEvent(args.Entity, ref ev); + + if (organ.Body == null) + return; + + organ.Body = null; + Dirty(args.Entity, organ); + } + + private void OnCanDrag(Entity ent, ref CanDragEvent args) + { + args.Handled = true; + } +} diff --git a/Content.Shared/Body/Components/BloodstreamComponent.cs b/Content.Shared/Body/Components/BloodstreamComponent.cs index 7cab5ee344b..35883c442df 100644 --- a/Content.Shared/Body/Components/BloodstreamComponent.cs +++ b/Content.Shared/Body/Components/BloodstreamComponent.cs @@ -20,9 +20,9 @@ namespace Content.Shared.Body.Components; [Access(typeof(SharedBloodstreamSystem))] public sealed partial class BloodstreamComponent : Component { - public const string DefaultChemicalsSolutionName = "chemicals"; public const string DefaultBloodSolutionName = "bloodstream"; public const string DefaultBloodTemporarySolutionName = "bloodstreamTemporary"; + public const string DefaultMetabolitesSolutionName = "metabolites"; /// /// The next time that blood level will be updated and bloodloss damage dealt. @@ -139,38 +139,32 @@ public sealed partial class BloodstreamComponent : Component // TODO probably damage bleed thresholds. /// - /// Max volume of internal chemical solution storage + /// Modifier applied to to determine maximum volume for bloodstream. /// - [DataField] - public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250); - - /// - /// Max volume of internal blood storage, - /// and starting level of blood. - /// - [DataField] - public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300); + [DataField, AutoNetworkedField] + public float MaxVolumeModifier = 2f; /// - /// Which reagent is considered this entities 'blood'? + /// Defines which reagents are considered as 'blood' and how much of it is normal. /// /// /// Slime-people might use slime as their blood or something like that. /// [DataField, AutoNetworkedField] - public ProtoId BloodReagent = "Blood"; + public Solution BloodReferenceSolution = new([new("Blood", 300)]); /// - /// Name/Key that is indexed by. + /// Caches the blood data of an entity. + /// This is modified by DNA on init so it's not savable. /// - [DataField] - public string BloodSolutionName = DefaultBloodSolutionName; + [ViewVariables(VVAccess.ReadOnly)] + public List? BloodData; /// - /// Name/Key that is indexed by. + /// Name/Key that is indexed by. /// [DataField] - public string ChemicalSolutionName = DefaultChemicalsSolutionName; + public string BloodSolutionName = DefaultBloodSolutionName; /// /// Name/Key that is indexed by. @@ -179,16 +173,16 @@ public sealed partial class BloodstreamComponent : Component public string BloodTemporarySolutionName = DefaultBloodTemporarySolutionName; /// - /// Internal solution for blood storage + /// Name/Key that is indexed by. /// - [ViewVariables] - public Entity? BloodSolution; + [DataField] + public string MetabolitesSolutionName = DefaultMetabolitesSolutionName; /// - /// Internal solution for reagent storage + /// Internal solution for blood storage /// [ViewVariables] - public Entity? ChemicalSolution; + public Entity? BloodSolution; /// /// Temporary blood solution. @@ -198,6 +192,12 @@ public sealed partial class BloodstreamComponent : Component [ViewVariables] public Entity? TemporarySolution; + /// + /// Internal solution for metabolite storage + /// + [ViewVariables] + public Entity? MetabolitesSolution; + /// /// Alert to show when bleeding. /// diff --git a/Content.Shared/Body/Components/BodyComponent.cs b/Content.Shared/Body/Components/BodyComponent.cs deleted file mode 100644 index 481e22150b0..00000000000 --- a/Content.Shared/Body/Components/BodyComponent.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Shared.Body.Prototypes; -using Content.Shared.Body.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Body.Components; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedBodySystem))] -public sealed partial class BodyComponent : Component -{ - /// - /// Relevant template to spawn for this body. - /// - [DataField, AutoNetworkedField] - public ProtoId? Prototype; - - /// - /// Container that holds the root body part. - /// - /// - /// Typically is the torso. - /// - [ViewVariables] public ContainerSlot RootContainer = default!; - - [ViewVariables] - public string RootPartSlot => RootContainer.ID; - - [DataField, AutoNetworkedField] - public SoundSpecifier GibSound = new SoundCollectionSpecifier("gib"); - - /// - /// The amount of legs required to move at full speed. - /// If 0, then legs do not impact speed. - /// - [DataField, AutoNetworkedField] - public int RequiredLegs; - - [ViewVariables] - [DataField, AutoNetworkedField] - public HashSet LegEntities = new(); -} diff --git a/Content.Shared/Body/Components/BrainComponent.cs b/Content.Shared/Body/Components/BrainComponent.cs new file mode 100644 index 00000000000..be3c3ecbe5a --- /dev/null +++ b/Content.Shared/Body/Components/BrainComponent.cs @@ -0,0 +1,6 @@ +using Content.Shared.Body.Systems; + +namespace Content.Shared.Body.Components; + +[RegisterComponent, Access(typeof(BrainSystem))] +public sealed partial class BrainComponent : Component; diff --git a/Content.Server/Body/Components/LungComponent.cs b/Content.Shared/Body/Components/LungComponent.cs similarity index 81% rename from Content.Server/Body/Components/LungComponent.cs rename to Content.Shared/Body/Components/LungComponent.cs index 4fb769d6702..ab1869f8b12 100644 --- a/Content.Server/Body/Components/LungComponent.cs +++ b/Content.Shared/Body/Components/LungComponent.cs @@ -1,12 +1,13 @@ -using Content.Server.Body.Systems; +using Content.Shared.Body.Systems; using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Chemistry.Components; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -namespace Content.Server.Body.Components; +namespace Content.Shared.Body.Components; -[RegisterComponent, Access(typeof(LungSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(LungSystem))] public sealed partial class LungComponent : Component { [DataField] @@ -21,7 +22,7 @@ public sealed partial class LungComponent : Component /// The name/key of the solution on this entity which these lungs act on. /// [DataField] - public string SolutionName = LungSystem.LungSolutionName; + public string SolutionName = "Lung"; /// /// The solution on this entity that these lungs act on. diff --git a/Content.Shared/Body/Components/StomachComponent.cs b/Content.Shared/Body/Components/StomachComponent.cs index 1b2f5878240..66f4674d9af 100644 --- a/Content.Shared/Body/Components/StomachComponent.cs +++ b/Content.Shared/Body/Components/StomachComponent.cs @@ -1,92 +1,28 @@ using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Whitelist; using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Shared.Body.Components -{ - [RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))] - public sealed partial class StomachComponent : Component - { - /// - /// The next time that the stomach will try to digest its contents. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan NextUpdate; - - /// - /// The interval at which this stomach digests its contents. - /// - [DataField] - public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); - - /// - /// Multiplier applied to for adjusting based on metabolic rate multiplier. - /// - [DataField] - public float UpdateIntervalMultiplier = 1f; - - /// - /// Adjusted update interval based off of the multiplier value. - /// - [ViewVariables] - public TimeSpan AdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier; - - /// - /// The solution inside of this stomach this transfers reagents to the body. - /// - [ViewVariables] - public Entity? Solution; - - /// - /// What solution should this stomach push reagents into, on the body? - /// - [DataField] - public string BodySolutionName = "chemicals"; - - /// - /// Time between reagents being ingested and them being - /// transferred to - /// - [DataField] - public TimeSpan DigestionDelay = TimeSpan.FromSeconds(20); +namespace Content.Shared.Body.Components; - /// - /// A whitelist for what special-digestible-required foods this stomach is capable of eating. - /// - [DataField] - public EntityWhitelist? SpecialDigestible = null; - - /// - /// Controls whitelist behavior. If true, this stomach can digest only food that passes the whitelist. If false, it can digest normal food and any food that passes the whitelist. - /// - [DataField] - public bool IsSpecialDigestibleExclusive = true; - - /// - /// Used to track how long each reagent has been in the stomach - /// - [ViewVariables] - public readonly List ReagentDeltas = new(); - - /// - /// Used to track quantity changes when ingesting & digesting reagents - /// - public sealed class ReagentDelta - { - public readonly ReagentQuantity ReagentQuantity; - public TimeSpan Lifetime { get; private set; } - - public ReagentDelta(ReagentQuantity reagentQuantity) - { - ReagentQuantity = reagentQuantity; - Lifetime = TimeSpan.Zero; - } - - public void Increment(TimeSpan delta) => Lifetime += delta; - } - } +[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem))] +public sealed partial class StomachComponent : Component +{ + /// + /// The solution inside of this stomach + /// + [ViewVariables] + public Entity? Solution; + + /// + /// A whitelist for what special-digestible-required foods this stomach is capable of eating. + /// + [DataField] + public EntityWhitelist? SpecialDigestible = null; + + /// + /// Controls whitelist behavior. If true, this stomach can digest only food that passes the whitelist. If false, it can digest normal food and any food that passes the whitelist. + /// + [DataField] + public bool IsSpecialDigestibleExclusive = true; } diff --git a/Content.Shared/Body/Events/BeingGibbedEvent.cs b/Content.Shared/Body/Events/BeingGibbedEvent.cs deleted file mode 100644 index 7ab34f2e18a..00000000000 --- a/Content.Shared/Body/Events/BeingGibbedEvent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.Body.Events; - -/// -/// Raised when a body gets gibbed, before it is deleted. -/// -[ByRefEvent] -public readonly record struct BeingGibbedEvent(HashSet GibbedParts); diff --git a/Content.Shared/Body/Events/MechanismBodyEvents.cs b/Content.Shared/Body/Events/MechanismBodyEvents.cs deleted file mode 100644 index 968b172aef5..00000000000 --- a/Content.Shared/Body/Events/MechanismBodyEvents.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Content.Shared.Body.Events; - -// All of these events are raised on a mechanism entity when added/removed to a body in different -// ways. - -/// -/// Raised on a mechanism when it is added to a body part. -/// -[ByRefEvent] -public readonly record struct OrganAddedEvent(EntityUid Part); - -/// -/// Raised on a mechanism when it is added to a body part within a body. -/// -[ByRefEvent] -public readonly record struct OrganAddedToBodyEvent(EntityUid Body, EntityUid Part); - -/// -/// Raised on a mechanism when it is removed from a body part. -/// -[ByRefEvent] -public readonly record struct OrganRemovedEvent(EntityUid OldPart); - -/// -/// Raised on a mechanism when it is removed from a body part within a body. -/// -[ByRefEvent] -public readonly record struct OrganRemovedFromBodyEvent(EntityUid OldBody, EntityUid OldPart); diff --git a/Content.Shared/Body/Events/MetabolismExclusionEvent.cs b/Content.Shared/Body/Events/MetabolismExclusionEvent.cs new file mode 100644 index 00000000000..3626e5af777 --- /dev/null +++ b/Content.Shared/Body/Events/MetabolismExclusionEvent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Chemistry.Reagent; + +namespace Content.Shared.Body.Events; + +/// +/// Event called by to get a list of +/// blood like reagents for metabolism to skip. +/// +[ByRefEvent] +public readonly record struct MetabolismExclusionEvent() +{ + public readonly List Reagents = []; +} diff --git a/Content.Shared/Body/GibbableOrganComponent.cs b/Content.Shared/Body/GibbableOrganComponent.cs new file mode 100644 index 00000000000..0ac2025facb --- /dev/null +++ b/Content.Shared/Body/GibbableOrganComponent.cs @@ -0,0 +1,12 @@ +using Content.Shared.Gibbing; +using Robust.Shared.GameStates; + +namespace Content.Shared.Body; + +/// +/// Component that causes this entity to become gibs when the body it's in is gibbed +/// +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(GibbableOrganSystem))] +public sealed partial class GibbableOrganComponent : Component; diff --git a/Content.Shared/Body/GibbableOrganSystem.cs b/Content.Shared/Body/GibbableOrganSystem.cs new file mode 100644 index 00000000000..56b0af11c19 --- /dev/null +++ b/Content.Shared/Body/GibbableOrganSystem.cs @@ -0,0 +1,18 @@ +using Content.Shared.Gibbing; + +namespace Content.Shared.Body; + +public sealed class GibbableOrganSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnBeingGibbed); + } + + private void OnBeingGibbed(Entity ent, ref BodyRelayedEvent args) + { + args.Args.Giblets.Add(ent); + } +} diff --git a/Content.Shared/Body/HandOrganComponent.cs b/Content.Shared/Body/HandOrganComponent.cs new file mode 100644 index 00000000000..a9de7ecdaa9 --- /dev/null +++ b/Content.Shared/Body/HandOrganComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Hands.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Body; + +/// +/// Organs with this component provide a hand with the given ID and data to the body when inserted. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(HandOrganSystem))] +public sealed partial class HandOrganComponent : Component +{ + /// + /// The hand ID used by on the body + /// + [DataField(required: true)] + public string HandID; + + /// + /// The data used to create the hand + /// + [DataField(required: true)] + public Hand Data; +} diff --git a/Content.Shared/Body/HandOrganSystem.cs b/Content.Shared/Body/HandOrganSystem.cs new file mode 100644 index 00000000000..0c3a0b0824b --- /dev/null +++ b/Content.Shared/Body/HandOrganSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.Hands.EntitySystems; + +namespace Content.Shared.Body; + +public sealed class HandOrganSystem : EntitySystem +{ + [Dependency] private readonly SharedHandsSystem _hands = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGotInserted); + SubscribeLocalEvent(OnGotRemoved); + } + + private void OnGotInserted(Entity ent, ref OrganGotInsertedEvent args) + { + _hands.AddHand(args.Target, ent.Comp.HandID, ent.Comp.Data); + } + + private void OnGotRemoved(Entity ent, ref OrganGotRemovedEvent args) + { + // prevent a recursive double-delete bug + if (LifeStage(args.Target) >= EntityLifeStage.Terminating) + return; + + _hands.RemoveHand(args.Target, ent.Comp.HandID); + } +} diff --git a/Content.Shared/Body/InitialBodyComponent.cs b/Content.Shared/Body/InitialBodyComponent.cs new file mode 100644 index 00000000000..a27e5c092c5 --- /dev/null +++ b/Content.Shared/Body/InitialBodyComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body; + +/// +/// On map initialization, spawns the given organs into the body. +/// Liable to change as the body becomes more complex. +/// +[RegisterComponent] +[Access(typeof(InitialBodySystem))] +public sealed partial class InitialBodyComponent : Component +{ + /// + /// The organs to spawn based on their category. + /// + [DataField(required: true)] + public Dictionary, EntProtoId> Organs; +} diff --git a/Content.Shared/Body/InitialBodySystem.cs b/Content.Shared/Body/InitialBodySystem.cs new file mode 100644 index 00000000000..639024171fb --- /dev/null +++ b/Content.Shared/Body/InitialBodySystem.cs @@ -0,0 +1,47 @@ +using System.Numerics; +using Robust.Shared.Containers; +using Robust.Shared.Map; + +namespace Content.Shared.Body; + +public sealed class InitialBodySystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _container = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var containerComp)) + return; + + if (TerminatingOrDeleted(ent) || !Exists(ent)) + return; + + if (!_container.TryGetContainer(ent, BodyComponent.ContainerID, out var container, containerComp)) + { + Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} is missing a container ({BodyComponent.ContainerID})."); + return; + } + + var xform = Transform(ent); + var coords = new EntityCoordinates(ent, Vector2.Zero); + + foreach (var proto in ent.Comp.Organs.Values) + { + // TODO: When e#6192 is merged replace this all with TrySpawnInContainer... + var spawn = Spawn(proto, coords); + + if (!_container.Insert(spawn, container, containerXform: xform)) + { + Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\n"); + Del(spawn); + } + } + } +} diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs deleted file mode 100644 index 3048927b5fb..00000000000 --- a/Content.Shared/Body/Organ/OrganComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Body.Systems; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; - -namespace Content.Shared.Body.Organ; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedBodySystem))] -public sealed partial class OrganComponent : Component -{ - /// - /// Relevant body this organ is attached to. - /// - [DataField, AutoNetworkedField] - public EntityUid? Body; -} diff --git a/Content.Shared/Body/OrganCategoryPrototype.cs b/Content.Shared/Body/OrganCategoryPrototype.cs new file mode 100644 index 00000000000..40bf13961de --- /dev/null +++ b/Content.Shared/Body/OrganCategoryPrototype.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body; + +/// +/// Marker prototype that defines well-known types of organs, e.g. "kidneys" or "left arm". +/// +[Prototype] +public sealed partial class OrganCategoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; +} diff --git a/Content.Shared/Body/OrganComponent.cs b/Content.Shared/Body/OrganComponent.cs new file mode 100644 index 00000000000..cad2ad86da7 --- /dev/null +++ b/Content.Shared/Body/OrganComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Body; + +/// +/// Marks an entity as being able to be inserted into an entity with . +/// +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(BodySystem))] +public sealed partial class OrganComponent : Component +{ + /// + /// The body entity containing this organ, if any + /// + [DataField, AutoNetworkedField] + public EntityUid? Body; + + /// + /// What kind of organ is this, if any + /// + [DataField] + public ProtoId? Category; +} diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs deleted file mode 100644 index c4e65c06a3f..00000000000 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedBodySystem))] -public sealed partial class BodyPartComponent : Component -{ - // Need to set this on container changes as it may be several transform parents up the hierarchy. - /// - /// Parent body for this part. - /// - [DataField, AutoNetworkedField] - public EntityUid? Body; - - [DataField, AutoNetworkedField] - public BodyPartType PartType = BodyPartType.Other; - - // TODO BODY Replace with a simulation of organs - /// - /// Whether or not the owning will die if all - /// s of this type are removed from it. - /// - [DataField("vital"), AutoNetworkedField] - public bool IsVital; - - [DataField, AutoNetworkedField] - public BodyPartSymmetry Symmetry = BodyPartSymmetry.None; - - /// - /// Child body parts attached to this body part. - /// - [DataField, AutoNetworkedField] - public Dictionary Children = new(); - - /// - /// Organs attached to this body part. - /// - [DataField, AutoNetworkedField] - public Dictionary Organs = new(); - - /// - /// These are only for VV/Debug do not use these for gameplay/systems - /// - [ViewVariables] - private List BodyPartSlotsVV - { - get - { - List temp = new(); - var containerSystem = IoCManager.Resolve().System(); - - foreach (var slotId in Children.Keys) - { - temp.Add((ContainerSlot) containerSystem.GetContainer(Owner, SharedBodySystem.PartSlotContainerIdPrefix+slotId)); - } - - return temp; - } - } - - [ViewVariables] - private List OrganSlotsVV - { - get - { - List temp = new(); - var containerSystem = IoCManager.Resolve().System(); - - foreach (var slotId in Organs.Keys) - { - temp.Add((ContainerSlot) containerSystem.GetContainer(Owner, SharedBodySystem.OrganSlotContainerIdPrefix+slotId)); - } - - return temp; - } - } -} - -/// -/// Contains metadata about a body part in relation to its slot. -/// -[NetSerializable, Serializable] -[DataRecord] -public partial struct BodyPartSlot -{ - public string Id; - public BodyPartType Type; - - public BodyPartSlot(string id, BodyPartType type) - { - Id = id; - Type = type; - } -}; - -/// -/// Contains metadata about an organ part in relation to its slot. -/// -[NetSerializable, Serializable] -[DataRecord] -public partial struct OrganSlot -{ - public string Id; - - public OrganSlot(string id) - { - Id = id; - } -}; diff --git a/Content.Shared/Body/Part/BodyPartEvents.cs b/Content.Shared/Body/Part/BodyPartEvents.cs deleted file mode 100644 index 0d8d2c8a268..00000000000 --- a/Content.Shared/Body/Part/BodyPartEvents.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.Body.Part; - -[ByRefEvent] -public readonly record struct BodyPartAddedEvent(string Slot, Entity Part); - -[ByRefEvent] -public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part); diff --git a/Content.Shared/Body/Part/BodyPartSymmetry.cs b/Content.Shared/Body/Part/BodyPartSymmetry.cs deleted file mode 100644 index aa008698231..00000000000 --- a/Content.Shared/Body/Part/BodyPartSymmetry.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Body.Components; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part -{ - /// - /// Defines the symmetry of a . - /// - [Serializable, NetSerializable] - public enum BodyPartSymmetry - { - None = 0, - Left, - Right - } -} diff --git a/Content.Shared/Body/Part/BodyPartType.cs b/Content.Shared/Body/Part/BodyPartType.cs deleted file mode 100644 index acaee76fd85..00000000000 --- a/Content.Shared/Body/Part/BodyPartType.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Shared.Body.Components; -using Robust.Shared.Serialization; - -namespace Content.Shared.Body.Part -{ - /// - /// Defines the type of a . - /// - [Serializable, NetSerializable] - public enum BodyPartType - { - Other = 0, - Torso, - Head, - Arm, - Hand, - Leg, - Foot, - Tail - } -} diff --git a/Content.Shared/Body/Prototypes/BodyPrototype.cs b/Content.Shared/Body/Prototypes/BodyPrototype.cs deleted file mode 100644 index 45ad6df0e42..00000000000 --- a/Content.Shared/Body/Prototypes/BodyPrototype.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.Body.Prototypes; - -[Prototype] -public sealed partial class BodyPrototype : IPrototype -{ - [IdDataField] public string ID { get; private set; } = default!; - - [DataField("name")] - public string Name { get; private set; } = ""; - - [DataField("root")] public string Root { get; private set; } = string.Empty; - - [DataField("slots")] public Dictionary Slots { get; private set; } = new(); - - private BodyPrototype() { } - - public BodyPrototype(string id, string name, string root, Dictionary slots) - { - ID = id; - Name = name; - Root = root; - Slots = slots; - } -} - -[DataRecord] -public sealed partial record BodyPrototypeSlot(EntProtoId? Part, HashSet Connections, Dictionary Organs); diff --git a/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs b/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs deleted file mode 100644 index 338e6c8ab84..00000000000 --- a/Content.Shared/Body/Prototypes/BodyPrototypeSerializer.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Linq; -using Content.Shared.Body.Organ; -using Content.Shared.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; -using Robust.Shared.Serialization.Markdown.Validation; -using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Serialization.TypeSerializers.Interfaces; - -namespace Content.Shared.Body.Prototypes; - -[TypeSerializer] -public sealed class BodyPrototypeSerializer : ITypeReader -{ - private (ValidationNode Node, List Connections) ValidateSlot(MappingDataNode slot, IDependencyCollection dependencies) - { - var nodes = new List(); - var prototypes = dependencies.Resolve(); - var factory = dependencies.Resolve(); - - var connections = new List(); - if (slot.TryGet("connections", out SequenceDataNode? connectionsNode)) - { - foreach (var node in connectionsNode) - { - if (node is not ValueDataNode connection) - { - nodes.Add(new ErrorNode(node, $"Connection is not a value data node")); - continue; - } - - connections.Add(connection.Value); - } - } - - if (slot.TryGet("organs", out MappingDataNode? organsNode)) - { - foreach (var (key, value) in organsNode) - { - if (value is not ValueDataNode organ) - { - nodes.Add(new ErrorNode(value, $"Value is not a value data node")); - continue; - } - - if (!prototypes.TryIndex(organ.Value, out EntityPrototype? organPrototype)) - { - nodes.Add(new ErrorNode(value, $"No organ entity prototype found with id {organ.Value}")); - continue; - } - - if (!organPrototype.HasComponent(factory)) - { - nodes.Add(new ErrorNode(value, $"Organ {organ.Value} does not have a body component")); - } - } - } - - var validation = new ValidatedSequenceNode(nodes); - return (validation, connections); - } - - public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, - IDependencyCollection dependencies, ISerializationContext? context = null) - { - var nodes = new List(); - - if (!node.TryGet("root", out ValueDataNode? root)) - nodes.Add(new ErrorNode(node, $"No root value data node found")); - - if (!node.TryGet("slots", out MappingDataNode? slots)) - { - nodes.Add(new ErrorNode(node, $"No slots mapping data node found")); - } - else if (root != null) - { - if (!slots.TryGet(root.Value, out MappingDataNode? _)) - { - nodes.Add(new ErrorNode(slots, $"No slot found with id {root.Value}")); - return new ValidatedSequenceNode(nodes); - } - - foreach (var (key, value) in slots) - { - if (value is not MappingDataNode slot) - { - nodes.Add(new ErrorNode(value, $"Slot is not a mapping data node")); - continue; - } - - var result = ValidateSlot(slot, dependencies); - nodes.Add(result.Node); - - foreach (var connection in result.Connections) - { - if (!slots.TryGet(connection, out MappingDataNode? _)) - nodes.Add(new ErrorNode(slots, $"No slot found with id {connection}")); - } - } - } - - return new ValidatedSequenceNode(nodes); - } - - public BodyPrototype Read(ISerializationManager serializationManager, MappingDataNode node, - IDependencyCollection dependencies, - SerializationHookContext hookCtx, ISerializationContext? context = null, - ISerializationManager.InstantiationDelegate? instanceProvider = null) - { - var id = node.Get("id").Value; - var name = node.Get("name").Value; - var root = node.Get("root").Value; - var slotNodes = node.Get("slots"); - var allConnections = new Dictionary? Connections, Dictionary? Organs)>(); - - foreach (var (slotId, valueNode) in slotNodes) - { - var slot = (MappingDataNode) valueNode; - - string? part = null; - if (slot.TryGet("part", out var value)) - { - part = value.Value; - } - - HashSet? connections = null; - if (slot.TryGet("connections", out SequenceDataNode? slotConnectionsNode)) - { - connections = new HashSet(); - - foreach (var connection in slotConnectionsNode.Cast()) - { - connections.Add(connection.Value); - } - } - - Dictionary? organs = null; - if (slot.TryGet("organs", out MappingDataNode? slotOrgansNode)) - { - organs = new Dictionary(); - - foreach (var (organKey, organValueNode) in slotOrgansNode) - { - organs.Add(organKey, ((ValueDataNode) organValueNode).Value); - } - } - - allConnections.Add(slotId, (part, connections, organs)); - } - - foreach (var (slotId, (_, connections, _)) in allConnections) - { - if (connections == null) - continue; - - foreach (var connection in connections) - { - var other = allConnections[connection]; - other.Connections ??= new HashSet(); - other.Connections.Add(slotId); - allConnections[connection] = other; - } - } - - var slots = new Dictionary(); - - foreach (var (slotId, (part, connections, organs)) in allConnections) - { - var slot = new BodyPrototypeSlot(part, connections ?? new HashSet(), organs ?? new Dictionary()); - slots.Add(slotId, slot); - } - - return new BodyPrototype(id, name, root, slots); - } -} diff --git a/Content.Shared/Body/Prototypes/MetabolismGroupPrototype.cs b/Content.Shared/Body/Prototypes/MetabolismGroupPrototype.cs deleted file mode 100644 index f8e05d89c03..00000000000 --- a/Content.Shared/Body/Prototypes/MetabolismGroupPrototype.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.Body.Prototypes -{ - [Prototype] - public sealed partial class MetabolismGroupPrototype : IPrototype - { - [IdDataField] - public string ID { get; private set; } = default!; - - [DataField("name", required: true)] - private LocId Name { get; set; } - - [ViewVariables(VVAccess.ReadOnly)] - public string LocalizedName => Loc.GetString(Name); - } -} diff --git a/Content.Shared/Body/Prototypes/MetabolizerTypePrototype.cs b/Content.Shared/Body/Prototypes/MetabolizerTypePrototype.cs deleted file mode 100644 index 8c932af6773..00000000000 --- a/Content.Shared/Body/Prototypes/MetabolizerTypePrototype.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.Body.Prototypes -{ - [Prototype] - public sealed partial class MetabolizerTypePrototype : IPrototype - { - [IdDataField] - public string ID { get; private set; } = default!; - - [DataField("name", required: true)] - private LocId Name { get; set; } - - [ViewVariables(VVAccess.ReadOnly)] - public string LocalizedName => Loc.GetString(Name); - } -} diff --git a/Content.Shared/Body/SharedVisualBodySystem.Initial.cs b/Content.Shared/Body/SharedVisualBodySystem.Initial.cs new file mode 100644 index 00000000000..b2ed4a966db --- /dev/null +++ b/Content.Shared/Body/SharedVisualBodySystem.Initial.cs @@ -0,0 +1,19 @@ +using Content.Shared.Humanoid; + +namespace Content.Shared.Body; + +public abstract partial class SharedVisualBodySystem +{ + private void InitializeInitial() + { + SubscribeLocalEvent(OnVisualMapInit, after: [typeof(InitialBodySystem)]); + } + + private void OnVisualMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent, out var humanoidProfile)) + return; + + ApplyAppearanceTo(ent.AsNullable(), HumanoidCharacterAppearance.DefaultWithSpecies(humanoidProfile.Species, humanoidProfile.Sex), humanoidProfile.Sex); + } +} diff --git a/Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs b/Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs new file mode 100644 index 00000000000..a7f51631104 --- /dev/null +++ b/Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs @@ -0,0 +1,200 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Administration.Managers; +using Content.Shared.Administration; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Preferences; +using Content.Shared.Verbs; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Body; + +public abstract partial class SharedVisualBodySystem +{ + [Dependency] private readonly ISharedAdminManager _admin = default!; + [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!; + + private void InitializeModifiers() + { + SubscribeLocalEvent>(OnGetVerbs); + + Subs.BuiEvents(HumanoidMarkingModifierKey.Key, + subs => + { + subs.Event(OnModifiersOpened); + subs.Event(OnSetModifiers); + }); + } + + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!_admin.HasAdminFlag(args.User, AdminFlags.Fun)) + return; + + var user = args.User; + args.Verbs.Add(new Verb + { + Text = "Modify markings", + Category = VerbCategory.Tricks, + Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"), + Act = () => + { + _userInterface.OpenUi(ent.Owner, HumanoidMarkingModifierKey.Key, user); + } + }); + } + + /// + /// Copies the appearance of organs from one body to another + /// + /// The body whose organs to copy the appearance from + /// The body whose organs to copy the appearance to + [PublicAPI] + public void CopyAppearanceFrom(Entity source, Entity target) + { + if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp)) + return; + + var sourceOrgans = _container.EnsureContainer(source, BodyComponent.ContainerID); + + foreach (var sourceOrgan in sourceOrgans.ContainedEntities) + { + var evt = new OrganCopyAppearanceEvent(sourceOrgan); + RaiseLocalEvent(target, ref evt); + } + } + + /// + /// Gathers all the markings-relevant data from this entity + /// + /// The entity to sample + /// If set, only returns data concerning the given layers + /// The profiles for the various organs + /// The marking parameters for the various organs + /// The markings that are applied to the entity + public bool TryGatherMarkingsData(Entity ent, + HashSet? filter, + [NotNullWhen(true)] out Dictionary, OrganProfileData>? profiles, + [NotNullWhen(true)] out Dictionary, OrganMarkingData>? markings, + [NotNullWhen(true)] out Dictionary, Dictionary>>? applied) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + { + profiles = null; + markings = null; + applied = null; + return false; + } + + profiles = new(); + markings = new(); + applied = new(); + + var organContainer = _container.EnsureContainer(ent, BodyComponent.ContainerID); + + foreach (var organ in organContainer.ContainedEntities) + { + if (!TryComp(organ, out var organComp) || organComp.Category is not { } category) + continue; + + if (TryComp(organ, out var visualOrgan)) + { + profiles.TryAdd(category, visualOrgan.Profile); + } + + if (TryComp(organ, out var visualOrganMarkings)) + { + markings.TryAdd(category, visualOrganMarkings.MarkingData); + if (filter is not null) + applied.TryAdd(category, visualOrganMarkings.Markings.Where(kvp => filter.Contains(kvp.Key)).ToDictionary()); + else + applied.TryAdd(category, visualOrganMarkings.Markings); + } + } + + return true; + } + + private void OnModifiersOpened(Entity ent, ref BoundUIOpenedEvent args) + { + TryGatherMarkingsData(ent.AsNullable(), null, out var profiles, out var markings, out var applied); + + _userInterface.SetUiState(ent.Owner, HumanoidMarkingModifierKey.Key, new HumanoidMarkingModifierState(applied!, markings!, profiles!)); + } + + private void OnSetModifiers(Entity ent, ref HumanoidMarkingModifierMarkingSetMessage args) + { + var markingsEvt = new ApplyOrganMarkingsEvent(args.Markings); + RaiseLocalEvent(ent, ref markingsEvt); + } + + /// + /// Applies the given set of markings to the body. + /// + /// The body whose organs to apply markings to. + /// A dictionary of organ categories to markings information. Organs not included in this dictionary will remain unaffected. + [PublicAPI] + public void ApplyMarkings(EntityUid ent, Dictionary, Dictionary>> markings) + { + var markingsEvt = new ApplyOrganMarkingsEvent(markings); + RaiseLocalEvent(ent, ref markingsEvt); + } + + private void ApplyAppearanceTo(Entity ent, HumanoidCharacterAppearance appearance, Sex sex) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + ApplyProfile(ent, + new() + { + Sex = sex, + SkinColor = appearance.SkinColor, + EyeColor = appearance.EyeColor, + }); + + var markingsEvt = new ApplyOrganMarkingsEvent(appearance.Markings); + RaiseLocalEvent(ent, ref markingsEvt); + } + + /// + /// Applies the information contained with a to a visual body's appearance. + /// This sets the profile data and markings of all organs contained within the profile. + /// + /// The body to apply the profile to + /// The profile to apply + [PublicAPI] + public void ApplyProfileTo(Entity ent, HumanoidCharacterProfile profile) + { + ApplyAppearanceTo(ent, profile.Appearance, profile.Sex); + } + + /// + /// Applies profile data to all visual organs within the body. + /// + /// The body to apply the organ profile to + /// The profile to apply + [PublicAPI] + public void ApplyProfile(EntityUid ent, OrganProfileData profile) + { + var profileEvt = new ApplyOrganProfileDataEvent(profile, null); + RaiseLocalEvent(ent, ref profileEvt); + } + + /// + /// Applies profile data to the specified visual organs within the body. + /// Organs not specified are left unchanged. + /// + /// The body to apply the organ profiles to. + /// The profiles to apply. + [PublicAPI] + public void ApplyProfiles(EntityUid ent, Dictionary, OrganProfileData> profiles) + { + var profileEvt = new ApplyOrganProfileDataEvent(null, profiles); + RaiseLocalEvent(ent, ref profileEvt); + } +} diff --git a/Content.Shared/Body/SharedVisualBodySystem.cs b/Content.Shared/Body/SharedVisualBodySystem.cs new file mode 100644 index 00000000000..553dbe838e3 --- /dev/null +++ b/Content.Shared/Body/SharedVisualBodySystem.cs @@ -0,0 +1,193 @@ +using System.Linq; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Body; + +/// +/// Class responsible for managing the appearance of an entity with via its organs with +/// +public abstract partial class SharedVisualBodySystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly MarkingManager _marking = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnVisualOrganCopyAppearance); + SubscribeLocalEvent>(OnMarkingsOrganCopyAppearance); + SubscribeLocalEvent>(OnVisualOrganApplyProfile); + SubscribeLocalEvent>(OnMarkingsOrganApplyMarkings); + + InitializeModifiers(); + InitializeInitial(); + } + + private List ResolveMarkings(List markings, Color? skinColor, Color? eyeColor, Dictionary appearances) + { + var ret = new List(); + var forcedColors = new List<(Marking, MarkingPrototype)>(); + + // This method uses two loops since some marking with constrained colors care about the colors of previous markings. + // As such we want to ensure we can apply the markings they rely on first. + foreach (var marking in markings) + { + if (!_marking.TryGetMarking(marking, out var proto)) + continue; + + if (!proto.ForcedColoring && appearances.GetValueOrDefault(proto.BodyPart)?.MatchSkin != true) + ret.Add(marking); + else + forcedColors.Add((marking, proto)); + } + + foreach (var (marking, prototype) in forcedColors) + { + var colors = MarkingColoring.GetMarkingLayerColors( + prototype, + skinColor, + eyeColor, + ret); + + var markingWithColor = new Marking(marking.MarkingId, colors) + { + Forced = marking.Forced, + }; + if (appearances.GetValueOrDefault(prototype.BodyPart) is { MatchSkin: true } appearance && skinColor is { } color) + { + markingWithColor = markingWithColor.WithColor(color.WithAlpha(appearance.LayerAlpha)); + } + ret.Add(markingWithColor); + } + + return ret; + } + + protected virtual void SetOrganColor(Entity ent, Color color) + { + ent.Comp.Data.Color = color; + Dirty(ent); + } + + protected virtual void SetOrganAppearance(Entity ent, PrototypeLayerData data) + { + ent.Comp.Data = data; + Dirty(ent); + } + + protected virtual void SetOrganMarkings(Entity ent, Dictionary> markings) + { + ent.Comp.Markings = markings; + Dirty(ent); + } + + private void OnVisualOrganCopyAppearance(Entity ent, ref BodyRelayedEvent args) + { + if (!TryComp(args.Args.Organ, out var other)) + return; + + if (!other.Layer.Equals(ent.Comp.Layer)) + return; + + SetOrganAppearance(ent, other.Data); + } + + private void OnMarkingsOrganCopyAppearance(Entity ent, ref BodyRelayedEvent args) + { + if (!TryComp(args.Args.Organ, out var other)) + return; + + if (!other.MarkingData.Layers.SetEquals(ent.Comp.MarkingData.Layers)) + return; + + SetOrganMarkings(ent, other.Markings); + } + + private void OnVisualOrganApplyProfile(Entity ent, ref BodyRelayedEvent args) + { + if (Comp(ent).Category is not { } category) + return; + + var relevantData = args.Args.Base; + if (args.Args.Profiles?.TryGetValue(category, out var profile) == true) + relevantData = profile; + + if (relevantData is not { } data) + return; + + ent.Comp.Profile = data; + + if (ent.Comp.Layer.Equals(HumanoidVisualLayers.Eyes)) + SetOrganColor(ent, ent.Comp.Profile.EyeColor); + else + SetOrganColor(ent, ent.Comp.Profile.SkinColor); + + if (ent.Comp.SexStateOverrides is { } overrides && overrides.TryGetValue(data.Sex, out var state)) + { + ent.Comp.Data.State = state; + SetOrganAppearance(ent, ent.Comp.Data); + } + } + + private void OnMarkingsOrganApplyMarkings(Entity ent, ref BodyRelayedEvent args) + { + if (Comp(ent).Category is not { } category) + return; + + if (!args.Args.Markings.TryGetValue(category, out var markingSet)) + return; + + var groupProto = _prototype.Index(ent.Comp.MarkingData.Group); + var organMarkings = ent.Comp.Markings.ShallowClone(); + + foreach (var layer in ent.Comp.MarkingData.Layers) + { + if (!markingSet.TryGetValue(layer, out var markings)) + continue; + + var okSet = new List(); + + foreach (var marking in markings) + { + if (!_marking.TryGetMarking(marking, out _)) + continue; + + okSet.Add(marking); + } + + organMarkings[layer] = okSet; + } + + var profile = Comp(ent).Profile; + var resolved = organMarkings.ToDictionary( + kvp => kvp.Key, + kvp => ResolveMarkings(kvp.Value, profile.SkinColor, profile.EyeColor, groupProto.Appearances)); + + SetOrganMarkings(ent, resolved); + } +} + +/// +/// Raised on body entity, when an organ is having its appearance copied to it +/// +[ByRefEvent] +public readonly record struct OrganCopyAppearanceEvent(EntityUid Organ); + +/// +/// Raised on body entity when profiles are being applied to it +/// +[ByRefEvent] +public readonly record struct ApplyOrganProfileDataEvent(OrganProfileData? Base, Dictionary, OrganProfileData>? Profiles); + +/// +/// Raised on body entity when a profile is being applied to it +/// +[ByRefEvent] +public readonly record struct ApplyOrganMarkingsEvent(Dictionary, Dictionary>> Markings); + diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Shared/Body/Systems/BrainSystem.cs similarity index 75% rename from Content.Server/Body/Systems/BrainSystem.cs rename to Content.Shared/Body/Systems/BrainSystem.cs index e916849a816..41eea8e1112 100644 --- a/Content.Server/Body/Systems/BrainSystem.cs +++ b/Content.Shared/Body/Systems/BrainSystem.cs @@ -1,12 +1,11 @@ -using Content.Server.Body.Components; -using Content.Server.Ghost.Components; -using Content.Shared.Body.Events; +using Content.Shared.Body.Components; +using Content.Shared.Ghost; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Components; using Content.Shared.Pointing; -namespace Content.Server.Body.Systems; +namespace Content.Shared.Body.Systems; public sealed class BrainSystem : EntitySystem { @@ -16,8 +15,8 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((uid, _, args) => HandleMind(args.Body, uid)); - SubscribeLocalEvent((uid, _, args) => HandleMind(uid, args.OldBody)); + SubscribeLocalEvent((uid, _, args) => HandleMind(args.Target, uid)); + SubscribeLocalEvent((uid, _, args) => HandleMind(uid, args.Target)); SubscribeLocalEvent(OnPointAttempt); } @@ -43,4 +42,3 @@ private void OnPointAttempt(Entity ent, ref PointAttemptEvent ar args.Cancel(); } } - diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Shared/Body/Systems/LungSystem.cs similarity index 79% rename from Content.Server/Body/Systems/LungSystem.cs rename to Content.Shared/Body/Systems/LungSystem.cs index fdc071c5f17..8a7a00001a8 100644 --- a/Content.Server/Body/Systems/LungSystem.cs +++ b/Content.Shared/Body/Systems/LungSystem.cs @@ -1,23 +1,22 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.Body.Components; -using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Body.Components; using Content.Shared.Chemistry.Components; -using Content.Shared.Clothing; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Inventory.Events; +using Robust.Shared.Prototypes; using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent; using InternalsComponent = Content.Shared.Body.Components.InternalsComponent; -namespace Content.Server.Body.Systems; +namespace Content.Shared.Body.Systems; public sealed class LungSystem : EntitySystem { - [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly InternalsSystem _internals = default!; + [Dependency] private readonly SharedAtmosphereSystem _atmos = default!; + [Dependency] private readonly SharedInternalsSystem _internals = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - public static string LungSolutionName = "Lung"; - public override void Initialize() { base.Initialize(); @@ -38,10 +37,10 @@ private void OnGotEquipped(Entity ent, ref GotEquippedEvent return; } - if (TryComp(args.Equipee, out InternalsComponent? internals)) + if (TryComp(args.EquipTarget, out InternalsComponent? internals)) { - ent.Comp.ConnectedInternalsEntity = args.Equipee; - _internals.ConnectBreathTool((args.Equipee, internals), ent); + ent.Comp.ConnectedInternalsEntity = args.EquipTarget; + _internals.ConnectBreathTool((args.EquipTarget, internals), ent); } } @@ -54,6 +53,7 @@ private void OnComponentInit(Entity entity, ref ComponentInit arg } } + // TODO: JUST METABOLIZE GASES DIRECTLY DON'T CONVERT TO REAGENTS!!! (Needs Metabolism refactor :B) public void GasToReagent(EntityUid uid, LungComponent lung) { if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs index 70beb9e0dc6..bde65b99fee 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs @@ -6,10 +6,12 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; -using Content.Shared.EntityEffects.Effects; +using Content.Shared.Damage.Systems; +using Content.Shared.EntityEffects.Effects.Solution; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Forensics.Components; +using Content.Shared.Gibbing; using Content.Shared.HealthExaminable; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; @@ -21,7 +23,6 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; -using Content.Shared.Imperial.Medieval.Body; namespace Content.Shared.Body.Systems; @@ -29,9 +30,9 @@ public abstract class SharedBloodstreamSystem : EntitySystem { public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss"; + [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPuddleSystem _puddle = default!; @@ -50,9 +51,10 @@ public override void Initialize() SubscribeLocalEvent>(OnReactionAttempt); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnHealthBeingExamined); - SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent(OnBeingGibbed); SubscribeLocalEvent(OnApplyMetabolicMultiplier); SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnMetabolismExclusion); } public override void Update(float frameTime) @@ -69,53 +71,41 @@ public override void Update(float frameTime) bloodstream.NextUpdate += bloodstream.AdjustedUpdateInterval; DirtyField(uid, bloodstream, nameof(BloodstreamComponent.NextUpdate)); // needs to be dirtied on the client so it can be rerolled during prediction - if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) + if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution)) continue; - // Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive. - if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid)) + // Blood level regulation. Must be alive. + if (!_mobStateSystem.IsDead(uid)) { - TryModifyBloodLevel((uid, bloodstream), bloodstream.BloodRefreshAmount); + TryRegulateBloodLevel(uid, bloodstream.BloodRefreshAmount); + + TickBleed((uid, bloodstream)); + + // deal bloodloss damage if their blood level is below a threshold. + var bloodPercentage = GetBloodLevel(uid); + if (bloodPercentage < bloodstream.BloodlossThreshold) + { + // bloodloss damage is based on the base value, and modified by how low your blood level is. + var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage); + + _damageableSystem.TryChangeDamage(uid, amt, ignoreResistances: false, interruptsDoAfters: false); + + // Apply dizziness as a symptom of bloodloss. + // The effect is applied in a way that it will never be cleared without being healthy. + // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out + _status.TrySetStatusEffectDuration(uid, Bloodloss); + } + else + { + // If they're healthy, we'll try and heal some bloodloss instead. + _damageableSystem.TryChangeDamage(uid, bloodstream.BloodlossHealDamage * bloodPercentage, ignoreResistances: true, interruptsDoAfters: false); + + _status.TryRemoveStatusEffect(uid, Bloodloss); + } } - - // Removes blood from the bloodstream based on bleed amount (bleed rate) - // as well as stop their bleeding to a certain extent. - if (bloodstream.BleedAmount > 0) - { - var ev = new BleedModifierEvent(bloodstream.BleedAmount, bloodstream.BleedReductionAmount); - RaiseLocalEvent(uid, ref ev); - - // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount - TryModifyBloodLevel((uid, bloodstream), -ev.BleedAmount); - - // Bleed rate is reduced by the bleed reduction amount in the bloodstream component. - TryModifyBleedAmount((uid, bloodstream), -ev.BleedReductionAmount); - } - - // deal bloodloss damage if their blood level is below a threshold. - var bloodPercentage = GetBloodLevelPercentage((uid, bloodstream)); - if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid)) + else { - // bloodloss damage is based on the base value, and modified by how low your blood level is. - var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage); - - _damageableSystem.TryChangeDamage(uid, amt, - ignoreResistances: false, interruptsDoAfters: false); - - // Apply dizziness as a symptom of bloodloss. - // The effect is applied in a way that it will never be cleared without being healthy. - // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out - _status.TrySetStatusEffectDuration(uid, Bloodloss); - } - else if (!_mobStateSystem.IsDead(uid)) - { - // If they're healthy, we'll try and heal some bloodloss instead. - _damageableSystem.TryChangeDamage( - uid, - bloodstream.BloodlossHealDamage * bloodPercentage, - ignoreResistances: true, interruptsDoAfters: false); - - _status.TryRemoveStatusEffect(uid, Bloodloss); + TickBleed((uid, bloodstream)); } } } @@ -134,9 +124,6 @@ private void OnEntRemoved(Entity entity, ref EntRemovedFro if (args.Entity == entity.Comp.BloodSolution?.Owner) entity.Comp.BloodSolution = null; - if (args.Entity == entity.Comp.ChemicalSolution?.Owner) - entity.Comp.ChemicalSolution = null; - if (args.Entity == entity.Comp.TemporarySolution?.Owner) entity.Comp.TemporarySolution = null; } @@ -150,7 +137,9 @@ private void OnReactionAttempt(Entity ent, ref ReactionAtt { switch (effect) { - case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream + // TODO: Rather than this, ReactionAttempt should allow systems to remove effects from the list before the reaction. + // TODO: I think there's a PR up on the repo for this and if there isn't I'll make one -Princess + case EntityEffects.Effects.EntitySpawning.SpawnEntity: // Prevent entities from spawning in the bloodstream case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels. args.Cancelled = true; return; @@ -169,7 +158,6 @@ private void OnReactionAttempt(Entity ent, ref ReactionAtt private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args) { if (args.Name != ent.Comp.BloodSolutionName - && args.Name != ent.Comp.ChemicalSolutionName && args.Name != ent.Comp.BloodTemporarySolutionName) { return; @@ -192,7 +180,7 @@ private void OnDamageChanged(Entity ent, ref DamageChanged } // TODO probably cache this or something. humans get hurt a lot - if (!_prototypeManager.Resolve(ent.Comp.DamageBleedModifiers, out var modifiers)) + if (!PrototypeManager.Resolve(ent.Comp.DamageBleedModifiers, out var modifiers)) return; // some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true @@ -209,18 +197,15 @@ private void OnDamageChanged(Entity ent, ref DamageChanged var totalFloat = total.Float(); TryModifyBleedAmount(ent.AsNullable(), totalFloat); - /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 - /// The crit chance is currently the bleed rate modifier divided by 25. - /// Higher damage weapons have a higher chance to crit! + // Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 + // The crit chance is currently the bleed rate modifier divided by 25. + // Higher damage weapons have a higher chance to crit! - // TODO: Replace with RandomPredicted once the engine PR is merged // Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 }); - var rand = new System.Random(seed); var prob = Math.Clamp(totalFloat / 25, 0, 1); - if (totalFloat > 0 && rand.Prob(prob)) + if (totalFloat > 0 && SharedRandomExtensions.PredictedProb(_timing, prob, GetNetEntity(ent), GetNetEntity(args.Origin))) { - TryModifyBloodLevel(ent.AsNullable(), -total / 5); + TryBleedOut(ent.AsNullable(), total / 5); _audio.PlayPredicted(ent.Comp.InstantBloodSound, ent, args.Origin); } @@ -268,14 +253,14 @@ private void OnHealthBeingExamined(Entity ent, ref HealthB } // If the mob's blood level is below the damage threshhold, the pale message is added. - if (GetBloodLevelPercentage(ent.AsNullable()) < ent.Comp.BloodlossThreshold) + if (GetBloodLevel(ent.AsNullable()) < ent.Comp.BloodlossThreshold) { args.Message.PushNewline(); args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner))); } } - private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args) + private void OnBeingGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) { SpillAllSolutions(ent.AsNullable()); } @@ -290,25 +275,47 @@ private void OnRejuvenate(Entity ent, ref RejuvenateEvent { TryModifyBleedAmount(ent.AsNullable(), -ent.Comp.BleedAmount); - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) - TryModifyBloodLevel(ent.AsNullable(), bloodSolution.AvailableVolume); + if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) + { + SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); + // TODO: Use Solutions API for this when it exists + TryRegulateBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume); + } + } - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution)) - SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value); + private void OnMetabolismExclusion(Entity ent, ref MetabolismExclusionEvent args) + { + // Adding all blood reagents for filtering blood in metabolizer + foreach (var (reagent, _) in ent.Comp.BloodReferenceSolution) + { + args.Reagents.Add(reagent); + } } /// - /// Returns the current blood level as a percentage (between 0 and 1). + /// This returns the minimum amount of *usable* blood. + /// For multi reagent bloodstreams, if you have 100 of Reagent Y need 100, and 50 of Reagent X and need 100, + /// this will return 0.5f /// - public float GetBloodLevelPercentage(Entity ent) + /// Returns the current blood level as a value from 0 to + public float GetBloodLevel(Entity entity) { - if (!Resolve(ent, ref ent.Comp) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) + if (!Resolve(entity, ref entity.Comp) + || !SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution) + || entity.Comp.BloodReferenceSolution.Volume == 0) { return 0.0f; } - return bloodSolution.FillFraction; + var totalBloodLevel = FixedPoint2.New(entity.Comp.MaxVolumeModifier); // Can't go above max volume factor... + + foreach (var (reagentId, quantity) in entity.Comp.BloodReferenceSolution.Contents) + { + // Ideally we use a different calculation for blood pressure, this just defines how much *usable* blood you have! + totalBloodLevel = FixedPoint2.Min(totalBloodLevel, bloodSolution.GetTotalPrototypeQuantity(reagentId.Prototype) / quantity); + } + + return (float)totalBloodLevel; } /// @@ -326,75 +333,141 @@ public void SetBloodLossThreshold(Entity ent, float thres /// /// Attempt to transfer a provided solution to internal solution. /// - public bool TryAddToChemicals(Entity ent, Solution solution) + public bool TryAddToBloodstream(Entity ent, Solution solution) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution)) + || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) return false; - if (SolutionContainer.TryAddSolution(ent.Comp.ChemicalSolution.Value, solution)) + if (SolutionContainer.TryAddSolution(ent.Comp.BloodSolution.Value, solution)) return true; return false; } /// - /// Removes a certain amount of all reagents except of a single excluded one from the bloodstream. + /// Removes a certain amount of all reagents except of a single excluded one from the bloodstream and blood itself. + /// + /// + /// Solution of removed chemicals or null if none were removed. + /// + public Solution? FlushChemicals(Entity ent, FixedPoint2 quantity, ProtoId? excludedReagent = null ) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false) + || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) + return null; + + var flushedSolution = new Solution(); + + for (var i = bloodSolution.Contents.Count - 1; i >= 0; i--) + { + var (reagentId, _) = bloodSolution.Contents[i]; + if (ent.Comp.BloodReferenceSolution.ContainsPrototype(reagentId.Prototype) || reagentId.Prototype == excludedReagent) + continue; + + var reagentFlushAmount = SolutionContainer.RemoveReagent(ent.Comp.BloodSolution.Value, reagentId, quantity); + flushedSolution.AddReagent(reagentId, reagentFlushAmount); + } + + return flushedSolution.Volume == 0 ? null : flushedSolution; + } + + /// + /// A simple helper that tries to move blood volume up or down by a specified amount. + /// Blood will not go over normal volume for this entity's bloodstream. + /// + public bool TryModifyBloodLevel(Entity ent, FixedPoint2 amount) + { + var reference = 1f; + + if (amount < 0) + { + reference = 0f; + amount *= -1; + } + + return TryRegulateBloodLevel(ent, amount, reference); + } + + /// + /// Attempts to bring an entity's blood level to a modified equilibrium volume. /// - public bool FlushChemicals(Entity ent, ProtoId? excludedReagentID, FixedPoint2 quantity) + /// Entity whose bloodstream we're modifying. + /// The absolute maximum amount of blood we can add or remove. + /// The modifier for an entity's blood equilibrium, try to hit an entity's default blood volume multiplied by this value. + /// This CANNOT go above maximum blood volume! + /// False if we were unable to regulate blood level. This may return true even if blood level doesn't change! + public bool TryRegulateBloodLevel(Entity ent, FixedPoint2 amount, float referenceFactor = 1f) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution)) + || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution) + || amount == 0) return false; - for (var i = chemSolution.Contents.Count - 1; i >= 0; i--) + // TODO: Either make this percentage based regeneration and pre-pass the percentage. + // TODO: Solution regulation API that doesn't result in very minor FixedPoint2 errors (Currently gingerbreadman only regenerates 0.99u instead of 1.00u) + referenceFactor = Math.Clamp(referenceFactor, 0f, ent.Comp.MaxVolumeModifier); + var ratio = (float)amount / (float)ent.Comp.BloodReferenceSolution.Volume; + + foreach (var (referenceReagent, referenceQuantity) in ent.Comp.BloodReferenceSolution) { - var (reagentId, _) = chemSolution.Contents[i]; - if (reagentId.Prototype != excludedReagentID) + var error = referenceQuantity * referenceFactor - bloodSolution.GetTotalPrototypeQuantity(referenceReagent.Prototype); + var adjustedAmount = referenceQuantity * ratio; + + if (error > 0) { - SolutionContainer.RemoveReagent(ent.Comp.ChemicalSolution.Value, reagentId, quantity); + error = FixedPoint2.Min(error, adjustedAmount); + bloodSolution.AddReagent(referenceReagent, error); + } + else if (error < 0) + { + // invert the error since we're removing reagents... + error = FixedPoint2.Min( -error, adjustedAmount); + bloodSolution.RemoveReagent(referenceReagent, error); } } return true; } + public void TickBleed(Entity entity) + { + // Removes blood from the bloodstream based on bleed amount (bleed rate) + // as well as stop their bleeding to a certain extent. + if (entity.Comp.BleedAmount <= 0) + return; + + var ev = new BleedModifierEvent(entity.Comp.BleedAmount, entity.Comp.BleedReductionAmount); + RaiseLocalEvent(entity, ref ev); + + // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount + TryBleedOut(entity.AsNullable(), ev.BleedAmount); + + // Bleed rate is reduced by the bleed reduction amount in the bloodstream component. + TryModifyBleedAmount(entity.AsNullable(), -ev.BleedReductionAmount); + } + /// - /// Attempts to modify the blood level of this entity directly. + /// Removes blood by spilling out the bloodstream. /// - public bool TryModifyBloodLevel(Entity ent, FixedPoint2 amount) + public bool TryBleedOut(Entity ent, FixedPoint2 amount) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) - return false; - - if (amount >= 0) + || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution) + || amount <= 0) { - // Imperial Medieval Skills start - var ev = new GetBloodRegenModifiersEvent(1f); - RaiseLocalEvent(ent.Owner, ref ev); - // Imperial Medieval Skills end - return SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, amount, null, GetEntityBloodData(ent)); + return false; } - // Removal is more involved, - // since we also wanna handle moving it to the temporary solution - // and then spilling it if necessary. - var newSol = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, -amount); + + var leakedBlood = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, amount); if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) return true; - tempSolution.AddSolution(newSol, _prototypeManager); + tempSolution.AddSolution(leakedBlood, PrototypeManager); if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold) { - // Pass some of the chemstream into the spilled blood. - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution)) - { - var temp = SolutionContainer.SplitSolution(ent.Comp.ChemicalSolution.Value, tempSolution.Volume / 10); - tempSolution.AddSolution(temp, _prototypeManager); - } - _puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false); tempSolution.RemoveAllSolution(); @@ -443,21 +516,14 @@ public void SpillAllSolutions(Entity ent) if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) { tempSol.MaxVolume += bloodSolution.MaxVolume; - tempSol.AddSolution(bloodSolution, _prototypeManager); + tempSol.AddSolution(bloodSolution, PrototypeManager); SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); } - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution)) - { - tempSol.MaxVolume += chemSolution.MaxVolume; - tempSol.AddSolution(chemSolution, _prototypeManager); - SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value); - } - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) { tempSol.MaxVolume += tempSolution.MaxVolume; - tempSol.AddSolution(tempSolution, _prototypeManager); + tempSol.AddSolution(tempSolution, PrototypeManager); SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value); } @@ -467,33 +533,61 @@ public void SpillAllSolutions(Entity ent) /// /// Change what someone's blood is made of, on the fly. /// + [Obsolete("ChangeBloodReagent is obsolete, please use ChangeBloodReagents.")] public void ChangeBloodReagent(Entity ent, ProtoId reagent) { - if (!Resolve(ent, ref ent.Comp, logMissing: false) - || reagent == ent.Comp.BloodReagent) + ChangeBloodReagents(ent, new([new(reagent, 1)])); + } + + /// + /// Change what someone's blood is made of, on the fly. + /// + public void ChangeBloodReagents(Entity ent, Solution reagents) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) { return; } if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) { - ent.Comp.BloodReagent = reagent; + ent.Comp.BloodReferenceSolution = reagents.Clone(); + DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution)); return; } - var currentVolume = bloodSolution.RemoveReagent(ent.Comp.BloodReagent, bloodSolution.Volume, ignoreReagentData: true); + var currentVolume = FixedPoint2.Zero; + foreach (var reagent in ent.Comp.BloodReferenceSolution) + { + currentVolume += bloodSolution.RemoveReagent(reagent.Reagent, quantity: bloodSolution.Volume, ignoreReagentData: true); + } + + ent.Comp.BloodReferenceSolution = reagents.Clone(); + DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution)); - ent.Comp.BloodReagent = reagent; - DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagent)); + if (currentVolume == FixedPoint2.Zero) + return; - if (currentVolume > 0) - SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, currentVolume, null, GetEntityBloodData(ent)); + var solution = ent.Comp.BloodReferenceSolution.Clone(); + solution.ScaleSolution(currentVolume / solution.Volume); + SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution); } /// /// Get the reagent data for blood that a specific entity should have. /// - public List GetEntityBloodData(EntityUid uid) + public List GetEntityBloodData(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return NewEntityBloodData(entity); + + return entity.Comp.BloodData ?? NewEntityBloodData(entity); + } + + /// + /// Gets new blood data for this entity and caches it in + /// + protected List NewEntityBloodData(EntityUid uid) { var bloodData = new List(); var dnaData = new DnaData(); @@ -504,7 +598,6 @@ public List GetEntityBloodData(EntityUid uid) dnaData.DNA = Loc.GetString("forensics-dna-unknown"); bloodData.Add(dnaData); - return bloodData; } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs deleted file mode 100644 index 250f90db8f3..00000000000 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Shared.Body.Components; -using Content.Shared.Body.Organ; -using Content.Shared.Body.Part; -using Content.Shared.Body.Prototypes; -using Content.Shared.DragDrop; -using Content.Shared.Gibbing.Components; -using Content.Shared.Gibbing.Events; -using Content.Shared.Gibbing.Systems; -using Content.Shared.Inventory; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Map; -using Robust.Shared.Utility; - -namespace Content.Shared.Body.Systems; - -public partial class SharedBodySystem -{ - /* - * tl;dr of how bobby works - * - BodyComponent uses a BodyPrototype as a template. - * - On MapInit we spawn the root entity in the prototype and spawn all connections outwards from here - * - Each "connection" is a body part (e.g. arm, hand, etc.) and each part can also contain organs. - */ - - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly GibbingSystem _gibbingSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - - private const float GibletLaunchImpulse = 8; - private const float GibletLaunchImpulseVariance = 3; - - private void InitializeBody() - { - // Body here to handle root body parts. - SubscribeLocalEvent(OnBodyInserted); - SubscribeLocalEvent(OnBodyRemoved); - - SubscribeLocalEvent(OnBodyInit); - SubscribeLocalEvent(OnBodyMapInit); - SubscribeLocalEvent(OnBodyCanDrag); - } - - private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args) - { - // Root body part? - var slotId = args.Container.ID; - - if (slotId != BodyRootContainerId) - return; - - var insertedUid = args.Entity; - - if (TryComp(insertedUid, out BodyPartComponent? part)) - { - AddPart((ent, ent), (insertedUid, part), slotId); - RecursiveBodyUpdate((insertedUid, part), ent); - } - - if (TryComp(insertedUid, out OrganComponent? organ)) - { - AddOrgan((insertedUid, organ), ent, ent); - } - } - - private void OnBodyRemoved(Entity ent, ref EntRemovedFromContainerMessage args) - { - // Root body part? - var slotId = args.Container.ID; - - if (slotId != BodyRootContainerId) - return; - - var removedUid = args.Entity; - DebugTools.Assert(!TryComp(removedUid, out BodyPartComponent? b) || b.Body == ent); - DebugTools.Assert(!TryComp(removedUid, out OrganComponent? o) || o.Body == ent); - - if (TryComp(removedUid, out BodyPartComponent? part)) - { - RemovePart((ent, ent), (removedUid, part), slotId); - RecursiveBodyUpdate((removedUid, part), null); - } - - if (TryComp(removedUid, out OrganComponent? organ)) - RemoveOrgan((removedUid, organ), ent); - } - - private void OnBodyInit(Entity ent, ref ComponentInit args) - { - // Setup the initial container. - ent.Comp.RootContainer = Containers.EnsureContainer(ent, BodyRootContainerId); - } - - private void OnBodyMapInit(Entity ent, ref MapInitEvent args) - { - if (ent.Comp.Prototype is null) - return; - - // One-time setup - // Obviously can't run in Init to avoid double-spawns on save / load. - var prototype = Prototypes.Index(ent.Comp.Prototype.Value); - MapInitBody(ent, prototype); - } - - private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype) - { - var protoRoot = prototype.Slots[prototype.Root]; - if (protoRoot.Part is null) - return; - - // This should already handle adding the entity to the root. - var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId); - var rootPart = Comp(rootPartUid); - rootPart.Body = bodyEntity; - Dirty(rootPartUid, rootPart); - - // Setup the rest of the body entities. - SetupOrgans((rootPartUid, rootPart), protoRoot.Organs); - MapInitParts(rootPartUid, prototype); - } - - private void OnBodyCanDrag(Entity ent, ref CanDragEvent args) - { - args.Handled = true; - } - - /// - /// Sets up all of the relevant body parts for a particular body entity and root part. - /// - private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype) - { - // Start at the root part and traverse the body graph, setting up parts as we go. - // Basic BFS pathfind. - var rootSlot = prototype.Root; - var frontier = new Queue(); - frontier.Enqueue(rootSlot); - - // Child -> Parent connection. - var cameFrom = new Dictionary(); - cameFrom[rootSlot] = rootSlot; - // Maps slot to its relevant entity. - var cameFromEntities = new Dictionary(); - cameFromEntities[rootSlot] = rootPartId; - - while (frontier.TryDequeue(out var currentSlotId)) - { - var currentSlot = prototype.Slots[currentSlotId]; - - foreach (var connection in currentSlot.Connections) - { - // Already been handled - if (!cameFrom.TryAdd(connection, currentSlotId)) - continue; - - // Setup part - var connectionSlot = prototype.Slots[connection]; - var parentEntity = cameFromEntities[currentSlotId]; - var parentPartComponent = Comp(parentEntity); - - // Spawn the entity on the target - // then get the body part type, create the slot, and finally - // we can insert it into the container. - var childPart = Spawn(connectionSlot.Part, new EntityCoordinates(parentEntity, Vector2.Zero)); - cameFromEntities[connection] = childPart; - - var childPartComponent = Comp(childPart); - var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent); - var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection)); - - if (partSlot is null || !Containers.Insert(childPart, cont)) - { - Log.Error($"Could not create slot for connection {connection} in body {prototype.ID}"); - QueueDel(childPart); - continue; - } - - // Add organs - SetupOrgans((childPart, childPartComponent), connectionSlot.Organs); - - // Enqueue it so we can also get its neighbors. - frontier.Enqueue(connection); - } - } - } - - private void SetupOrgans(Entity ent, Dictionary organs) - { - foreach (var (organSlotId, organProto) in organs) - { - var slot = CreateOrganSlot((ent, ent), organSlotId); - SpawnInContainerOrDrop(organProto, ent, GetOrganContainerId(organSlotId)); - - if (slot is null) - { - Log.Error($"Could not create organ for slot {organSlotId} in {ToPrettyString(ent)}"); - } - } - } - - /// - /// Gets all body containers on this entity including the root one. - /// - public IEnumerable GetBodyContainers( - EntityUid id, - BodyComponent? body = null, - BodyPartComponent? rootPart = null) - { - if (!Resolve(id, ref body, logMissing: false) - || body.RootContainer.ContainedEntity is null - || !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart)) - { - yield break; - } - - yield return body.RootContainer; - - foreach (var childContainer in GetPartContainers(body.RootContainer.ContainedEntity.Value, rootPart)) - { - yield return childContainer; - } - } - - /// - /// Gets all child body parts of this entity, including the root entity. - /// - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildren( - EntityUid? id, - BodyComponent? body = null, - BodyPartComponent? rootPart = null) - { - if (id is null - || !Resolve(id.Value, ref body, logMissing: false) - || body.RootContainer.ContainedEntity is null - || !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart)) - { - yield break; - } - - foreach (var child in GetBodyPartChildren(body.RootContainer.ContainedEntity.Value, rootPart)) - { - yield return child; - } - } - - public IEnumerable<(EntityUid Id, OrganComponent Component)> GetBodyOrgans( - EntityUid? bodyId, - BodyComponent? body = null) - { - if (bodyId is null || !Resolve(bodyId.Value, ref body, logMissing: false)) - yield break; - - foreach (var part in GetBodyChildren(bodyId, body)) - { - foreach (var organ in GetPartOrgans(part.Id, part.Component)) - { - yield return organ; - } - } - } - - /// - /// Returns all body part slots for this entity. - /// - /// - /// - /// - public IEnumerable GetBodyAllSlots( - EntityUid bodyId, - BodyComponent? body = null) - { - if (!Resolve(bodyId, ref body, logMissing: false) - || body.RootContainer.ContainedEntity is null) - { - yield break; - } - - foreach (var slot in GetAllBodyPartSlots(body.RootContainer.ContainedEntity.Value)) - { - yield return slot; - } - } - - public virtual HashSet GibBody( - EntityUid bodyId, - bool gibOrgans = false, - BodyComponent? body = null, - bool launchGibs = true, - Vector2? splatDirection = null, - float splatModifier = 1, - Angle splatCone = default, - SoundSpecifier? gibSoundOverride = null) - { - var gibs = new HashSet(); - - if (!Resolve(bodyId, ref body, logMissing: false)) - return gibs; - - var root = GetRootPartOrNull(bodyId, body); - if (root != null && TryComp(root.Value.Entity, out GibbableComponent? gibbable)) - { - gibSoundOverride ??= gibbable.GibSound; - } - var parts = GetBodyChildren(bodyId, body).ToArray(); - gibs.EnsureCapacity(parts.Length); - foreach (var part in parts) - { - - _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs, - playAudio: false, launchGibs:true, launchDirection:splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, - launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone); - - if (!gibOrgans) - continue; - - foreach (var organ in GetPartOrgans(part.Id, part.Component)) - { - _gibbingSystem.TryGibEntityWithRef(bodyId, organ.Id, GibType.Drop, GibContentsOption.Skip, - ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier, - launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone); - } - } - - var bodyTransform = Transform(bodyId); - if (TryComp(bodyId, out var inventory)) - { - foreach (var item in _inventory.GetHandOrInventoryEntities(bodyId)) - { - SharedTransform.DropNextTo(item, (bodyId, bodyTransform)); - gibs.Add(item); - } - } - _audioSystem.PlayPredicted(gibSoundOverride, bodyTransform.Coordinates, null); - return gibs; - } -} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.LegacyCompat.cs b/Content.Shared/Body/Systems/SharedBodySystem.LegacyCompat.cs new file mode 100644 index 00000000000..741e4b6abc6 --- /dev/null +++ b/Content.Shared/Body/Systems/SharedBodySystem.LegacyCompat.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Body.Systems; + +/// +/// Imperial Medieval merge shim for removed SharedBodySystem. +/// +public sealed class SharedBodySystem : EntitySystem +{ +} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs deleted file mode 100644 index f83dd50c998..00000000000 --- a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Body.Components; -using Content.Shared.Body.Events; -using Content.Shared.Body.Organ; -using Content.Shared.Body.Part; -using Robust.Shared.Containers; - -namespace Content.Shared.Body.Systems; - -public partial class SharedBodySystem -{ - private void AddOrgan( - Entity organEnt, - EntityUid bodyUid, - EntityUid parentPartUid) - { - organEnt.Comp.Body = bodyUid; - var addedEv = new OrganAddedEvent(parentPartUid); - RaiseLocalEvent(organEnt, ref addedEv); - - if (organEnt.Comp.Body is not null) - { - var addedInBodyEv = new OrganAddedToBodyEvent(bodyUid, parentPartUid); - RaiseLocalEvent(organEnt, ref addedInBodyEv); - } - - Dirty(organEnt, organEnt.Comp); - } - - private void RemoveOrgan(Entity organEnt, EntityUid parentPartUid) - { - var removedEv = new OrganRemovedEvent(parentPartUid); - RaiseLocalEvent(organEnt, ref removedEv); - - if (organEnt.Comp.Body is { Valid: true } bodyUid) - { - var removedInBodyEv = new OrganRemovedFromBodyEvent(bodyUid, parentPartUid); - RaiseLocalEvent(organEnt, ref removedInBodyEv); - } - - organEnt.Comp.Body = null; - Dirty(organEnt, organEnt.Comp); - } - - /// - /// Creates the specified organ slot on the parent entity. - /// - private OrganSlot? CreateOrganSlot(Entity parentEnt, string slotId) - { - if (!Resolve(parentEnt, ref parentEnt.Comp, logMissing: false)) - return null; - - Containers.EnsureContainer(parentEnt, GetOrganContainerId(slotId)); - var slot = new OrganSlot(slotId); - parentEnt.Comp.Organs.Add(slotId, slot); - return slot; - } - - /// - /// Attempts to create the specified organ slot on the specified parent if it exists. - /// - public bool TryCreateOrganSlot( - EntityUid? parent, - string slotId, - [NotNullWhen(true)] out OrganSlot? slot, - BodyPartComponent? part = null) - { - slot = null; - - if (parent is null || !Resolve(parent.Value, ref part, logMissing: false)) - { - return false; - } - - Containers.EnsureContainer(parent.Value, GetOrganContainerId(slotId)); - slot = new OrganSlot(slotId); - return part.Organs.TryAdd(slotId, slot.Value); - } - - /// - /// Returns whether the slotId exists on the partId. - /// - public bool CanInsertOrgan( - EntityUid partId, - string slotId, - BodyPartComponent? part = null) - { - return Resolve(partId, ref part) && part.Organs.ContainsKey(slotId); - } - - /// - /// Returns whether the specified organ slot exists on the partId. - /// - public bool CanInsertOrgan( - EntityUid partId, - OrganSlot slot, - BodyPartComponent? part = null) - { - return CanInsertOrgan(partId, slot.Id, part); - } - - public bool InsertOrgan( - EntityUid partId, - EntityUid organId, - string slotId, - BodyPartComponent? part = null, - OrganComponent? organ = null) - { - if (!Resolve(organId, ref organ, logMissing: false) - || !Resolve(partId, ref part, logMissing: false) - || !CanInsertOrgan(partId, slotId, part)) - { - return false; - } - - var containerId = GetOrganContainerId(slotId); - - return Containers.TryGetContainer(partId, containerId, out var container) - && Containers.Insert(organId, container); - } - - /// - /// Removes the organ if it is inside of a body part. - /// - public bool RemoveOrgan(EntityUid organId, OrganComponent? organ = null) - { - if (!Containers.TryGetContainingContainer((organId, null, null), out var container)) - return false; - - var parent = container.Owner; - - return HasComp(parent) - && Containers.Remove(organId, container); - } - - /// - /// Tries to add this organ to any matching slot on this body part. - /// - public bool AddOrganToFirstValidSlot( - EntityUid partId, - EntityUid organId, - BodyPartComponent? part = null, - OrganComponent? organ = null) - { - if (!Resolve(partId, ref part, logMissing: false) - || !Resolve(organId, ref organ, logMissing: false)) - { - return false; - } - - foreach (var slotId in part.Organs.Keys) - { - InsertOrgan(partId, organId, slotId, part, organ); - return true; - } - - return false; - } - - /// - /// Returns a list of Entity<, > - /// for each organ of the body - /// - /// The component that we want to return - /// The body to check the organs of - public List> GetBodyOrganEntityComps( - Entity entity) - where T : IComponent - { - if (!Resolve(entity, ref entity.Comp)) - return new List>(); - - var query = GetEntityQuery(); - var list = new List>(3); - foreach (var organ in GetBodyOrgans(entity.Owner, entity.Comp)) - { - if (query.TryGetComponent(organ.Id, out var comp)) - list.Add((organ.Id, comp, organ.Component)); - } - - return list; - } - - /// - /// Tries to get a list of ValueTuples of and OrganComponent on each organs - /// in the given body. - /// - /// The body entity id to check on. - /// The list of components. - /// The body to check for organs on. - /// The component to check for. - /// Whether any were found. - public bool TryGetBodyOrganEntityComps( - Entity entity, - [NotNullWhen(true)] out List>? comps) - where T : IComponent - { - if (!Resolve(entity.Owner, ref entity.Comp)) - { - comps = null; - return false; - } - - comps = GetBodyOrganEntityComps(entity); - - if (comps.Count != 0) - return true; - - comps = null; - return false; - } -} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs deleted file mode 100644 index 78d270ddc94..00000000000 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ /dev/null @@ -1,796 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Shared.Body.Components; -using Content.Shared.Body.Events; -using Content.Shared.Body.Organ; -using Content.Shared.Body.Part; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.Movement.Components; -using Robust.Shared.Containers; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Shared.Body.Systems; - -public partial class SharedBodySystem -{ - private static readonly ProtoId BloodlossDamageType = "Bloodloss"; - private void InitializeParts() - { - // TODO: This doesn't handle comp removal on child ents. - - // If you modify this also see the Body partial for root parts. - SubscribeLocalEvent(OnBodyPartInserted); - SubscribeLocalEvent(OnBodyPartRemoved); - } - - private void OnBodyPartInserted(Entity ent, ref EntInsertedIntoContainerMessage args) - { - // Body part inserted into another body part. - var insertedUid = args.Entity; - var slotId = args.Container.ID; - - if (ent.Comp.Body is null) - return; - - if (TryComp(insertedUid, out BodyPartComponent? part)) - { - AddPart(ent.Comp.Body.Value, (insertedUid, part), slotId); - RecursiveBodyUpdate((insertedUid, part), ent.Comp.Body.Value); - } - - if (TryComp(insertedUid, out OrganComponent? organ)) - AddOrgan((insertedUid, organ), ent.Comp.Body.Value, ent); - } - - private void OnBodyPartRemoved(Entity ent, ref EntRemovedFromContainerMessage args) - { - // Body part removed from another body part. - var removedUid = args.Entity; - var slotId = args.Container.ID; - - DebugTools.Assert(!TryComp(removedUid, out BodyPartComponent? b) || b.Body == ent.Comp.Body); - DebugTools.Assert(!TryComp(removedUid, out OrganComponent? o) || o.Body == ent.Comp.Body); - - if (TryComp(removedUid, out BodyPartComponent? part) && part.Body is not null) - { - RemovePart(part.Body.Value, (removedUid, part), slotId); - RecursiveBodyUpdate((removedUid, part), null); - } - - if (TryComp(removedUid, out OrganComponent? organ)) - RemoveOrgan((removedUid, organ), ent); - } - - private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyUid) - { - ent.Comp.Body = bodyUid; - Dirty(ent, ent.Comp); - - foreach (var slotId in ent.Comp.Organs.Keys) - { - if (!Containers.TryGetContainer(ent, GetOrganContainerId(slotId), out var container)) - continue; - - foreach (var organ in container.ContainedEntities) - { - if (!TryComp(organ, out OrganComponent? organComp)) - continue; - - Dirty(organ, organComp); - - if (organComp.Body is { Valid: true } oldBodyUid) - { - var removedEv = new OrganRemovedFromBodyEvent(oldBodyUid, ent); - RaiseLocalEvent(organ, ref removedEv); - } - - organComp.Body = bodyUid; - if (bodyUid is not null) - { - var addedEv = new OrganAddedToBodyEvent(bodyUid.Value, ent); - RaiseLocalEvent(organ, ref addedEv); - } - } - } - - foreach (var slotId in ent.Comp.Children.Keys) - { - if (!Containers.TryGetContainer(ent, GetPartSlotContainerId(slotId), out var container)) - continue; - - foreach (var containedUid in container.ContainedEntities) - { - if (TryComp(containedUid, out BodyPartComponent? childPart)) - RecursiveBodyUpdate((containedUid, childPart), bodyUid); - } - } - } - - protected virtual void AddPart( - Entity bodyEnt, - Entity partEnt, - string slotId) - { - Dirty(partEnt, partEnt.Comp); - partEnt.Comp.Body = bodyEnt; - - var ev = new BodyPartAddedEvent(slotId, partEnt); - RaiseLocalEvent(bodyEnt, ref ev); - - AddLeg(partEnt, bodyEnt); - } - - protected virtual void RemovePart( - Entity bodyEnt, - Entity partEnt, - string slotId) - { - Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false); - Dirty(partEnt, partEnt.Comp); - partEnt.Comp.Body = null; - - var ev = new BodyPartRemovedEvent(slotId, partEnt); - RaiseLocalEvent(bodyEnt, ref ev); - - RemoveLeg(partEnt, bodyEnt); - PartRemoveDamage(bodyEnt, partEnt); - } - - private void AddLeg(Entity legEnt, Entity bodyEnt) - { - if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) - return; - - if (legEnt.Comp.PartType == BodyPartType.Leg) - { - bodyEnt.Comp.LegEntities.Add(legEnt); - UpdateMovementSpeed(bodyEnt); - Dirty(bodyEnt, bodyEnt.Comp); - } - } - - private void RemoveLeg(Entity legEnt, Entity bodyEnt) - { - if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) - return; - - if (legEnt.Comp.PartType == BodyPartType.Leg) - { - bodyEnt.Comp.LegEntities.Remove(legEnt); - UpdateMovementSpeed(bodyEnt); - Dirty(bodyEnt, bodyEnt.Comp); - - if (!bodyEnt.Comp.LegEntities.Any()) - { - Standing.Down(bodyEnt); - } - } - } - - private void PartRemoveDamage(Entity bodyEnt, Entity partEnt) - { - if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) - return; - - if (!_timing.ApplyingState - && partEnt.Comp.IsVital - && !GetBodyChildrenOfType(bodyEnt, partEnt.Comp.PartType, bodyEnt.Comp).Any() - ) - { - // TODO BODY SYSTEM KILL : remove this when wounding and required parts are implemented properly - var damage = new DamageSpecifier(Prototypes.Index(BloodlossDamageType), 300); - Damageable.TryChangeDamage(bodyEnt, damage); - } - } - - /// - /// Tries to get the parent body part to this if applicable. - /// Doesn't validate if it's a part of body system. - /// - public EntityUid? GetParentPartOrNull(EntityUid uid) - { - if (!Containers.TryGetContainingContainer((uid, null, null), out var container)) - return null; - - var parent = container.Owner; - - if (!HasComp(parent)) - return null; - - return parent; - } - - /// - /// Tries to get the parent body part and slot to this if applicable. - /// - public (EntityUid Parent, string Slot)? GetParentPartAndSlotOrNull(EntityUid uid) - { - if (!Containers.TryGetContainingContainer((uid, null, null), out var container)) - return null; - - var slotId = GetPartSlotContainerIdFromContainer(container.ID); - - if (string.IsNullOrEmpty(slotId)) - return null; - - var parent = container.Owner; - - if (!TryComp(parent, out var parentBody) - || !parentBody.Children.ContainsKey(slotId)) - return null; - - return (parent, slotId); - } - - /// - /// Tries to get the relevant parent body part to this if it exists. - /// It won't exist if this is the root body part or if it's not in a body. - /// - public bool TryGetParentBodyPart( - EntityUid partUid, - [NotNullWhen(true)] out EntityUid? parentUid, - [NotNullWhen(true)] out BodyPartComponent? parentComponent) - { - DebugTools.Assert(HasComp(partUid)); - parentUid = null; - parentComponent = null; - - if (Containers.TryGetContainingContainer((partUid, null, null), out var container) && - TryComp(container.Owner, out parentComponent)) - { - parentUid = container.Owner; - return true; - } - - return false; - } - - #region Slots - - /// - /// Creates a BodyPartSlot on the specified partUid. - /// - private BodyPartSlot? CreatePartSlot( - EntityUid partUid, - string slotId, - BodyPartType partType, - BodyPartComponent? part = null) - { - if (!Resolve(partUid, ref part, logMissing: false)) - return null; - - Containers.EnsureContainer(partUid, GetPartSlotContainerId(slotId)); - var partSlot = new BodyPartSlot(slotId, partType); - part.Children.Add(slotId, partSlot); - Dirty(partUid, part); - return partSlot; - } - - /// - /// Tries to create a BodyPartSlot on the specified partUid. - /// - /// false if not relevant or can't add it. - public bool TryCreatePartSlot( - EntityUid? partId, - string slotId, - BodyPartType partType, - [NotNullWhen(true)] out BodyPartSlot? slot, - BodyPartComponent? part = null) - { - slot = null; - - if (partId is null - || !Resolve(partId.Value, ref part, logMissing: false)) - { - return false; - } - - Containers.EnsureContainer(partId.Value, GetPartSlotContainerId(slotId)); - slot = new BodyPartSlot(slotId, partType); - - if (!part.Children.TryAdd(slotId, slot.Value)) - return false; - - Dirty(partId.Value, part); - return true; - } - - public bool TryCreatePartSlotAndAttach( - EntityUid parentId, - string slotId, - EntityUid childId, - BodyPartType partType, - BodyPartComponent? parent = null, - BodyPartComponent? child = null) - { - return TryCreatePartSlot(parentId, slotId, partType, out _, parent) - && AttachPart(parentId, slotId, childId, parent, child); - } - - #endregion - - #region RootPartManagement - - /// - /// Returns true if the partId is the root body container for the specified bodyId. - /// - public bool IsPartRoot( - EntityUid bodyId, - EntityUid partId, - BodyComponent? body = null, - BodyPartComponent? part = null) - { - return Resolve(partId, ref part) - && Resolve(bodyId, ref body) - && Containers.TryGetContainingContainer(bodyId, partId, out var container) - && container.ID == BodyRootContainerId; - } - - /// - /// Returns true if we can attach the partId to the bodyId as the root entity. - /// - public bool CanAttachToRoot( - EntityUid bodyId, - EntityUid partId, - BodyComponent? body = null, - BodyPartComponent? part = null) - { - return Resolve(bodyId, ref body) - && Resolve(partId, ref part) - && body.RootContainer.ContainedEntity is null - && bodyId != part.Body; - } - - /// - /// Returns the root part of this body if it exists. - /// - public (EntityUid Entity, BodyPartComponent BodyPart)? GetRootPartOrNull(EntityUid bodyId, BodyComponent? body = null) - { - if (!Resolve(bodyId, ref body) - || body.RootContainer.ContainedEntity is null) - { - return null; - } - - return (body.RootContainer.ContainedEntity.Value, - Comp(body.RootContainer.ContainedEntity.Value)); - } - - /// - /// Returns true if the partId can be attached to the parentId in the specified slot. - /// - public bool CanAttachPart( - EntityUid parentId, - BodyPartSlot slot, - EntityUid partId, - BodyPartComponent? parentPart = null, - BodyPartComponent? part = null) - { - return Resolve(partId, ref part, logMissing: false) - && Resolve(parentId, ref parentPart, logMissing: false) - && CanAttachPart(parentId, slot.Id, partId, parentPart, part); - } - - /// - /// Returns true if we can attach the specified partId to the parentId in the specified slot. - /// - public bool CanAttachPart( - EntityUid parentId, - string slotId, - EntityUid partId, - BodyPartComponent? parentPart = null, - BodyPartComponent? part = null) - { - return Resolve(partId, ref part, logMissing: false) - && Resolve(parentId, ref parentPart, logMissing: false) - && parentPart.Children.TryGetValue(slotId, out var parentSlotData) - && part.PartType == parentSlotData.Type - && Containers.TryGetContainer(parentId, GetPartSlotContainerId(slotId), out var container) - && Containers.CanInsert(partId, container); - } - - public bool AttachPartToRoot( - EntityUid bodyId, - EntityUid partId, - BodyComponent? body = null, - BodyPartComponent? part = null) - { - return Resolve(bodyId, ref body) - && Resolve(partId, ref part) - && CanAttachToRoot(bodyId, partId, body, part) - && Containers.Insert(partId, body.RootContainer); - } - - #endregion - - #region Attach/Detach - - /// - /// Attaches a body part to the specified body part parent. - /// - public bool AttachPart( - EntityUid parentPartId, - string slotId, - EntityUid partId, - BodyPartComponent? parentPart = null, - BodyPartComponent? part = null) - { - return Resolve(parentPartId, ref parentPart, logMissing: false) - && parentPart.Children.TryGetValue(slotId, out var slot) - && AttachPart(parentPartId, slot, partId, parentPart, part); - } - - /// - /// Attaches a body part to the specified body part parent. - /// - public bool AttachPart( - EntityUid parentPartId, - BodyPartSlot slot, - EntityUid partId, - BodyPartComponent? parentPart = null, - BodyPartComponent? part = null) - { - if (!Resolve(parentPartId, ref parentPart, logMissing: false) - || !Resolve(partId, ref part, logMissing: false) - || !CanAttachPart(parentPartId, slot.Id, partId, parentPart, part) - || !parentPart.Children.ContainsKey(slot.Id)) - { - return false; - } - - if (!Containers.TryGetContainer(parentPartId, GetPartSlotContainerId(slot.Id), out var container)) - { - DebugTools.Assert($"Unable to find body slot {slot.Id} for {ToPrettyString(parentPartId)}"); - return false; - } - - return Containers.Insert(partId, container); - } - - #endregion - - #region Misc - - public void UpdateMovementSpeed( - EntityUid bodyId, - BodyComponent? body = null, - MovementSpeedModifierComponent? movement = null) - { - if (!Resolve(bodyId, ref body, ref movement, logMissing: false) - || body.RequiredLegs <= 0) - { - return; - } - - var walkSpeed = 0f; - var sprintSpeed = 0f; - var acceleration = 0f; - foreach (var legEntity in body.LegEntities) - { - if (!TryComp(legEntity, out var legModifier)) - continue; - - walkSpeed += legModifier.WalkSpeed; - sprintSpeed += legModifier.SprintSpeed; - acceleration += legModifier.Acceleration; - } - walkSpeed /= body.RequiredLegs; - sprintSpeed /= body.RequiredLegs; - acceleration /= body.RequiredLegs; - Movement.ChangeBaseSpeed(bodyId, walkSpeed, sprintSpeed, acceleration, movement); - } - - #endregion - - #region Queries - - /// - /// Get all organs for the specified body part. - /// - public IEnumerable<(EntityUid Id, OrganComponent Component)> GetPartOrgans(EntityUid partId, BodyPartComponent? part = null) - { - if (!Resolve(partId, ref part, logMissing: false)) - yield break; - - foreach (var slotId in part.Organs.Keys) - { - var containerSlotId = GetOrganContainerId(slotId); - - if (!Containers.TryGetContainer(partId, containerSlotId, out var container)) - continue; - - foreach (var containedEnt in container.ContainedEntities) - { - if (!TryComp(containedEnt, out OrganComponent? organ)) - continue; - - yield return (containedEnt, organ); - } - } - } - - /// - /// Gets all BaseContainers for body parts on this entity and its child entities. - /// - public IEnumerable GetPartContainers(EntityUid id, BodyPartComponent? part = null) - { - if (!Resolve(id, ref part, logMissing: false) || - part.Children.Count == 0) - { - yield break; - } - - foreach (var slotId in part.Children.Keys) - { - var containerSlotId = GetPartSlotContainerId(slotId); - - if (!Containers.TryGetContainer(id, containerSlotId, out var container)) - continue; - - yield return container; - - foreach (var ent in container.ContainedEntities) - { - foreach (var childContainer in GetPartContainers(ent)) - { - yield return childContainer; - } - } - } - } - - /// - /// Returns all body part components for this entity including itself. - /// - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyPartChildren( - EntityUid partId, - BodyPartComponent? part = null) - { - if (!Resolve(partId, ref part, logMissing: false)) - yield break; - - yield return (partId, part); - - foreach (var slotId in part.Children.Keys) - { - var containerSlotId = GetPartSlotContainerId(slotId); - - if (Containers.TryGetContainer(partId, containerSlotId, out var container)) - { - foreach (var containedEnt in container.ContainedEntities) - { - if (!TryComp(containedEnt, out BodyPartComponent? childPart)) - continue; - - foreach (var value in GetBodyPartChildren(containedEnt, childPart)) - { - yield return value; - } - } - } - } - } - - /// - /// Returns all body part slots for this entity. - /// - public IEnumerable GetAllBodyPartSlots( - EntityUid partId, - BodyPartComponent? part = null) - { - if (!Resolve(partId, ref part, logMissing: false)) - yield break; - - foreach (var (slotId, slot) in part.Children) - { - yield return slot; - - var containerSlotId = GetOrganContainerId(slotId); - - if (Containers.TryGetContainer(partId, containerSlotId, out var container)) - { - foreach (var containedEnt in container.ContainedEntities) - { - if (!TryComp(containedEnt, out BodyPartComponent? childPart)) - continue; - - foreach (var subSlot in GetAllBodyPartSlots(containedEnt, childPart)) - { - yield return subSlot; - } - } - } - } - } - - /// - /// Returns true if the bodyId has any parts of this type. - /// - public bool BodyHasPartType( - EntityUid bodyId, - BodyPartType type, - BodyComponent? body = null) - { - return GetBodyChildrenOfType(bodyId, type, body).Any(); - } - - /// - /// Returns true if the parentId has the specified childId. - /// - public bool PartHasChild( - EntityUid parentId, - EntityUid childId, - BodyPartComponent? parent, - BodyPartComponent? child) - { - if (!Resolve(parentId, ref parent, logMissing: false) - || !Resolve(childId, ref child, logMissing: false)) - { - return false; - } - - foreach (var (foundId, _) in GetBodyPartChildren(parentId, parent)) - { - if (foundId == childId) - return true; - } - return false; - } - - /// - /// Returns true if the bodyId has the specified partId. - /// - public bool BodyHasChild( - EntityUid bodyId, - EntityUid partId, - BodyComponent? body = null, - BodyPartComponent? part = null) - { - return Resolve(bodyId, ref body, logMissing: false) - && body.RootContainer.ContainedEntity is not null - && Resolve(partId, ref part, logMissing: false) - && TryComp(body.RootContainer.ContainedEntity, out BodyPartComponent? rootPart) - && PartHasChild(body.RootContainer.ContainedEntity.Value, partId, rootPart, part); - } - - public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildrenOfType( - EntityUid bodyId, - BodyPartType type, - BodyComponent? body = null) - { - foreach (var part in GetBodyChildren(bodyId, body)) - { - if (part.Component.PartType == type) - yield return part; - } - } - - /// - /// Returns a list of ValueTuples of and OrganComponent on each organ - /// in the given part. - /// - /// The part entity id to check on. - /// The part to check for organs on. - /// The component to check for. - public List<(T Comp, OrganComponent Organ)> GetBodyPartOrganComponents( - EntityUid uid, - BodyPartComponent? part = null) - where T : IComponent - { - if (!Resolve(uid, ref part)) - return new List<(T Comp, OrganComponent Organ)>(); - - var query = GetEntityQuery(); - var list = new List<(T Comp, OrganComponent Organ)>(); - - foreach (var organ in GetPartOrgans(uid, part)) - { - if (query.TryGetComponent(organ.Id, out var comp)) - list.Add((comp, organ.Component)); - } - - return list; - } - - /// - /// Tries to get a list of ValueTuples of and OrganComponent on each organs - /// in the given part. - /// - /// The part entity id to check on. - /// The list of components. - /// The part to check for organs on. - /// The component to check for. - /// Whether any were found. - public bool TryGetBodyPartOrganComponents( - EntityUid uid, - [NotNullWhen(true)] out List<(T Comp, OrganComponent Organ)>? comps, - BodyPartComponent? part = null) - where T : IComponent - { - if (!Resolve(uid, ref part)) - { - comps = null; - return false; - } - - comps = GetBodyPartOrganComponents(uid, part); - - if (comps.Count != 0) - return true; - - comps = null; - return false; - } - - /// - /// Gets the parent body part and all immediate child body parts for the partId. - /// - public IEnumerable GetBodyPartAdjacentParts( - EntityUid partId, - BodyPartComponent? part = null) - { - if (!Resolve(partId, ref part, logMissing: false)) - yield break; - - if (TryGetParentBodyPart(partId, out var parentUid, out _)) - yield return parentUid.Value; - - foreach (var slotId in part.Children.Keys) - { - var container = Containers.GetContainer(partId, GetPartSlotContainerId(slotId)); - - foreach (var containedEnt in container.ContainedEntities) - { - yield return containedEnt; - } - } - } - - public IEnumerable<(EntityUid AdjacentId, T Component)> GetBodyPartAdjacentPartsComponents( - EntityUid partId, - BodyPartComponent? part = null) - where T : IComponent - { - if (!Resolve(partId, ref part, logMissing: false)) - yield break; - - var query = GetEntityQuery(); - foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) - { - if (query.TryGetComponent(adjacentId, out var component)) - yield return (adjacentId, component); - } - } - - public bool TryGetBodyPartAdjacentPartsComponents( - EntityUid partId, - [NotNullWhen(true)] out List<(EntityUid AdjacentId, T Component)>? comps, - BodyPartComponent? part = null) - where T : IComponent - { - if (!Resolve(partId, ref part, logMissing: false)) - { - comps = null; - return false; - } - - var query = GetEntityQuery(); - comps = new List<(EntityUid AdjacentId, T Component)>(); - - foreach (var adjacentId in GetBodyPartAdjacentParts(partId, part)) - { - if (query.TryGetComponent(adjacentId, out var component)) - comps.Add((adjacentId, component)); - } - - if (comps.Count != 0) - return true; - - comps = null; - return false; - } - - #endregion -} diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs deleted file mode 100644 index a45966fcc37..00000000000 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Movement.Systems; -using Content.Shared.Standing; -using Robust.Shared.Containers; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.Body.Systems; - -public abstract partial class SharedBodySystem : EntitySystem -{ - /* - * See the body partial for how this works. - */ - - /// - /// Container ID prefix for any body parts. - /// - public const string PartSlotContainerIdPrefix = "body_part_slot_"; - - /// - /// Container ID for the ContainerSlot on the body entity itself. - /// - public const string BodyRootContainerId = "body_root_part"; - - /// - /// Container ID prefix for any body organs. - /// - public const string OrganSlotContainerIdPrefix = "body_organ_slot_"; - - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] protected readonly IPrototypeManager Prototypes = default!; - [Dependency] protected readonly DamageableSystem Damageable = default!; - [Dependency] protected readonly MovementSpeedModifierSystem Movement = default!; - [Dependency] protected readonly SharedContainerSystem Containers = default!; - [Dependency] protected readonly SharedTransformSystem SharedTransform = default!; - [Dependency] protected readonly StandingStateSystem Standing = default!; - - public override void Initialize() - { - base.Initialize(); - - InitializeBody(); - InitializeParts(); - } - - /// - /// Inverse of - /// - protected static string? GetPartSlotContainerIdFromContainer(string containerSlotId) - { - // This is blursed - var slotIndex = containerSlotId.IndexOf(PartSlotContainerIdPrefix, StringComparison.Ordinal); - - if (slotIndex < 0) - return null; - - var slotId = containerSlotId.Remove(slotIndex, PartSlotContainerIdPrefix.Length); - return slotId; - } - - /// - /// Gets the container Id for the specified slotId. - /// - public static string GetPartSlotContainerId(string slotId) - { - return PartSlotContainerIdPrefix + slotId; - } - - /// - /// Gets the container Id for the specified slotId. - /// - public static string GetOrganContainerId(string slotId) - { - return OrganSlotContainerIdPrefix + slotId; - } -} diff --git a/Content.Shared/Body/Systems/SharedInternalsSystem.cs b/Content.Shared/Body/Systems/SharedInternalsSystem.cs index 7cc53db7c96..5dca3cb8919 100644 --- a/Content.Shared/Body/Systems/SharedInternalsSystem.cs +++ b/Content.Shared/Body/Systems/SharedInternalsSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.IdentityManagement; using Content.Shared.Internals; using Content.Shared.Inventory; +using Content.Shared.Movement.Components; using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Containers; @@ -258,11 +259,15 @@ protected short GetSeverity(InternalsComponent component) Entity user) { // TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses - // Prioritise - // 1. back equipped tanks - // 2. exo-slot tanks - // 3. in-hand tanks - // 4. pocket/belt tanks + // Lookup order: + // 1. Back + // 2. Exo-slot + // 3. In-hand + // 4. Pocket/belt + // Jetpacks will only be used as a fallback if no other tank is found + + // Store the first jetpack seen + Entity? found = null; if (!Resolve(user, ref user.Comp2, ref user.Comp3)) return null; @@ -271,22 +276,36 @@ protected short GetSeverity(InternalsComponent component) TryComp(backEntity, out var backGasTank) && _gasTank.CanConnectToInternals((backEntity.Value, backGasTank))) { - return (backEntity.Value, backGasTank); + found = (backEntity.Value, backGasTank); + if (!HasComp(backEntity.Value)) + { + return found; + } } if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) && TryComp(entity, out var gasTank) && _gasTank.CanConnectToInternals((entity.Value, gasTank))) { - return (entity.Value, gasTank); + found ??= (entity.Value, gasTank); + if (!HasComp(entity.Value)) + { + return (entity.Value, gasTank); + } } foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2))) { if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank))) - return (item, gasTank); + { + found ??= (item, gasTank); + if (!HasComp(item)) + { + return (item, gasTank); + } + } } - return null; + return found; } } diff --git a/Content.Shared/Body/Systems/SharedMetabolizerSystem.cs b/Content.Shared/Body/Systems/SharedMetabolizerSystem.cs deleted file mode 100644 index 24ab438f6d8..00000000000 --- a/Content.Shared/Body/Systems/SharedMetabolizerSystem.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Shared.Body.Events; - -namespace Content.Shared.Body.Systems; - -public abstract class SharedMetabolizerSystem : EntitySystem -{ - /// - /// Updates the metabolic rate multiplier for a given entity, - /// raising both to determine what the multiplier is and to update relevant components. - /// - /// - public void UpdateMetabolicMultiplier(EntityUid uid) - { - var getEv = new GetMetabolicMultiplierEvent(); - RaiseLocalEvent(uid, ref getEv); - - var applyEv = new ApplyMetabolicMultiplierEvent(getEv.Multiplier); - RaiseLocalEvent(uid, ref applyEv); - } -} diff --git a/Content.Shared/Body/Systems/StomachSystem.cs b/Content.Shared/Body/Systems/StomachSystem.cs index 3d8647b14d9..9adc79c6ea4 100644 --- a/Content.Shared/Body/Systems/StomachSystem.cs +++ b/Content.Shared/Body/Systems/StomachSystem.cs @@ -1,138 +1,44 @@ using Content.Shared.Body.Components; -using Content.Shared.Body.Events; -using Content.Shared.Body.Organ; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; -using Robust.Shared.Containers; -using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Content.Shared.Body.Systems -{ - public sealed class StomachSystem : EntitySystem - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - - public const string DefaultSolutionName = "stomach"; - - public override void Initialize() - { - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnUnpaused); - SubscribeLocalEvent(OnEntRemoved); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - } - - private void OnMapInit(Entity ent, ref MapInitEvent args) - { - ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval; - } - - private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) - { - ent.Comp.NextUpdate += args.PausedTime; - } - - private void OnEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) - { - // Make sure the removed entity was our contained solution - if (ent.Comp.Solution is not { } solution || args.Entity != solution.Owner) - return; - - // Cleared our cached reference to the solution entity - ent.Comp.Solution = null; - } - - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var stomach, out var organ, out var sol)) - { - if (_gameTiming.CurTime < stomach.NextUpdate) - continue; +namespace Content.Shared.Body.Systems; - stomach.NextUpdate += stomach.AdjustedUpdateInterval; - - // Get our solutions - if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution, out var stomachSolution)) - continue; - - if (organ.Body is not { } body || !_solutionContainerSystem.TryGetSolution(body, stomach.BodySolutionName, out var bodySolution)) - continue; - - var transferSolution = new Solution(); - - var queue = new RemQueue(); - foreach (var delta in stomach.ReagentDeltas) - { - delta.Increment(stomach.AdjustedUpdateInterval); - if (delta.Lifetime > stomach.DigestionDelay) - { - if (stomachSolution.TryGetReagent(delta.ReagentQuantity.Reagent, out var reagent)) - { - if (reagent.Quantity > delta.ReagentQuantity.Quantity) - reagent = new(reagent.Reagent, delta.ReagentQuantity.Quantity); - - stomachSolution.RemoveReagent(reagent); - transferSolution.AddReagent(reagent); - } - - queue.Add(delta); - } - } - - foreach (var item in queue) - { - stomach.ReagentDeltas.Remove(item); - } - - _solutionContainerSystem.UpdateChemicals(stomach.Solution.Value); +public sealed class StomachSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - // Transfer everything to the body solution! - _solutionContainerSystem.TryAddSolution(bodySolution.Value, transferSolution); - } - } + public const string DefaultSolutionName = "stomach"; - private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args) - { - ent.Comp.UpdateIntervalMultiplier = args.Multiplier; - } + public bool CanTransferSolution( + EntityUid uid, + Solution solution, + StomachComponent? stomach = null, + SolutionContainerManagerComponent? solutions = null) + { + return Resolve(uid, ref stomach, ref solutions, logMissing: false) + && _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution) + // TODO: For now no partial transfers. Potentially change by design + && stomachSolution.CanAddSolution(solution); + } - public bool CanTransferSolution( - EntityUid uid, - Solution solution, - StomachComponent? stomach = null, - SolutionContainerManagerComponent? solutions = null) + public bool TryTransferSolution( + EntityUid uid, + Solution solution, + StomachComponent? stomach = null, + SolutionContainerManagerComponent? solutions = null) + { + if (!Resolve(uid, ref stomach, ref solutions, logMissing: false) + || !_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution) + || !CanTransferSolution(uid, solution, stomach, solutions)) { - return Resolve(uid, ref stomach, ref solutions, logMissing: false) - && _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution) - // TODO: For now no partial transfers. Potentially change by design - && stomachSolution.CanAddSolution(solution); + return false; } - public bool TryTransferSolution( - EntityUid uid, - Solution solution, - StomachComponent? stomach = null, - SolutionContainerManagerComponent? solutions = null) - { - if (!Resolve(uid, ref stomach, ref solutions, logMissing: false) - || !_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution) - || !CanTransferSolution(uid, solution, stomach, solutions)) - { - return false; - } - - _solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution); - // Add each reagent to ReagentDeltas. Used to track how long each reagent has been in the stomach - foreach (var reagent in solution.Contents) - { - stomach.ReagentDeltas.Add(new StomachComponent.ReagentDelta(reagent)); - } + _solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution); - return true; - } + return true; } } diff --git a/Content.Shared/Body/VisualBodyComponent.cs b/Content.Shared/Body/VisualBodyComponent.cs new file mode 100644 index 00000000000..96c3920a248 --- /dev/null +++ b/Content.Shared/Body/VisualBodyComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Body; + +/// +/// Component on an entity with that modifies its appearance based on contained organs with +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedVisualBodySystem))] +public sealed partial class VisualBodyComponent : Component; diff --git a/Content.Shared/Body/VisualOrganComponent.cs b/Content.Shared/Body/VisualOrganComponent.cs new file mode 100644 index 00000000000..6427c8f0e3a --- /dev/null +++ b/Content.Shared/Body/VisualOrganComponent.cs @@ -0,0 +1,65 @@ +using Content.Shared.Humanoid; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Body; + +/// +/// Defines an organ that applies a sprite to the specified within the body +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedVisualBodySystem))] +public sealed partial class VisualOrganComponent : Component +{ + /// + /// The sprite layer on the entity that this contributes to + /// + [DataField(required: true)] + public Enum Layer; + + /// + /// The sprite data for the layer + /// + [DataField(required: true), AutoNetworkedField, AlwaysPushInheritance] + public PrototypeLayerData Data; + + /// + /// When applying a profile, if the sex is present in this dictionary, overrides the state of the sprite data. + /// Used for e.g. male vs female torsoes. + /// + [DataField] + public Dictionary? SexStateOverrides; + + /// + /// The current profile data of this organ, used for alternate sprite selection and colouration. + /// + [DataField, AutoNetworkedField] + public OrganProfileData Profile = new(); +} + +/// +/// Defines the coloration, sex, etc. of organs +/// +[DataDefinition] +[Serializable, NetSerializable] +public partial record struct OrganProfileData +{ + /// + /// The "sex" of this organ + /// + [DataField] + public Sex Sex; + + /// + /// The "eye color" of this organ + /// + [DataField] + public Color EyeColor = Color.White; + + /// + /// The "skin color" of this organ + /// + [DataField] + public Color SkinColor = Color.White; +} + diff --git a/Content.Shared/Body/VisualOrganMarkingsComponent.cs b/Content.Shared/Body/VisualOrganMarkingsComponent.cs new file mode 100644 index 00000000000..31c34a42f99 --- /dev/null +++ b/Content.Shared/Body/VisualOrganMarkingsComponent.cs @@ -0,0 +1,70 @@ +using Content.Shared.DisplacementMap; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Body; + +/// +/// Defines an organ that applies markings on top of the layer specified in +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] +[Access(typeof(SharedVisualBodySystem))] +public sealed partial class VisualOrganMarkingsComponent : Component +{ + /// + /// Defines the type of markings this organ can take + /// + [DataField(required: true), AlwaysPushInheritance] + public OrganMarkingData MarkingData = default!; + + /// + /// The list of markings this organ is currently providing to the entity + /// + [DataField, AutoNetworkedField] + public Dictionary> Markings = new(); + + /// + /// Layers that are eligible for hiding based on e.g. clothing + /// + [DataField, AutoNetworkedField] + public HashSet HideableLayers = new(); + + /// + /// A dictionary of layers to other layers that visually depend on them for hiding, e.g. SnoutCover depends on Snout + /// + [DataField, AutoNetworkedField] + public Dictionary> DependentHidingLayers = new(); + + /// + /// Optional displacement data for this organ to apply to markings. + /// Only applies to markings which support displacement data. + /// + [DataField, AutoNetworkedField] + public Dictionary MarkingsDisplacement = new(); + + /// + /// Client only - the last markings applied by this component + /// + [ViewVariables] + public List AppliedMarkings = new(); +} + +/// +/// Defines the layers and group an organ takes markings for +/// +[DataDefinition] +[Serializable, NetSerializable] +public partial record struct OrganMarkingData +{ + [DataField(required: true)] + public HashSet Layers = default!; + + /// + /// The type of organ this is for markings + /// + [DataField(required: true)] + public ProtoId Group = default!; +} diff --git a/Content.Shared/Buckle/Components/ActiveIgniteOnBuckleComponent.cs b/Content.Shared/Buckle/Components/ActiveIgniteOnBuckleComponent.cs new file mode 100644 index 00000000000..f4fd4f59a4c --- /dev/null +++ b/Content.Shared/Buckle/Components/ActiveIgniteOnBuckleComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Buckle.Components; + +/// +/// Component for entities that are currently being ignited by +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentPause, AutoGenerateComponentState] +public sealed partial class ActiveIgniteOnBuckleComponent : Component +{ + // We cache data in this component and apply it to those who get buckled to have to do less lookups. + + /// + /// How many fire stacks to add per cycle. + /// + [DataField] + public float FireStacks = 0.5f; + + /// + /// How frequently the ignition should be applied, in seconds. + /// + [DataField] + public float IgniteTime = 1f; + + /// + /// Maximum fire stacks that can be added by this source. + /// If target already has this many or more fire stacks, no additional stacks will be added. + /// Null means unlimited. + /// + [DataField] + public float? MaxFireStacks = 2.5f; + + /// + /// Next time that fire stacks will be applied. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField, AutoNetworkedField] + public TimeSpan NextIgniteTime = TimeSpan.Zero; +} diff --git a/Content.Shared/Buckle/Components/IgniteOnBuckleComponent.cs b/Content.Shared/Buckle/Components/IgniteOnBuckleComponent.cs new file mode 100644 index 00000000000..61af16f792a --- /dev/null +++ b/Content.Shared/Buckle/Components/IgniteOnBuckleComponent.cs @@ -0,0 +1,31 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Buckle.Components; + +/// +/// Component that makes an entity ignite entities that are buckled to it. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class IgniteOnBuckleComponent : Component +{ + /// + /// How many fire stacks to add per cycle. + /// + [DataField] + public float FireStacks = 0.5f; + + /// + /// How frequently the ignition should be applied, in seconds. + /// + [DataField] + public float IgniteTime = 1f; + + /// + /// Maximum fire stacks that can be added by this source. + /// If target already has this many or more fire stacks, no additional stacks will be added. + /// Null means unlimited. + /// + [DataField] + public float? MaxFireStacks = 2.5f; +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 4681ab9ad77..4ccf7b9c3cc 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -244,7 +244,7 @@ private bool CanBuckle(EntityUid buckleUid, // Does it pass the Whitelist if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) || - _whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid)) + _whitelistSystem.IsWhitelistPass(strapComp.Blacklist, buckleUid)) { if (popup) _popup.PopupClient(Loc.GetString("buckle-component-cannot-fit-message"), user, PopupType.Medium); @@ -486,7 +486,7 @@ private void Unbuckle(Entity buckle, Entity str // TODO: This is doing 4 moveevents this is why I left the warning in, if you're going to remove it make it only do 1 moveevent. if (strap.Comp.BuckleOffset != Vector2.Zero) { - buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset); + _transform.SetCoordinates(buckle, buckleXform, oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset)); } } diff --git a/Content.Shared/CCVar/CCVars.Admin.cs b/Content.Shared/CCVar/CCVars.Admin.cs index 6065b364373..8f592499edb 100644 --- a/Content.Shared/CCVar/CCVars.Admin.cs +++ b/Content.Shared/CCVar/CCVars.Admin.cs @@ -196,4 +196,9 @@ public sealed partial class CCVars public static readonly CVarDef AdminAllowMultiServerPlay = CVarDef.Create("admin.allow_multi_server_play", true, CVar.SERVERONLY); + /// + /// Whether admins should gain role playtime even when playing while not de-adminned. + /// + public static readonly CVarDef GameAdminJobTracking = + CVarDef.Create("game.admin_job_tracking", false, CVar.SERVERONLY); } diff --git a/Content.Shared/CCVar/CCVars.Atmos.cs b/Content.Shared/CCVar/CCVars.Atmos.cs index 7ef40b7911b..eccee011512 100644 --- a/Content.Shared/CCVar/CCVars.Atmos.cs +++ b/Content.Shared/CCVar/CCVars.Atmos.cs @@ -138,18 +138,27 @@ public sealed partial class CCVars CVarDef.Create("atmos.speedup", 8f, CVar.SERVERONLY); /// - /// Like atmos.speedup, but only for gas and reaction heat values. 64x means - /// gases heat up and cool down 64x faster than real life. - /// + /// Like atmos.speedup, but only for gas and reaction heat values. 64x means + /// gases heat up and cool down 64x faster than real life. + /// + /// This is only supposed to facilitate the speedup of + /// heat transfer between bodies by making the heat capacities appear smaller. + /// In the context of a heat-only reaction like gas reactions creating heat, + /// you shouldn't be using the scaled specific heat. + /// So things like thermomachines and the TEG, which work based on heat transfer, would need to use the scaled value, + /// whereas heat-only reactions like gas reactions and anything else that "generates" or "consumes" heat would use the unscaled value. + /// + /// Mechanics like the thermomachine or TEG should be affected by this CVAR change, + /// whereas gas reactions and heat-only interactions should stay the same. public static readonly CVarDef AtmosHeatScale = - CVarDef.Create("atmos.heat_scale", 8f, CVar.SERVERONLY); + CVarDef.Create("atmos.heat_scale", 8f, CVar.REPLICATED | CVar.SERVER); /// - /// Maximum explosion radius for explosions caused by bursting a gas tank ("max caps"). - /// Setting this to zero disables the explosion but still allows the tank to burst and leak. + /// Maximum explosion intensity for explosions caused by bursting a gas tank ("max caps"). + /// Setting this to zero disables the limits. /// public static readonly CVarDef AtmosTankFragment = - CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY); + CVarDef.Create("atmos.max_explosion_range", 0f, CVar.SERVER); /// /// Whether atmospherics will process delta-pressure damage on entities with a DeltaPressureComponent. diff --git a/Content.Shared/CCVar/CCVars.Debug.cs b/Content.Shared/CCVar/CCVars.Debug.cs new file mode 100644 index 00000000000..a95acc4dac2 --- /dev/null +++ b/Content.Shared/CCVar/CCVars.Debug.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// Component to be inspected using the "Quick Inspect Component" keybind. + /// Set by the "quickinspect" command. + /// + public static readonly CVarDef DebugQuickInspect = + CVarDef.Create("debug.quick_inspect", "", CVar.CLIENTONLY | CVar.ARCHIVE); +} diff --git a/Content.Shared/CCVar/CCVars.Feedback.cs b/Content.Shared/CCVar/CCVars.Feedback.cs new file mode 100644 index 00000000000..90a3fc662c0 --- /dev/null +++ b/Content.Shared/CCVar/CCVars.Feedback.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// Used to set what popups are shown. Can accept multiple origins, just use spaces! See + /// FeedbackPopupPrototype's PopupOrigin field. + /// Only prototypes who's PopupOrigin matches one of the FeedbackValidOrigins will be shown to players. + /// + /// + /// wizden deltav + /// + public static readonly CVarDef FeedbackValidOrigins = + CVarDef.Create("feedback.valid_origins", "", CVar.SERVER | CVar.REPLICATED); +} diff --git a/Content.Shared/CCVar/CCVars.Game.cs b/Content.Shared/CCVar/CCVars.Game.cs index 151de34133e..cdafe8ecf17 100644 --- a/Content.Shared/CCVar/CCVars.Game.cs +++ b/Content.Shared/CCVar/CCVars.Game.cs @@ -409,4 +409,13 @@ public static readonly CVarDef /// public static readonly CVarDef GameHostnameInTitlebar = CVarDef.Create("game.hostname_in_titlebar", true, CVar.SERVER | CVar.REPLICATED); + + /// + /// The maximum amount of tiles you can stack on top of each other. 0 is unlimited. + /// + /// + /// Having it too high can result in "doomstacking" tiles - this messes with efficiency of explosions, deconstruction of tiles, and might result in memory problems. + /// + public static readonly CVarDef TileStackLimit = + CVarDef.Create("game.tile_stack_limit", 5, CVar.SERVER | CVar.REPLICATED); } diff --git a/Content.Shared/CCVar/CCVars.Ic.cs b/Content.Shared/CCVar/CCVars.Ic.cs index 5f8d108a221..a075bb01446 100644 --- a/Content.Shared/CCVar/CCVars.Ic.cs +++ b/Content.Shared/CCVar/CCVars.Ic.cs @@ -65,6 +65,13 @@ public sealed partial class CCVars public static readonly CVarDef ICRandomSpeciesWeights = CVarDef.Create("ic.random_species_weights", "SpeciesWeights", CVar.SERVER); + /// + /// The list of species that will NOT be given to new account joins when they are assigned a random character. + /// This only affects the first time a character is made for an account, nothing else. + /// + public static readonly CVarDef ICNewAccountSpeciesBlacklist = + CVarDef.Create("ic.blacklist_species_new_account", "Diona,Vulpkanin,Vox,SlimePerson", CVar.SERVER); + /// /// Control displaying SSD indicators near players /// diff --git a/Content.Shared/CCVar/CCVars.Interactions.cs b/Content.Shared/CCVar/CCVars.Interactions.cs index fcefa73a96a..401fabfa297 100644 --- a/Content.Shared/CCVar/CCVars.Interactions.cs +++ b/Content.Shared/CCVar/CCVars.Interactions.cs @@ -70,4 +70,17 @@ public sealed partial class CCVars /// public static readonly CVarDef NestedStorage = CVarDef.Create("control.nested_storage", true, CVar.REPLICATED | CVar.SERVER); + + /// + /// If enabled, melee weapons that have click-to-attack patterns (unarmed, slow weapons) will continue attacking if the button is held. + /// + public static readonly CVarDef ControlHoldToAttackMelee = + CVarDef.Create("control.hold_to_attack_melee", false, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// If enabled, ranged weapons that have click-to-attack patterns (burst and semi-auto guns) will continue attacking if the button is held. + /// + public static readonly CVarDef ControlHoldToAttackRanged = + CVarDef.Create("control.hold_to_attack_ranged", false, CVar.CLIENTONLY | CVar.ARCHIVE); + } diff --git a/Content.Shared/CCVar/CCVars.Net.cs b/Content.Shared/CCVar/CCVars.Net.cs index b7465def2eb..8cd8857d96e 100644 --- a/Content.Shared/CCVar/CCVars.Net.cs +++ b/Content.Shared/CCVar/CCVars.Net.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Configuration; +using Robust.Shared.Configuration; namespace Content.Shared.CCVar; diff --git a/Content.Shared/CCVar/CCVars.Shuttle.cs b/Content.Shared/CCVar/CCVars.Shuttle.cs index adb4110353e..797e3771006 100644 --- a/Content.Shared/CCVar/CCVars.Shuttle.cs +++ b/Content.Shared/CCVar/CCVars.Shuttle.cs @@ -95,10 +95,17 @@ public sealed partial class CCVars CVarDef.Create("shuttle.arrival_time", 5f, CVar.SERVERONLY); /// - /// How much time needs to pass before a shuttle can FTL again. + /// How much time in seconds that needs to pass before a non-arrivals shuttle can FTL again. /// public static readonly CVarDef FTLCooldown = - CVarDef.Create("shuttle.cooldown", 10f, CVar.SERVERONLY); + CVarDef.Create("shuttle.cooldown", 60f, CVar.SERVERONLY); + + /// + /// How much time in seconds that needs to pass before the arrivals shuttle can FTL again. + /// If this is adjusted, ensure that shuttle.arrivals_cooldown is longer than this value. + /// + public static readonly CVarDef ArrivalsFTLCooldown = + CVarDef.Create("shuttle.arrivals_ftl_cooldown", 10f, CVar.SERVERONLY); /// /// The maximum a grid can have before it becomes unable to FTL. diff --git a/Content.Shared/CCVar/CCVars.Worldgen.cs b/Content.Shared/CCVar/CCVars.Worldgen.cs deleted file mode 100644 index da165ce74a7..00000000000 --- a/Content.Shared/CCVar/CCVars.Worldgen.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Configuration; - -namespace Content.Shared.CCVar; - -public sealed partial class CCVars -{ - /// - /// Whether or not world generation is enabled. - /// - public static readonly CVarDef WorldgenEnabled = - CVarDef.Create("worldgen.enabled", false, CVar.SERVERONLY); - - /// - /// The worldgen config to use. - /// - public static readonly CVarDef WorldgenConfig = - CVarDef.Create("worldgen.worldgen_config", "Default", CVar.SERVERONLY); -} diff --git a/Content.Shared/Cabinet/ItemCabinetSystem.cs b/Content.Shared/Cabinet/ItemCabinetSystem.cs index 749065ac476..ea500aaeaaa 100644 --- a/Content.Shared/Cabinet/ItemCabinetSystem.cs +++ b/Content.Shared/Cabinet/ItemCabinetSystem.cs @@ -37,7 +37,7 @@ private void OnStartup(Entity ent, ref ComponentStartup ar private void OnMapInit(Entity ent, ref MapInitEvent args) { // update at mapinit to avoid copy pasting locked: true and locked: false for each closed/open prototype - SetSlotLock(ent, !_openable.IsOpen(ent)); + SetSlotLock(ent, _openable.IsClosed(ent, null)); } private void UpdateAppearance(Entity ent) diff --git a/Content.Shared/Cargo/CargoOrderData.cs b/Content.Shared/Cargo/CargoOrderData.cs index a0b3fbed50a..f7e56e59f56 100644 --- a/Content.Shared/Cargo/CargoOrderData.cs +++ b/Content.Shared/Cargo/CargoOrderData.cs @@ -7,12 +7,6 @@ namespace Content.Shared.Cargo [DataDefinition, NetSerializable, Serializable] public sealed partial class CargoOrderData { - /// - /// Price when the order was added. - /// - [DataField] - public int Price; - /// /// A unique (arbitrary) ID which identifies this order. /// @@ -20,16 +14,10 @@ public sealed partial class CargoOrderData public int OrderId { get; private set; } /// - /// Prototype Id for the item to be created - /// - [DataField] - public string ProductId { get; private set; } - - /// - /// Prototype Name + /// The ID of the cargo product ordered. /// [DataField] - public string ProductName { get; private set; } + public ProtoId Product; /// /// The number of items in the order. Not readonly, as it might change @@ -60,12 +48,10 @@ public sealed partial class CargoOrderData [DataField] public ProtoId Account; - public CargoOrderData(int orderId, string productId, string productName, int price, int amount, string requester, string reason, ProtoId account) + public CargoOrderData(int orderId, ProtoId product, int amount, string requester, string reason, ProtoId account) { OrderId = orderId; - ProductId = productId; - ProductName = productName; - Price = price; + Product = product; OrderQuantity = amount; Requester = requester; Reason = reason; diff --git a/Content.Shared/Cargo/Components/TradeStationComponent.cs b/Content.Shared/Cargo/Components/TradeStationComponent.cs new file mode 100644 index 00000000000..ec0cba53866 --- /dev/null +++ b/Content.Shared/Cargo/Components/TradeStationComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.HijackBeacon; +using Robust.Shared.GameStates; + +namespace Content.Shared.Cargo.Components; + +/// +/// Target for approved orders to spawn at. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TradeStationComponent : Component +{ + /// + /// The Trade Station's current hijack state. Modified by HijackBeaconSystem. + /// + [DataField, AutoNetworkedField] + public bool Hacked = false; +} diff --git a/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs index 46a3a0de137..592b4d8b526 100644 --- a/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs +++ b/Content.Shared/Cargo/Prototypes/CargoProductPrototype.cs @@ -1,5 +1,6 @@ +using Robust.Shared.Containers; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; @@ -17,9 +18,15 @@ public sealed partial class CargoProductPrototype : IPrototype, IInheritingProto [AbstractDataField] public bool Abstract { get; private set; } - [DataField("name")] private string _name = string.Empty; + [DataField("name")] + private LocId? _nameLoc; - [DataField("description")] private string _description = string.Empty; + private string _name = string.Empty; + + [DataField("description")] + private LocId? _descLoc; + + private string _description = string.Empty; [ViewVariables] [IdDataField] @@ -36,7 +43,11 @@ public string Name if (_name.Trim().Length != 0) return _name; - if (IoCManager.Resolve().Resolve(Product, out EntityPrototype? prototype)) + if (_nameLoc is { } nameLoc) + { + _name = Loc.GetString(nameLoc); + } + else if (IoCManager.Resolve().Resolve(Product, out var prototype)) { _name = prototype.Name; } @@ -56,7 +67,11 @@ public string Description if (_description.Trim().Length != 0) return _description; - if (IoCManager.Resolve().Resolve(Product, out EntityPrototype? prototype)) + if (_descLoc is { } descLoc) + { + _description = Loc.GetString(descLoc); + } + else if (IoCManager.Resolve().Resolve(Product, out var prototype)) { _description = prototype.Description; } @@ -77,6 +92,12 @@ public string Description [DataField] public EntProtoId Product { get; private set; } = string.Empty; + /// + /// The entity to spawn and insert the product into. If null, just the product is spawned. + /// + [DataField] + public CargoProductContainer? Container; + /// /// The point cost of the product. /// @@ -95,4 +116,21 @@ public string Description [DataField] public ProtoId Group { get; private set; } = "market"; } + + /// + [DataDefinition, Serializable, NetSerializable] + public sealed partial class CargoProductContainer + { + /// + /// What entity to spawn as the container. + /// + [DataField(required: true)] + public EntProtoId Entity; + + /// + /// What container in the product should be inserted into. + /// + [DataField(required: true)] + public string ContainerId; + } } diff --git a/Content.Shared/Cargo/SharedCargoSystem.cs b/Content.Shared/Cargo/SharedCargoSystem.cs index 8925ce0de14..291d3abbc31 100644 --- a/Content.Shared/Cargo/SharedCargoSystem.cs +++ b/Content.Shared/Cargo/SharedCargoSystem.cs @@ -1,5 +1,7 @@ using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using Content.Shared.HijackBeacon; +using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; @@ -16,6 +18,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnHijackSuccess); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -24,6 +27,23 @@ private void OnMapInit(Entity ent, ref MapInitEvent Dirty(ent); } + private void OnHijackSuccess(ref HijackBeaconSuccessEvent args) + { + var stationQuery = EntityQueryEnumerator(); + while (stationQuery.MoveNext(out var uid, out var comp)) + { + foreach (var (account, cash) in comp.Accounts) + { + comp.Accounts[account] = cash - args.Fine; + args.Total += args.Fine; + } + + var ev = new BankBalanceUpdatedEvent(uid, comp.Accounts); + RaiseLocalEvent(uid, ref ev, true); + Dirty(uid, comp); + } + } + /// /// For a given station, retrieves the balance in a specific account. /// @@ -55,6 +75,151 @@ public Dictionary, double> CreateAccountDistribut } return distribution; } + + /// + /// Returns information about the given bank account. + /// + /// Station to get bank account info from. + /// Bank account prototype ID to get info for. + /// The amount of money in the account + /// Whether or not the bank account exists. + public bool TryGetAccount(Entity station, ProtoId accountPrototypeId, out int money) + { + money = 0; + + if (!Resolve(station, ref station.Comp)) + return false; + + return station.Comp.Accounts.TryGetValue(accountPrototypeId, out money); + } + + /// + /// Returns a readonly dictionary of all accounts and their money info. + /// + /// Station to get bank account info from. + /// Whether or not the bank account exists. + public IReadOnlyDictionary, int> GetAccounts(Entity station) + { + if (!Resolve(station, ref station.Comp)) + return new Dictionary, int>(); + + return station.Comp.Accounts; + } + + /// + /// Attempts to adjust the money of a certain bank account. + /// + /// Station where the bank account is from + /// the id of the bank account + /// how much money to set the account to + /// Whether or not it should create the account if it doesn't exist. + /// Whether to mark the bank account component as dirty. + /// Whether or not setting the value succeeded. + public bool TryAdjustBankAccount( + Entity station, + ProtoId accountPrototypeId, + int money, + bool createAccount = false, + bool dirty = true) + { + if (!Resolve(station, ref station.Comp)) + return false; + + var accounts = station.Comp.Accounts; + + if (!accounts.ContainsKey(accountPrototypeId) && !createAccount) + return false; + + accounts[accountPrototypeId] += money; + var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts); + RaiseLocalEvent(station, ref ev, true); + + if (!dirty) + return true; + + Dirty(station); + return true; + } + + /// + /// Attempts to set the money of a certain bank account. + /// + /// Station where the bank account is from + /// the id of the bank account + /// how much money to set the account to + /// Whether or not it should create the account if it doesn't exist. + /// Whether to mark the bank account component as dirty. + /// Whether or not setting the value succeeded. + public bool TrySetBankAccount( + Entity station, + ProtoId accountPrototypeId, + int money, + bool createAccount = false, + bool dirty = true) + { + if (!Resolve(station, ref station.Comp)) + return false; + + var accounts = station.Comp.Accounts; + + if (!accounts.ContainsKey(accountPrototypeId) && !createAccount) + return false; + + accounts[accountPrototypeId] = money; + var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts); + RaiseLocalEvent(station, ref ev, true); + + if (!dirty) + return true; + + Dirty(station); + return true; + } + + public void UpdateBankAccount( + Entity ent, + int balanceAdded, + ProtoId account, + bool dirty = true) + { + UpdateBankAccount( + ent, + balanceAdded, + new Dictionary, double> { {account, 1} }, + dirty: dirty); + } + + /// + /// Adds or removes funds from the . + /// + /// The station. + /// The amount of funds to add or remove. + /// The distribution between individual . + /// Whether to mark the bank account component as dirty. + [PublicAPI] + public void UpdateBankAccount( + Entity ent, + int balanceAdded, + Dictionary, double> accountDistribution, + bool dirty = true) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + foreach (var (account, percent) in accountDistribution) + { + var accountBalancedAdded = (int) Math.Round(percent * balanceAdded); + ent.Comp.Accounts[account] += accountBalancedAdded; + } + + var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts); + RaiseLocalEvent(ent, ref ev, true); + + if (!dirty) + return; + + Dirty(ent); + } } [NetSerializable, Serializable] diff --git a/Content.Shared/Changeling/ChangelingTransformEvents.cs b/Content.Shared/Changeling/ChangelingTransformEvents.cs index 9940a607052..9a4ff4454ef 100644 --- a/Content.Shared/Changeling/ChangelingTransformEvents.cs +++ b/Content.Shared/Changeling/ChangelingTransformEvents.cs @@ -14,3 +14,26 @@ public sealed partial class ChangelingTransformActionEvent : InstantActionEvent; /// [Serializable, NetSerializable] public sealed partial class ChangelingTransformDoAfterEvent : SimpleDoAfterEvent; + +/// +/// Raised on a changeling before they transform into a stored identity. +/// This is raised after the DoAfter finished. +/// +public readonly record struct BeforeChangelingTransformEvent(EntityUid StoredIdentity) +{ + /// + /// The stored identity the changeling will transform into. + /// + public readonly EntityUid StoredIdentity = StoredIdentity; +}; + +/// +/// Raised on a changeling after they successfully transformed into a stored identity. +/// +public readonly record struct AfterChangelingTransformEvent(EntityUid StoredIdentity) +{ + /// + /// The stored identity the changeling transformed into. + /// + public readonly EntityUid StoredIdentity = StoredIdentity; +}; diff --git a/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs b/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs new file mode 100644 index 00000000000..20cb6908352 --- /dev/null +++ b/Content.Shared/Changeling/Components/ChangelingClonerComponent.cs @@ -0,0 +1,100 @@ +using Content.Shared.Charges.Components; +using Content.Shared.Cloning; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Changeling.Components; + +/// +/// Changeling transformation in item form! +/// An entity with this component works like an implanter: +/// First you use it on a humanoid to make a copy of their identity, along with all species relevant components, +/// then use it on someone else to tranform them into a clone of them. +/// Can be used in combination with +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChangelingClonerComponent : Component +{ + /// + /// A clone of the player you have copied the identity from. + /// This is a full humanoid backup, stored on a paused map. + /// + /// + /// Since this entity is stored on a separate map it will be outside PVS range. + /// + [DataField, AutoNetworkedField] + public EntityUid? ClonedBackup; + + /// + /// Current state of the item. + /// + [DataField, AutoNetworkedField] + public ChangelingClonerState State = ChangelingClonerState.Empty; + + /// + /// The cloning settings to use. + /// + [DataField, AutoNetworkedField] + public ProtoId Settings = "ChangelingCloningSettings"; + + /// + /// Doafter time for drawing and injecting. + /// + [DataField, AutoNetworkedField] + public TimeSpan DoAfter = TimeSpan.FromSeconds(5); + + /// + /// Can this item be used more than once? + /// + [DataField, AutoNetworkedField] + public bool Reusable = true; + + /// + /// Whether or not to add a reset verb to purge the stored identity, + /// allowing you to draw a new one. + /// + [DataField, AutoNetworkedField] + public bool CanReset = true; + + /// + /// Raise events when renaming the target? + /// This will change their ID card, crew manifest entry, and so on. + /// For admeme purposes. + /// + [DataField, AutoNetworkedField] + public bool RaiseNameChangeEvents; + + /// + /// The sound to play when taking someone's identity with the item. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? DrawSound; + + /// + /// The sound to play when someone is transformed. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? InjectSound; +} + +/// +/// Current state of the item. +/// +[Serializable, NetSerializable] +public enum ChangelingClonerState : byte +{ + /// + /// No sample taken yet. + /// + Empty, + /// + /// Filled with a DNA sample. + /// + Filled, + /// + /// Has been used (single use only). + /// + Spent, +} diff --git a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs index a874afe9ce6..f0a7bb44ba3 100644 --- a/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingDevourComponent.cs @@ -1,12 +1,10 @@ using Content.Shared.Changeling.Systems; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Changeling.Components; @@ -14,24 +12,24 @@ namespace Content.Shared.Changeling.Components; /// Component responsible for Changelings Devour attack. Including the amount of damage /// and how long it takes to devour someone /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(ChangelingDevourSystem))] public sealed partial class ChangelingDevourComponent : Component { /// - /// The Action for devouring + /// The action for devouring. /// [DataField] public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour"; /// - /// The action entity associated with devouring + /// The action entity associated with devouring. /// [DataField, AutoNetworkedField] public EntityUid? ChangelingDevourActionEntity; /// - /// The whitelist of targets for devouring + /// The whitelist of targets for devouring. /// [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist = new() @@ -39,12 +37,12 @@ public sealed partial class ChangelingDevourComponent : Component Components = [ "MobState", - "HumanoidAppearance", + "HumanoidProfile", ], }; /// - /// The Sound to use during consumption of a victim + /// The sound to use during consumption of a victim. /// /// /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. @@ -54,7 +52,7 @@ public sealed partial class ChangelingDevourComponent : Component public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6)); /// - /// The Sound to use during the windup before consuming a victim + /// The sound to use during the windup before consuming a victim. /// /// /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. @@ -64,42 +62,31 @@ public sealed partial class ChangelingDevourComponent : Component public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6)); /// - /// The time between damage ticks - /// - [DataField, AutoNetworkedField] - public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1); - - /// - /// The windup time before the changeling begins to engage in devouring the identity of a target + /// The windup time before the changeling begins to engage in devouring the identity of a target. /// [DataField, AutoNetworkedField] public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2); /// - /// The time it takes to FULLY consume someones identity. + /// The time it takes to consume someones identity. + /// Starts after the windup. /// [DataField, AutoNetworkedField] public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10); /// - /// Damage cap that a target is allowed to be caused due to IdentityConsumption - /// - [DataField, AutoNetworkedField] - public float DevourConsumeDamageCap = 350f; - - /// - /// The Currently active devour sound in the world + /// The currently active devour sound in the world. /// [DataField] public EntityUid? CurrentDevourSound; /// - /// The damage profile for a single tick of devour damage + /// The damage dealt after the windup finished and devouring started. /// [DataField, AutoNetworkedField] - public DamageSpecifier DamagePerTick = new() + public DamageSpecifier WindupDamage = new() { - DamageDict = new Dictionary + DamageDict = new() { { "Slash", 10}, { "Piercing", 10 }, @@ -108,7 +95,21 @@ public sealed partial class ChangelingDevourComponent : Component }; /// - /// The list of protective damage types capable of preventing a devour if over the threshold + /// The damage dealt after the devouring is fully finished. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier DevourDamage = new() + { + DamageDict = new() + { + { "Slash", 20}, + { "Piercing", 20 }, + { "Blunt", 10 }, + }, + }; + + /// + /// The list of protective damage types capable of preventing a devour if over the threshold. /// [DataField, AutoNetworkedField] public List> ProtectiveDamageTypes = new() @@ -119,13 +120,7 @@ public sealed partial class ChangelingDevourComponent : Component }; /// - /// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event)) - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] - public TimeSpan NextTick = TimeSpan.Zero; - - /// - /// The percentage of ANY brute damage resistance that will prevent devouring + /// The percentage of ANY brute damage resistance that will prevent devouring. /// [DataField, AutoNetworkedField] public float DevourPreventionPercentageThreshold = 0.1f; diff --git a/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs new file mode 100644 index 00000000000..c235ea071a9 --- /dev/null +++ b/Content.Shared/Changeling/Components/ChangelingDevouredComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling.Components; + +/// +/// Component used for marking entities devoured by a changeling. +/// Used to prevent granting the identity several times. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChangelingDevouredComponent : Component +{ + /// + /// HashSet of all changelings that have devoured this entity. + /// + // TODO: This should be using some sort of relation system in the future. + [DataField, AutoNetworkedField] + public HashSet DevouredBy = new(); +} diff --git a/Content.Shared/Changeling/Components/ChangelingFleshClothingAbilityComponent.cs b/Content.Shared/Changeling/Components/ChangelingFleshClothingAbilityComponent.cs new file mode 100644 index 00000000000..40df5b2bad2 --- /dev/null +++ b/Content.Shared/Changeling/Components/ChangelingFleshClothingAbilityComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.Alert; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling.Components; + +/// +/// Allows the changeling to spawn dummy chameleon clothing items that will transform with them, +/// mimicing the equipment of the stored disguise. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChangelingFleshClothingAbilityComponent : Component +{ + /// + /// Is the ability currently enabled? + /// + [DataField, AutoNetworkedField] + public bool Enabled = true; + + /// + /// The alert for showing if the ability is active and for toggling it. + /// + [DataField] + public ProtoId AlertId = "ChangelingFleshClothing"; + + /// + /// The chameleon clothing items to spawn into any empty slots if the changeling transformed. + /// These need so that they will change their visuals according to the identity we transformed into. + /// The key of the dictionary is the corresponding inventory slot. + /// + [DataField] + public Dictionary ClothingPrototypes = new(); +} diff --git a/Content.Shared/Changeling/Components/ChangelingFleshClothingComponent.cs b/Content.Shared/Changeling/Components/ChangelingFleshClothingComponent.cs new file mode 100644 index 00000000000..affd1f3a8ff --- /dev/null +++ b/Content.Shared/Changeling/Components/ChangelingFleshClothingComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared.Clothing.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling.Components; + +/// +/// Allows this clothing item to transform along with the changeling so that it looks like whatever the mob we transform into is wearing in that slot. +/// Requires and . +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ChangelingFleshClothingComponent : Component; diff --git a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs index 8e74f835373..bef822b720f 100644 --- a/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs +++ b/Content.Shared/Changeling/Components/ChangelingIdentityComponent.cs @@ -13,11 +13,12 @@ public sealed partial class ChangelingIdentityComponent : Component { /// /// The list of entities that exist on a paused map. They are paused clones of the victims that the ling has consumed, with all relevant components copied from the original. + /// The key is the EntityUid of the stored identity, the value is the original entity the identity came from. + /// The value will be set to null if that entity is deleted. /// - // TODO: Store a reference to the original entity as well so you cannot infinitely devour somebody. Currently very tricky due the inability to send over EntityUid if the original is ever deleted. Can be fixed by something like WeakEntityReference. + // TODO: This should be handled via a relation system in the future. [DataField, AutoNetworkedField] - public List ConsumedIdentities = new(); - + public Dictionary ConsumedIdentities = new(); /// /// The currently assumed identity. @@ -26,8 +27,8 @@ public sealed partial class ChangelingIdentityComponent : Component public EntityUid? CurrentIdentity; /// - /// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their - /// respective systems. + /// The cloning settings to use when cloning a devoured identity to the paused map. + /// This contains a whitelist of all components that need to be backed up so that the changeling can transform into them later. /// [DataField] public ProtoId IdentityCloningSettings = "ChangelingCloningSettings"; diff --git a/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs b/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs new file mode 100644 index 00000000000..63a0a67f9b5 --- /dev/null +++ b/Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs @@ -0,0 +1,309 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Body; +using Content.Shared.Changeling.Components; +using Content.Shared.Cloning; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Forensics.Systems; +using Content.Shared.Humanoid; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Changeling.Systems; + +public sealed class ChangelingClonerSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedCloningSystem _cloning = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; + [Dependency] private readonly SharedForensicsSystem _forensics = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent>(OnGetVerbs); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnDraw); + SubscribeLocalEvent(OnInject); + SubscribeLocalEvent(OnShutDown); + } + + private void OnShutDown(Entity ent, ref ComponentShutdown args) + { + // Delete the stored clone. + PredictedQueueDel(ent.Comp.ClonedBackup); + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + var msg = ent.Comp.State switch + { + ChangelingClonerState.Empty => "changeling-cloner-component-empty", + ChangelingClonerState.Filled => "changeling-cloner-component-filled", + ChangelingClonerState.Spent => "changeling-cloner-component-spent", + _ => "error" + }; + + args.PushMarkup(Loc.GetString(msg)); + + } + + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || args.Hands == null) + return; + + if (!ent.Comp.CanReset || ent.Comp.State == ChangelingClonerState.Spent) + return; + + var user = args.User; + args.Verbs.Add(new Verb + { + Text = Loc.GetString("changeling-cloner-component-reset-verb"), + Disabled = ent.Comp.ClonedBackup == null, + Act = () => Reset(ent.AsNullable(), user), + DoContactInteraction = true, + }); + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target == null) + return; + + switch (ent.Comp.State) + { + case ChangelingClonerState.Empty: + args.Handled |= TryDraw(ent.AsNullable(), args.Target.Value, args.User); + break; + case ChangelingClonerState.Filled: + args.Handled |= TryInject(ent.AsNullable(), args.Target.Value, args.User); + break; + case ChangelingClonerState.Spent: + default: + break; + } + + } + + private void OnDraw(Entity ent, ref ClonerDrawDoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Target == null) + return; + + Draw(ent.AsNullable(), args.Target.Value, args.User); + args.Handled = true; + } + + private void OnInject(Entity ent, ref ClonerInjectDoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Target == null) + return; + + Inject(ent.AsNullable(), args.Target.Value, args.User); + args.Handled = true; + } + + /// + /// Start a DoAfter to draw a DNA sample from the target. + /// + public bool TryDraw(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.State != ChangelingClonerState.Empty) + return false; + + if (!HasComp(target)) + return false; // cloning only works for humanoids at the moment + + var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + }; + + if (!_doAfter.TryStartDoAfter(args)) + return false; + + var userIdentity = Identity.Entity(user, EntityManager); + var targetIdentity = Identity.Entity(target, EntityManager); + var userMsg = Loc.GetString("changeling-cloner-component-draw-user", ("user", userIdentity), ("target", targetIdentity)); + var targetMsg = Loc.GetString("changeling-cloner-component-draw-target", ("user", userIdentity), ("target", targetIdentity)); + _popup.PopupClient(userMsg, target, user); + + if (user != target) // don't show the warning if using the item on yourself + _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution); + + return true; + } + + /// + /// Start a DoAfter to inject a DNA sample into someone, turning them into a clone of the original. + /// + public bool TryInject(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (ent.Comp.State != ChangelingClonerState.Filled) + return false; + + if (!HasComp(target)) + return false; // cloning only works for humanoids at the moment + + var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + }; + + if (!_doAfter.TryStartDoAfter(args)) + return false; + + var userIdentity = Identity.Entity(user, EntityManager); + var targetIdentity = Identity.Entity(target, EntityManager); + var userMsg = Loc.GetString("changeling-cloner-component-inject-user", ("user", userIdentity), ("target", targetIdentity)); + var targetMsg = Loc.GetString("changeling-cloner-component-inject-target", ("user", userIdentity), ("target", targetIdentity)); + _popup.PopupClient(userMsg, target, user); + + if (user != target) // don't show the warning if using the item on yourself + _popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution); + + return true; + } + + /// + /// Draw a DNA sample from the target. + /// This will create a clone stored on a paused map for data storage. + /// + public void Draw(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.State != ChangelingClonerState.Empty) + return; + + if (!HasComp(target)) + return; // cloning only works for humanoids at the moment + + _adminLogger.Add(LogType.Identity, + $"{user} is using {ent.Owner} to draw DNA from {target}."); + + // Make a copy of the target on a paused map, so that we can apply their components later. + ent.Comp.ClonedBackup = _changelingIdentity.CloneToPausedMap(ent.Comp.Settings, target); + if (ent.Comp.ClonedBackup == null) + return; + + ent.Comp.State = ChangelingClonerState.Filled; + _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Filled); + Dirty(ent); + + _audio.PlayPredicted(ent.Comp.DrawSound, target, user); + _forensics.TransferDna(ent, target); + } + + /// + /// Inject a DNA sample into someone, turning them into a clone of the original. + /// + public void Inject(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.State != ChangelingClonerState.Filled) + return; + + if (!HasComp(target)) + return; // cloning only works for humanoids at the moment + + if (!_prototype.Resolve(ent.Comp.Settings, out var settings)) + return; + + _audio.PlayPredicted(ent.Comp.InjectSound, target, user); + _forensics.TransferDna(ent, target); // transfer DNA before overwriting it + + if (!ent.Comp.Reusable) + { + ent.Comp.State = ChangelingClonerState.Spent; + _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Spent); + Dirty(ent); + } + + if (!Exists(ent.Comp.ClonedBackup)) + return; // the entity is likely out of PVS range on the client + + _adminLogger.Add(LogType.Identity, + $"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}."); + + // Do the actual transformation. + _visualBody.CopyAppearanceFrom(ent.Comp.ClonedBackup.Value, target); + _cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings); + _metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents); + + } + + /// + /// Purge the stored DNA and allow to draw again. + /// + public void Reset(Entity ent, EntityUid? user) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + // Delete the stored clone. + PredictedQueueDel(ent.Comp.ClonedBackup); + ent.Comp.ClonedBackup = null; + ent.Comp.State = ChangelingClonerState.Empty; + _appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Empty); + Dirty(ent); + + if (user == null) + return; + + _popup.PopupClient(Loc.GetString("changeling-cloner-component-reset-popup"), user.Value, user.Value); + } +} + +/// +/// Doafter event for drawing a DNA sample. +/// +[Serializable, NetSerializable] +public sealed partial class ClonerDrawDoAfterEvent : SimpleDoAfterEvent; + +/// +/// DoAfterEvent for injecting a DNA sample, turning a player into someone else. +/// +[Serializable, NetSerializable] +public sealed partial class ClonerInjectDoAfterEvent : SimpleDoAfterEvent; + +/// +/// Key for the generic visualizer. +/// +[Serializable, NetSerializable] +public enum ChangelingClonerVisuals : byte +{ + State, +} diff --git a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs index 500ee06b22d..e0cb17f51fb 100644 --- a/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs @@ -2,29 +2,23 @@ using Content.Shared.Administration.Logs; using Content.Shared.Armor; using Content.Shared.Atmos.Rotting; -using Content.Shared.Body.Components; using Content.Shared.Changeling.Components; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; -using Content.Shared.Nutrition.Components; using Content.Shared.Popups; -using Content.Shared.Storage; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; -using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Shared.Changeling.Systems; public sealed class ChangelingDevourSystem : EntitySystem { - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; @@ -33,10 +27,8 @@ public sealed class ChangelingDevourSystem : EntitySystem [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; public override void Initialize() { @@ -46,7 +38,6 @@ public override void Initialize() SubscribeLocalEvent(OnDevourAction); SubscribeLocalEvent(OnDevourWindup); SubscribeLocalEvent(OnDevourConsume); - SubscribeLocalEvent>(OnConsumeAttemptTick); SubscribeLocalEvent(OnShutdown); } @@ -63,90 +54,25 @@ private void OnShutdown(Entity ent, ref ComponentShut } } - //TODO: Allow doafters to have proper update loop support. Attempt events should not be doing state changes. - private void OnConsumeAttemptTick(Entity ent, - ref DoAfterAttemptEvent eventData) - { - - var curTime = _timing.CurTime; - - if (curTime < ent.Comp.NextTick) - return; - - ConsumeDamageTick(eventData.Event.Target, ent.Comp, eventData.Event.User); - ent.Comp.NextTick += ent.Comp.DamageTimeBetweenTicks; - Dirty(ent, ent.Comp); - } - - private void ConsumeDamageTick(EntityUid? target, ChangelingDevourComponent comp, EntityUid? user) - { - if (target == null) - return; - - if (!TryComp(target, out var damage)) - return; - - foreach (var damagePoints in comp.DamagePerTick.DamageDict) - { - - if (damage.Damage.DamageDict.TryGetValue(damagePoints.Key, out var val) && val > comp.DevourConsumeDamageCap) - return; - } - _damageable.TryChangeDamage(target, comp.DamagePerTick, true, true, damage, user); - } - - /// - /// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. - /// - /// The Targeted entity - /// Changelings Devour Component - /// Is the target Protected from the attack - private bool IsTargetProtected(EntityUid target, Entity ent) - { - var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); - - RaiseLocalEvent(target, ev); - - foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) - { - if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) - continue; - if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) - return true; - } - - return false; - } - + // The action was used. + // Start the first doafter for the windup. private void OnDevourAction(Entity ent, ref ChangelingDevourActionEvent args) { - if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target) - || !HasComp(ent)) + if (args.Handled + || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target) + || !HasComp(ent)) return; args.Handled = true; var target = args.Target; - if (target == ent.Owner) - return; // don't eat yourself - - if (HasComp(target)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium); + if (!CanDevour(ent.AsNullable(), target)) return; - } - - if (IsTargetProtected(target, ent)) - { - _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), ent, ent, PopupType.Medium); - return; - } if (_net.IsServer) { - var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent); - if (pvsSound != null) - ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); + ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent)?.Entity; } _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ent:player} started changeling devour windup against {target:player}"); @@ -154,7 +80,7 @@ private void OnDevourAction(Entity ent, ref Changelin _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent) { BreakOnMove = true, - BlockDuplicate = true, + CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, }); @@ -168,110 +94,171 @@ private void OnDevourAction(Entity ent, ref Changelin PopupType.MediumCaution); } + // First doafter finished. + // Start the second doafter for the actual consumption and deal a small amount of damage. private void OnDevourWindup(Entity ent, ref ChangelingDevourWindupDoAfterEvent args) { - var curTime = _timing.CurTime; args.Handled = true; - - if (!EntityManager.EntityExists(ent.Comp.CurrentDevourSound)) - _audio.Stop(ent.Comp.CurrentDevourSound!); + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); if (args.Cancelled) return; + if (args.Target is not { } target) + return; + + _damageable.ChangeDamage(target, ent.Comp.WindupDamage, true, true, ent.Owner); + var selfMessage = Loc.GetString("changeling-devour-begin-consume-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-devour-begin-consume-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupPredicted( selfMessage, othersMessage, - args.User, - args.User, + ent.Owner, + ent.Owner, PopupType.LargeCaution); if (_net.IsServer) - { - var pvsSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent); - - if (pvsSound != null) - ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; - } - + ent.Comp.CurrentDevourSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent)?.Entity; - ent.Comp.NextTick = curTime + ent.Comp.DamageTimeBetweenTicks; - - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(args.Target):player} identity"); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(target):player}'s identity"); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourConsumeTime, new ChangelingDevourConsumeDoAfterEvent(), ent, - target: args.Target, + target: target, used: ent) { - AttemptFrequency = AttemptFrequency.EveryTick, BreakOnMove = true, - BlockDuplicate = true, + CancelDuplicate = true, DuplicateCondition = DuplicateConditions.None, }); } + // Second doafter finished. + // Save the identity and deal more damage. private void OnDevourConsume(Entity ent, ref ChangelingDevourConsumeDoAfterEvent args) { args.Handled = true; - var target = args.Target; + ent.Comp.CurrentDevourSound = _audio.Stop(ent.Comp.CurrentDevourSound); - if (target == null) + if (args.Cancelled) return; - if (EntityManager.EntityExists(ent.Comp.CurrentDevourSound)) - _audio.Stop(ent.Comp.CurrentDevourSound!); - - if (args.Cancelled) + if (args.Target is not { } target) return; - if (!_mobState.IsDead((EntityUid)target)) - { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity"); - _popupSystem.PopupClient(Loc.GetString("changeling-devour-consume-failed-not-dead"), args.User, args.User, PopupType.Medium); + // Damage first before the CanDevour check to make sure they don't gib in-between and to kill them again in case they somehow revived. + _damageable.ChangeDamage(target, ent.Comp.DevourDamage, true, true, ent.Owner); + + if (!CanDevour(ent.AsNullable(), target)) // Check again if the conditions are still met. return; - } - var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(args.User, EntityManager))); - var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(args.User, EntityManager))); + var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(ent.Owner, EntityManager))); + var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(ent.Owner, EntityManager))); _popupSystem.PopupPredicted( selfMessage, othersMessage, - args.User, - args.User, + ent.Owner, + ent.Owner, PopupType.LargeCaution); - if (_mobState.IsDead(target.Value) - && TryComp(target, out var body) - && HasComp(target) - && TryComp(args.User, out var identityStorage)) + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(target):player}'s identity"); + + if (!TryComp(ent.Owner, out var identityStorage)) + return; + + _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target); + + // We add a reference to ourselves to prevent repeated identity gain. + var targetDevoured = EnsureComp(target); + targetDevoured.DevouredBy.Add(ent.Owner); + Dirty(target, targetDevoured); + Dirty(ent); + } + + /// + /// Has the given victim been devoured by the given changeling before? + /// + public bool HasDevoured(Entity changeling, EntityUid devoured) + { + if (!Resolve(changeling, ref changeling.Comp, false)) + return false; + + return changeling.Comp.ConsumedIdentities.ContainsValue(devoured); + } + + /// + /// Can the given changeling devour the given victim? + /// + public bool CanDevour(Entity changeling, EntityUid victim, bool showPopup = true) + { + if (!Resolve(changeling, ref changeling.Comp)) + return false; + + if (changeling.Owner == victim) + return false; // Can't devour yourself. + + if (!HasComp(victim)) { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity"); - _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target.Value); + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-cannot-devour"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } - if (_inventorySystem.TryGetSlotEntity(target.Value, "jumpsuit", out var item) - && TryComp(item, out var butcherable)) - RipClothing(target.Value, (item.Value, butcherable)); + if (HasDevoured(changeling.Owner, victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-already-devoured"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; } - Dirty(ent); + if (!_mobState.IsDead(victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-not-dead"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + if (HasComp(victim)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + if (IsTargetProtected(victim, changeling!)) + { + if (showPopup) + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), changeling.Owner, changeling.Owner, PopupType.Medium); + return false; + } + + return true; } - private void RipClothing(EntityUid victim, Entity item) + /// + /// Checks if the target's outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. + /// + /// The Targeted entity + /// Changelings Devour Component + /// Is the target Protected from the attack + private bool IsTargetProtected(EntityUid target, Entity ent) { - var spawnEntities = EntitySpawnCollection.GetSpawns(item.Comp.SpawnedEntities, _robustRandom); + var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); + + RaiseLocalEvent(target, ev); - foreach (var proto in spawnEntities) + foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) { - // TODO: once predictedRandom is in, make this a Coordinate offset of 0.25f from the victims position - PredictedSpawnNextToOrDrop(proto, victim); + if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) + continue; + if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) + return true; } - PredictedQueueDel(item.Owner); + return false; } } diff --git a/Content.Shared/Changeling/Systems/ChangelingFleshClothingSystem.cs b/Content.Shared/Changeling/Systems/ChangelingFleshClothingSystem.cs new file mode 100644 index 00000000000..8082c3bb87b --- /dev/null +++ b/Content.Shared/Changeling/Systems/ChangelingFleshClothingSystem.cs @@ -0,0 +1,137 @@ +using Content.Shared.Alert; +using Content.Shared.Changeling.Components; +using Content.Shared.Clothing.Components; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Inventory; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling.Systems; + +public sealed class ChangelingFleshClothingSystem : EntitySystem +{ + [Dependency] private readonly SharedChameleonClothingSystem _chameleonClothing = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnToggle); + SubscribeLocalEvent(OnBeforeChangelingTransform); + SubscribeLocalEvent(OnAfterChangelingTransform); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _alerts.ShowAlert(ent.Owner, ent.Comp.AlertId, 1); + } + + private void OnToggle(Entity ent, ref ToggleFleshClothingEvent args) + { + if (args.Handled) + return; + + ent.Comp.Enabled = !ent.Comp.Enabled; + Dirty(ent); + _alerts.ShowAlert(ent.Owner, ent.Comp.AlertId, (short)(ent.Comp.Enabled ? 1 : 0)); + + args.Handled = true; + } + + // If we transform into another species and loose an inventory item then it will get dropped as a result. + // But we don't want fleeting clothing to do a sound and popup in that case, + // so we have to delete any flesh clothing slots that would drop before transforming. + private void OnBeforeChangelingTransform(Entity ent, ref BeforeChangelingTransformEvent args) + { + // We always remove slots that are no longer supported by the transformation, even if the component is disabled. + RemoveRedundantFleshClothing(ent.Owner, args.StoredIdentity); + } + + private void OnAfterChangelingTransform(Entity ent, ref AfterChangelingTransformEvent args) + { + if (ent.Comp.Enabled) + SpawnAndTransformClothing(ent.Owner, args.StoredIdentity, ent.Comp.ClothingPrototypes); + } + + /// + /// Removes any flesh clothing items the target is wearing in slots the original does not have. + /// + public void RemoveRedundantFleshClothing(Entity target, Entity original) + { + // TODO: Not predicted yet because equipping is not predicted either and we don't want to be nude for a few frames. + if (_net.IsClient) + return; + + if (!Resolve(target, ref target.Comp)) + return; + + Resolve(original, ref original.Comp, false); // They might now have an inventory at all. + + var slots = _inventory.GetSlotEnumerator(target, SlotFlags.WITHOUT_POCKET); + while (slots.NextItem(out var targetItem, out var slotDefinition)) + { + if (!HasComp(targetItem)) + continue; // Do nothing for normal items. + + // If the original does not have that slot or it contains an invalid prototype then we remove any flesh clothing item in our corresponding slot. + if (original.Comp == null + || !_inventory.TryGetSlotEntity(original, slotDefinition.Name, out var originalItem, inventoryComponent: original.Comp) + || MetaData(originalItem.Value).EntityPrototype?.ID == null) + { + _inventory.TryUnequip(target, slotDefinition.Name, silent: true, force: true, inventory: target.Comp); + QueueDel(targetItem); + } + } + } + + /// + /// Spawns the given clothing items into empty inventory slots. + /// If the item has and then + /// it will have its visuals changed to match the item another given player is wearing in the same slot. + /// + public void SpawnAndTransformClothing(Entity target, Entity original, Dictionary clothingPrototypes) + { + // TODO: Remove this guard when chameleon clothing is properly predicted. + // Otherwise we will see the default item sprite for a moment after spawn until it's updated. + if (_net.IsClient) + return; + + if (!Resolve(target, ref target.Comp) || !Resolve(original, ref original.Comp, false)) // Don't log because the original might be outside PVS range since it's on another map. + return; + + var slots = _inventory.GetSlotEnumerator(target, SlotFlags.WITHOUT_POCKET); + var coords = Transform(target).Coordinates; + while (slots.MoveNext(out var containerSlot, out var slotDefinition)) + { + var targetItem = containerSlot.ContainedEntity; + if (!_inventory.TryGetSlotEntity(original, slotDefinition.Name, out var originalItem, inventoryComponent: original.Comp) + || MetaData(originalItem.Value).EntityPrototype?.ID is not { } chameleonProtoId) + continue; + + // If our slot is empty then spawn a new chameleon clothing item inside. + if (targetItem == null && clothingPrototypes.TryGetValue(slotDefinition.Name, out var fleshProtoId)) + { + targetItem = SpawnAtPosition(fleshProtoId, coords); + _inventory.TryEquip(target, targetItem.Value, slotDefinition.Name, silent: true, force: true, inventory: target.Comp); + } + + // If the item in our slot is flesh clothing then set the chameleon prototype to mirror the target. + if (HasComp(targetItem)) + { + if (TryComp(originalItem, out var originalChameleonComp)) + _chameleonClothing.SetSelectedPrototype(targetItem.Value, originalChameleonComp.Default, validate: false); // If it is also a chameleon item then use whatever that is mimicing. + else + _chameleonClothing.SetSelectedPrototype(targetItem.Value, chameleonProtoId, validate: false); // We don't validate because a lot of clothing is not chameleon whitelisted, like warden's beret. + } + } + } +} + +/// +/// Event raised to toggle the . +/// +public sealed partial class ToggleFleshClothingEvent : BaseAlertEvent; diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs index e5551473528..50f5ceb5470 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.UI.cs @@ -3,13 +3,25 @@ namespace Content.Shared.Changeling.Systems; /// -/// Send when a player selects an intentity to transform into in the radial menu. +/// Send when a player selects an identity to transform into in the radial menu. /// [Serializable, NetSerializable] public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage { /// - /// The uid of the cloned identity. + /// The uid of the stored identity. + /// + public readonly NetEntity TargetIdentity = targetIdentity; +} + +/// +/// Send when a player selects an identity to drop from their storage. +/// +[Serializable, NetSerializable] +public sealed class ChangelingTransformIdentityDropMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage +{ + /// + /// The uid of the stored identity. /// public readonly NetEntity TargetIdentity = targetIdentity; } diff --git a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs index cf8d9d7cb6e..8ec2d36cd86 100644 --- a/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs +++ b/Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs @@ -1,13 +1,15 @@ using Content.Shared.Actions; using Content.Shared.Administration.Logs; +using Content.Shared.Body; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; using Content.Shared.Database; using Content.Shared.DoAfter; -using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Popups; +using Content.Shared.Storage; using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Prototypes; @@ -16,16 +18,19 @@ namespace Content.Shared.Changeling.Systems; public sealed partial class ChangelingTransformSystem : EntitySystem { [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearanceSystem = default!; - [Dependency] private readonly MetaDataSystem _metaSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; + [Dependency] private readonly SharedCloningSystem _cloning = default!; + [Dependency] private readonly SharedVisualBodySystem _visualBody = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly IdentitySystem _identity = default!; + [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!; private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; public override void Initialize() @@ -34,24 +39,28 @@ public override void Initialize() SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnTransformAction); - SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnTransformSelected); + SubscribeLocalEvent(OnTransformDrop); + SubscribeLocalEvent(OnSuccessfulTransform); SubscribeLocalEvent(OnShutdown); + + // Components that need special handling outside of cloning. + SubscribeLocalEvent(StorageBeforeTransform); } private void OnMapInit(Entity ent, ref MapInitEvent init) { - _actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); + _actions.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); var userInterfaceComp = EnsureComp(ent); - _uiSystem.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); + _ui.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.ChangelingTransformActionEntity != null) { - _actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); + _actions.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); } } @@ -64,17 +73,54 @@ private void OnTransformAction(Entity ent, if (!TryComp(ent, out var userIdentity)) return; - if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer)) + if (!_ui.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer)) { - _uiSystem.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer); + _ui.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer); } //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch, // issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI) // but pressing the number does. } + private void OnTransformSelected(Entity ent, + ref ChangelingTransformIdentitySelectMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't transform into ourselves + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + TransformInto(ent.AsNullable(), targetIdentity.Value); + } + + private void OnTransformDrop(Entity ent, + ref ChangelingTransformIdentityDropMessage args) + { + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't drop our current identity + + if (!identity.ConsumedIdentities.ContainsKey(targetIdentity.Value)) + return; // this identity does not belong to this player + + _popup.PopupClient(Loc.GetString("changeling-transform-bui-drop-identity-entity-popup", ("entity", targetIdentity.Value)), ent.Owner, PopupType.Large); + _changelingIdentity.DropStoredIdentity(ent.Owner, targetIdentity.Value); + } + /// /// Transform the changeling into another identity. - /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentiyComponent, + /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentityComponent, /// so make sure to validate the target before. /// public void TransformInto(Entity ent, EntityUid targetIdentity) @@ -84,7 +130,7 @@ public void TransformInto(Entity ent, EntityUid t var selfMessage = Loc.GetString("changeling-transform-attempt-self", ("user", Identity.Entity(ent.Owner, EntityManager))); var othersMessage = Loc.GetString("changeling-transform-attempt-others", ("user", Identity.Entity(ent.Owner, EntityManager))); - _popupSystem.PopupPredicted( + _popup.PopupPredicted( selfMessage, othersMessage, ent, @@ -92,14 +138,17 @@ public void TransformInto(Entity ent, EntityUid t PopupType.MediumCaution); if (_net.IsServer) + { + ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); // cancel any previous sounds first ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity; + } if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) "); else _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\""); - _doAfterSystem.TryStartDoAfter(new DoAfterArgs( + _doAfter.TryStartDoAfter(new DoAfterArgs( EntityManager, ent, ent.Comp.TransformWindup, @@ -115,33 +164,11 @@ public void TransformInto(Entity ent, EntityUid t }); } - private void OnTransformSelected(Entity ent, - ref ChangelingTransformIdentitySelectMessage args) - { - _uiSystem.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent); - - if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) - return; - - if (!TryComp(ent, out var identity)) - return; - - if (identity.CurrentIdentity == targetIdentity) - return; // don't transform into ourselves - - if (!identity.ConsumedIdentities.Contains(targetIdentity.Value)) - return; // this identity does not belong to this player - - TransformInto(ent.AsNullable(), targetIdentity.Value); - } - private void OnSuccessfulTransform(Entity ent, ref ChangelingTransformDoAfterEvent args) { args.Handled = true; - - if (EntityManager.EntityExists(ent.Comp.CurrentTransformSound)) - _audio.Stop(ent.Comp.CurrentTransformSound); + ent.Comp.CurrentTransformSound = _audio.Stop(ent.Comp.CurrentTransformSound); if (args.Cancelled) return; @@ -152,14 +179,19 @@ private void OnSuccessfulTransform(Entity ent, if (args.Target is not { } targetIdentity) return; - _humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User); - _cloningSystem.CloneComponents(targetIdentity, args.User, settings); + var beforeTransformEvent = new BeforeChangelingTransformEvent(targetIdentity); + RaiseLocalEvent(args.User, beforeTransformEvent); + + _visualBody.CopyAppearanceFrom(targetIdentity, args.User); + _cloning.CloneComponents(targetIdentity, args.User, settings); if (TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})"); else _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\""); - _metaSystem.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); + + _metaData.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); // Don't raise events because we don't want to rename the ID card. + _identity.QueueIdentityUpdate(ent); // We have to manually refresh the identity because we did not raise events. Dirty(ent); @@ -168,5 +200,17 @@ private void OnSuccessfulTransform(Entity ent, identity.CurrentIdentity = targetIdentity; Dirty(ent.Owner, identity); } + + var afterTransformEvent = new AfterChangelingTransformEvent(targetIdentity); + RaiseLocalEvent(args.User, afterTransformEvent); + } + + private void StorageBeforeTransform(Entity ent, ref BeforeChangelingTransformEvent args) + { + if (HasComp(args.StoredIdentity)) + return; // If we have a storage component and the target has one as well, then do nothing. + + // If the target identity does not have a storage anymore, drop all items inside our storage so that they don't become unreachable. + _container.EmptyContainer(ent.Comp.Container); } } diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index e7e46d79a1f..8e28cd48ad3 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -1,8 +1,6 @@ -using System.Numerics; +using System.Linq; using Content.Shared.Changeling.Components; using Content.Shared.Cloning; -using Content.Shared.Humanoid; -using Content.Shared.NameModifier.EntitySystems; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Network; @@ -14,16 +12,12 @@ namespace Content.Shared.Changeling.Systems; public abstract class SharedChangelingIdentitySystem : EntitySystem { [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; - [Dependency] private readonly NameModifierSystem _nameMod = default!; [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; - [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!; public MapId? PausedMapId; - private int _numberOfStoredIdentities = 0; // TODO: remove this public override void Initialize() { @@ -34,6 +28,8 @@ public override void Initialize() SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnStoredRemove); + + SubscribeLocalEvent(OnDevouredShutdown); } private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) @@ -57,7 +53,32 @@ private void OnShutdown(Entity ent, ref ComponentSh { if (TryComp(ent, out var actor)) CleanupPvsOverride(ent, actor.PlayerSession); + CleanupChangelingNullspaceIdentities(ent); + CleanupDevouredReferences(ent); + } + + // Set all references to this entity to null to prevent PVS errors when networking. + private void OnDevouredShutdown(Entity ent, ref ComponentShutdown args) + { + foreach (var ling in ent.Comp.DevouredBy) + { + if (!TryComp(ling, out var identityComp)) + continue; + + var keysToUpdate = identityComp.ConsumedIdentities + .Where(kvp => kvp.Value == ent.Owner) + .Select(kvp => kvp.Key) + .ToList(); + + if (keysToUpdate.Count == 0) + continue; // No need to dirty. + + foreach (var key in keysToUpdate) + identityComp.ConsumedIdentities[key] = null; + + Dirty(ling, identityComp); + } } private void OnStoredRemove(Entity ent, ref ComponentRemove args) @@ -78,53 +99,92 @@ public void CleanupChangelingNullspaceIdentities(Entity - /// Clone a target humanoid into nullspace and add it to the Changelings list of identities. - /// It creates a perfect copy of the target and can be used to pull components down for future use + /// Removes all references to the owning changeling from ChangelingDevouredComponents. /// - /// the Changeling - /// the targets uid - public EntityUid? CloneToPausedMap(Entity ent, EntityUid target) + /// The changeling entity + private void CleanupDevouredReferences(Entity ent) + { + foreach (var devouredUid in ent.Comp.ConsumedIdentities.Values) + { + if (!TryComp(devouredUid, out var devouredComp)) + continue; + + if (devouredComp.DevouredBy.Remove(ent.Owner)) + Dirty(devouredUid.Value, devouredComp); + } + } + + /// + /// Clone a target humanoid to a paused map. + /// It creates a perfect copy of the target and can be used to pull components down for future use. + /// + /// The settings to use for cloning. + /// The target to clone. + public EntityUid? CloneToPausedMap(ProtoId settings, EntityUid target) { // Don't create client side duplicate clones or a clientside map. if (_net.IsClient) return null; - if (!TryComp(target, out var humanoid) - || !_prototype.Resolve(humanoid.Species, out var speciesPrototype) - || !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings)) + EnsurePausedMap(); + if (PausedMapId == null) return null; - EnsurePausedMap(); - // TODO: Setting the spawn location is a shitty bandaid to prevent admins from crashing our servers. - // Movercontrollers and mob collisions are currently being calculated even for paused entities. - // Spawning all of them in the same spot causes severe performance problems. - // Cryopods and Polymorph have the same problem. - var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value)); + var mapCoords = new MapCoordinates(0, 0, PausedMapId.Value); + if (!_cloningSystem.TryCloning(target, mapCoords, settings, out var clone)) + return null; - var storedIdentity = EnsureComp(clone); - storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed + var storedIdentity = EnsureComp(clone.Value); + storedIdentity.OriginalEntity = target; // TODO: network this once we have a relations system so that this does not cause PVS errors. if (TryComp(target, out var actor)) storedIdentity.OriginalSession = actor.PlayerSession; - _humanoidSystem.CloneAppearance(target, clone); - _cloningSystem.CloneComponents(target, clone, settings); + return clone; + } + + /// + /// Clone a target humanoid to a paused map and add it to the Changelings list of identities. + /// It creates a perfect copy of the target and can be used to pull components down for future use. + /// + /// The Changeling. + /// The target to clone. + public EntityUid? CloneToPausedMap(Entity ent, EntityUid target) + { + var clone = CloneToPausedMap(ent.Comp.IdentityCloningSettings, target); + + if (clone == null) + return null; - var targetName = _nameMod.GetBaseName(target); - _metaSystem.SetEntityName(clone, targetName); - ent.Comp.ConsumedIdentities.Add(clone); + ent.Comp.ConsumedIdentities.Add(clone.Value, target); Dirty(ent); - HandlePvsOverride(ent, clone); + HandlePvsOverride(ent, clone.Value); return clone; } + /// + /// Drop a stored identity from the changeling's storage. + /// + public void DropStoredIdentity(Entity ent, EntityUid identity) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!HasComp(identity)) + return; // Not a stored identity. + + PredictedQueueDel(identity); + if (ent.Comp.ConsumedIdentities.Remove(identity)) + Dirty(ent); + } + /// /// Simple helper to add a PVS override to a nullspace identity. /// @@ -142,12 +202,12 @@ private void HandlePvsOverride(EntityUid uid, EntityUid identity) /// Cleanup all PVS overrides for the owner of the ChangelingIdentity /// /// The changeling storing the identities. - /// + /// The session you wish to remove the overrides from. private void CleanupPvsOverride(Entity ent, ICommonSession session) { foreach (var identity in ent.Comp.ConsumedIdentities) { - _pvsOverrideSystem.RemoveSessionOverride(identity, session); + _pvsOverrideSystem.RemoveSessionOverride(identity.Key, session); } } @@ -160,7 +220,7 @@ public void HandOverPvsOverride(Entity ent, ICommon { foreach (var identity in ent.Comp.ConsumedIdentities) { - _pvsOverrideSystem.AddSessionOverride(identity, session); + _pvsOverrideSystem.AddSessionOverride(identity.Key, session); } } diff --git a/Content.Shared/Charges/Systems/SharedChargesSystem.cs b/Content.Shared/Charges/Systems/SharedChargesSystem.cs index 504648c41d6..a48fbaaa8d1 100644 --- a/Content.Shared/Charges/Systems/SharedChargesSystem.cs +++ b/Content.Shared/Charges/Systems/SharedChargesSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions.Events; using Content.Shared.Charges.Components; using Content.Shared.Examine; +using Content.Shared.Rejuvenate; using JetBrains.Annotations; using Robust.Shared.Timing; @@ -19,7 +20,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnExamine); - + SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnChargesAttempt); SubscribeLocalEvent(OnChargesMapInit); SubscribeLocalEvent(OnChargesPerformed); @@ -48,6 +49,11 @@ private void OnExamine(EntityUid uid, LimitedChargesComponent comp, ExaminedEven args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1")))); } + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + ResetCharges(ent.AsNullable()); + } + private void OnChargesAttempt(Entity ent, ref ActionAttemptEvent args) { if (args.Cancelled) diff --git a/Content.Shared/Chat/EmotesEvents.cs b/Content.Shared/Chat/EmotesEvents.cs index 4479f8b2ab9..196bbb43bda 100644 --- a/Content.Shared/Chat/EmotesEvents.cs +++ b/Content.Shared/Chat/EmotesEvents.cs @@ -1,9 +1,49 @@ -using Content.Shared.Chat.Prototypes; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Inventory; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Chat; +/// +/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance. +/// +[ByRefEvent] +public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote) + : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public readonly EntityUid Source = source; + public readonly EmotePrototype Emote = emote; + + /// + /// The equipment that is blocking emoting. Should only be non-null if the event was canceled. + /// + public EntityUid? Blocker = null; + + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// Raised by the chat system when an entity made some emote. +/// Use it to play sound, change sprite or something else. +/// +[ByRefEvent] +public record struct EmoteEvent(EmotePrototype Emote) +{ + /// + /// The used emote. + /// + public EmotePrototype Emote = Emote; + + /// + /// If this message has already been "handled" by a previous system. + /// + public bool Handled; +} + +/// +/// Sent by the client when requesting the server to play a specific emote selected from the emote radial menu. +/// [Serializable, NetSerializable] public sealed class PlayEmoteMessage(ProtoId protoId) : EntityEventArgs { diff --git a/Content.Shared/Chat/ISharedChatManager.cs b/Content.Shared/Chat/ISharedChatManager.cs index 39c1d85dd25..76fb4fbea85 100644 --- a/Content.Shared/Chat/ISharedChatManager.cs +++ b/Content.Shared/Chat/ISharedChatManager.cs @@ -3,6 +3,28 @@ namespace Content.Shared.Chat; public interface ISharedChatManager { void Initialize(); + + /// + /// Send an admin alert to the admin chat channel. + /// + /// The message to send. void SendAdminAlert(string message); + + /// + /// Send an admin alert to the admin chat channel specifically about the given player. + /// Will include info extra like their antag status and name. + /// + /// The player that the message is about. + /// The message to send. void SendAdminAlert(EntityUid player, string message); + + /// + /// This is a dangerous function! Only pass in property escaped text. + /// See: + ///

+ /// Use this for things that need to be unformatted (like tpto links) but ensure that everything else + /// is formated properly. If it's not, players could sneak in ban links or other nasty commands that the admins + /// could clink on. + ///
+ void SendAdminAlertNoFormatOrEscape(string message); } diff --git a/Content.Shared/Chat/InGameOocMessageAttemptEvent.cs b/Content.Shared/Chat/InGameOocMessageAttemptEvent.cs new file mode 100644 index 00000000000..926b4046b89 --- /dev/null +++ b/Content.Shared/Chat/InGameOocMessageAttemptEvent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Player; + +namespace Content.Shared.Chat; + +/// +/// Event fired before a player's entity speaks on LOOC or Deadchat. +/// +[ByRefEvent] +public record struct InGameOocMessageAttemptEvent(ICommonSession Session, InGameOOCChatType Type, bool Cancelled = false); diff --git a/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs index e6fdae4cf1b..a3d6ccce7fd 100644 --- a/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs +++ b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs @@ -14,30 +14,44 @@ public sealed partial class AutoEmotePrototype : IPrototype /// The ID of the emote prototype. ///
[DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string EmoteId = String.Empty; + public string EmoteId = string.Empty; /// /// How often an attempt at the emote will be made. /// - [DataField("interval", required: true)] + [DataField(required: true)] public TimeSpan Interval; /// /// Probability of performing the emote each interval. - /// + /// [DataField("chance")] public float Chance = 1; /// /// Also send the emote in chat. - /// - [DataField("withChat")] + /// + [DataField] public bool WithChat = true; /// - /// Hide the chat message from the chat window, only showing the popup. + /// Should we ignore action blockers? + /// This does nothing if WithChat is false. + /// + [DataField] + public bool IgnoreActionBlocker; + + /// + /// Should we ignore whitelists and force the emote? /// This does nothing if WithChat is false. + /// + [DataField] + public bool Force; + /// - [DataField("hiddenFromChatWindow")] - public bool HiddenFromChatWindow = false; + /// Hide the chat message from the chat window, only showing the popup. + /// This does nothing if WithChat is false. + /// + [DataField] + public bool HiddenFromChatWindow; } diff --git a/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs b/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs index 58315161a56..43a79e133ef 100644 --- a/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs +++ b/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs @@ -6,7 +6,7 @@ namespace Content.Shared.Chat.Prototypes; /// /// A predefined notification used to warn a player of specific events. /// -[Prototype("chatNotification")] +[Prototype] public sealed partial class ChatNotificationPrototype : IPrototype { [ViewVariables] diff --git a/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs b/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs index 0138009ef9c..b75ed796e2e 100644 --- a/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs +++ b/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs @@ -21,7 +21,7 @@ public sealed partial class EmoteSoundsPrototype : IPrototype, IInheritingProtot /// [AbstractDataField] [NeverPushInheritance] - public bool Abstract { get; } + public bool Abstract { get; private set; } /// /// Optional fallback sound that will play if collection diff --git a/Content.Shared/Chat/SharedChatEvents.cs b/Content.Shared/Chat/SharedChatEvents.cs index c187fd59a8d..1f436287921 100644 --- a/Content.Shared/Chat/SharedChatEvents.cs +++ b/Content.Shared/Chat/SharedChatEvents.cs @@ -1,12 +1,13 @@ +using Content.Shared.Inventory; +using Content.Shared.Radio; using Content.Shared.Speech; using Robust.Shared.Prototypes; -using Content.Shared.Inventory; namespace Content.Shared.Chat; /// -/// This event should be sent everytime an entity talks (Radio, local chat, etc...). -/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). +/// This event should be sent everytime an entity talks (Radio, local chat, etc...). +/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks). /// public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent { @@ -22,3 +23,76 @@ public TransformSpeakerNameEvent(EntityUid sender, string name) SpeechVerb = null; } } + +/// +/// Raised broadcast in order to transform speech.transmit +/// +public sealed class TransformSpeechEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public EntityUid Sender; + public string Message; + + public TransformSpeechEvent(EntityUid sender, string message) + { + Sender = sender; + Message = message; + } +} + +public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs +{ + public EntityUid Sender; + public bool IgnoreBlocker; + + public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker) + { + Sender = sender; + IgnoreBlocker = ignoreBlocker; + } +} + +/// +/// Raised on an entity when it speaks, either through 'say' or 'whisper'. +/// +public sealed class EntitySpokeEvent : EntityEventArgs +{ + public readonly EntityUid Source; + public readonly string Message; + public readonly string? ObfuscatedMessage; // not null if this was a whisper + + /// + /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio + /// message gets sent on this channel, this should be set to null to prevent duplicate messages. + /// + public RadioChannelPrototype? Channel; + + // Imperial Medieval languages + public readonly Content.Shared.Imperial.Medieval.Language.LanguagePrototype? Language; + public readonly bool Whisper; + public readonly bool CheckNrp; + + public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) + : this(source, message, null, channel, obfuscatedMessage) + { + } + + // Imperial Medieval language-aware spoke event + public EntitySpokeEvent( + EntityUid source, + string message, + Content.Shared.Imperial.Medieval.Language.LanguagePrototype? language, + RadioChannelPrototype? channel, + string? obfuscatedMessage, + bool whisper = false, + bool checkNrp = true) + { + Source = source; + Message = message; + Channel = channel; + ObfuscatedMessage = obfuscatedMessage; + Language = language; + Whisper = whisper; + CheckNrp = checkNrp; + } +} diff --git a/Content.Shared/Chat/SharedChatSystem.Emote.cs b/Content.Shared/Chat/SharedChatSystem.Emote.cs new file mode 100644 index 00000000000..175474a2c7a --- /dev/null +++ b/Content.Shared/Chat/SharedChatSystem.Emote.cs @@ -0,0 +1,317 @@ +using System.Collections.Frozen; +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Humanoid; +using Content.Shared.Speech; +using Robust.Shared.Audio; +using Robust.Shared.Random; + +namespace Content.Shared.Chat; + +public abstract partial class SharedChatSystem +{ + private FrozenDictionary _wordEmoteDict = FrozenDictionary.Empty; + + private void CacheEmotes() + { + var dict = new Dictionary(); + var emotes = _prototypeManager.EnumeratePrototypes(); + foreach (var emote in emotes) + { + foreach (var word in emote.ChatTriggers) + { + var lowerWord = word.ToLower(); + if (dict.TryGetValue(lowerWord, out var value)) + { + var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {value.ID}"; + Log.Error(errMsg); + continue; + } + + dict.Add(lowerWord, emote); + } + } + + _wordEmoteDict = dict.ToFrozenDictionary(); + } + + /// + /// Makes the selected entity emote using the given and sends a message to chat. + /// + /// The entity that is speaking + /// The id of emote prototype. Should have valid + /// Whether this message should appear in the adminlog window, or not. + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... + /// Whether emote action blocking should be ignored or not. + /// + /// The name to use for the speaking entity. Usually this should just be modified via . + /// If this is set, the event will not get raised. + /// + /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. + public bool TryEmoteWithChat( + EntityUid source, + string emoteId, + ChatTransmitRange range = ChatTransmitRange.Normal, + bool hideLog = false, + string? nameOverride = null, + bool ignoreActionBlocker = false, + bool forceEmote = false + ) + { + if (!_prototypeManager.Resolve(emoteId, out var proto)) + return false; + + return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote); + } + + /// + /// Makes the selected entity emote using the given and sends a message to chat. + /// + /// The entity that is speaking. + /// The emote prototype. Should have valid . + /// Whether this message should appear in the adminlog window or not. + /// Whether emote action blocking should be ignored or not. + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... + /// + /// The name to use for the speaking entity. Usually this should just be modified via . + /// If this is set, the event will not get raised. + /// + /// Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. + public bool TryEmoteWithChat( + EntityUid source, + EmotePrototype emote, + ChatTransmitRange range = ChatTransmitRange.Normal, + bool hideLog = false, + string? nameOverride = null, + bool ignoreActionBlocker = false, + bool forceEmote = false + ) + { + if (!forceEmote && !AllowedToUseEmote(source, emote)) + return false; + + var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker); + + // check if proto has valid message for chat + if (didEmote && emote.ChatMessages.Count != 0) + { + // not all emotes are loc'd, but for the ones that are we pass in entity + var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source)); + SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); + } + + return didEmote; + } + + /// + /// Makes the selected entity emote using the given without sending any messages to chat. + /// + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. + public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false) + { + if (!_prototypeManager.Resolve(emoteId, out var proto)) + return false; + + return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker); + } + + /// + /// Makes the selected entity emote using the given without sending any messages to chat. + /// + /// True if an emote was performed. False if the emote is unavailable, cancelled, etc. + public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false) + { + if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker) + return false; + + return TryInvokeEmoteEvent(uid, proto); + } + + /// + /// Tries to find and play the relevant emote sound in an emote sounds collection. + /// + /// True if emote sound was played. + public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null) + { + return TryPlayEmoteSound(uid, proto, emote.ID, audioParams); + } + + /// + /// Tries to find and play the relevant emote sound in an emote sounds collection. + /// + /// True if emote sound was played. + public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null) + { + if (proto == null) + return false; + + // try to get specific sound for this emote + if (!proto.Sounds.TryGetValue(emoteId, out var sound)) + { + // no specific sound - check fallback + sound = proto.FallbackSound; + if (sound == null) + return false; + } + + // optional override params > general params for all sounds in set > individual sound params + var param = audioParams ?? proto.GeneralParams ?? sound.Params; + // Imperial Medieval AgePitch Begin + if (TryComp(uid, out var comp)) + { + float age = comp.Age; + + // Базовые возрастные группы и их характерные pitch значения + if (age < 14f) + { + // Дети: высокий голос, но не писклявый + float childFactor = (14f - age) / 14f; // от 0 до 1 + param.Pitch += 0.3f + childFactor * 0.4f; // +0.3 до +0.7 + } + else if (age < 25f) + { + // Подростки и молодые взрослые: естественный высокий голос + float youthFactor = (25f - age) / 11f; // от 0 до 1 + param.Pitch += 0.1f + youthFactor * 0.2f; // +0.1 до +0.3 + } + else if (age < 50f) + { + // Взрослые: нейтральный голос с легкими вариациями + float adultFactor = (age - 25f) / 25f; // от 0 до 1 + param.Pitch += 0f - adultFactor * 0.1f; // 0 до -0.1 + } + else + { + // Пожилые: более низкий голос, но не глухой + float seniorFactor = (age - 50f) / 50f; // от 0 до 1 (ограничиваем эффект) + float pitchReduction = seniorFactor * 0.25f; // максимум -0.25 + param.Pitch -= Math.Min(pitchReduction, 0.25f); + } + + // Добавляем небольшую случайную вариацию для естественности (±5%) + param.Pitch *= 1f + (_random.NextFloat() - 0.5f) * 0.1f; + + // Ограничиваем общий диапазон разумными пределами + param.Pitch = Math.Clamp(param.Pitch, 0.7f, 1.8f); + } + // Imperial Medieval AgePitch End + _audio.PlayPvs(sound, uid, param); + return true; + } + /// + /// Checks if a valid emote was typed, to play sounds and etc and invokes an event. + /// + /// The entity that is speaking + /// Formatted emote message. + /// True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be. + protected bool TryEmoteChatInput(EntityUid source, string textInput) + { + var actionTrimmedLower = TrimPunctuation(textInput.ToLower()); + if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote)) + return true; + + if (!AllowedToUseEmote(source, emote)) + return true; + + return TryInvokeEmoteEvent(source, emote); + + } + /// + /// Checks if we can use this emote based on the emotes whitelist, blacklist, and availability to the entity. + /// + /// The entity that is speaking + /// The emote being used + public bool AllowedToUseEmote(EntityUid source, EmotePrototype emote) + { + // If emote is in AllowedEmotes, it will bypass whitelist and blacklist + if (TryComp(source, out var speech) && + speech.AllowedEmotes.Contains(emote.ID)) + { + return true; + } + + // Check the whitelist and blacklist + if (_whitelist.IsWhitelistFail(emote.Whitelist, source) || + _whitelist.IsWhitelistPass(emote.Blacklist, source)) + { + return false; + } + + // Check if the emote is available for all + if (!emote.Available) + { + return false; + } + + return true; + } + + /// + /// Creates and raises and then to let other systems do things like play audio. + /// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player + /// explaining why the emote didn't happen. + /// + /// The entity which is emoting + /// The emote which is being performed + /// True if the emote was performed, false otherwise. + private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto) + { + var beforeEv = new BeforeEmoteEvent(uid, proto); + RaiseLocalEvent(uid, ref beforeEv); + + if (beforeEv.Cancelled) + { + // Chat is not predicted anyways, so no need to predict this popup either. + if (_net.IsClient) + return false; + + if (beforeEv.Blocker != null) + { + _popup.PopupEntity( + Loc.GetString( + "chat-system-emote-cancelled-blocked", + ("emote", Loc.GetString(proto.Name).ToLower()), + ("blocker", beforeEv.Blocker.Value) + ), + uid, + uid + ); + } + else + { + _popup.PopupEntity( + Loc.GetString("chat-system-emote-cancelled-generic", + ("emote", Loc.GetString(proto.Name).ToLower())), + uid, + uid + ); + } + + return false; + } + + var ev = new EmoteEvent(proto); + RaiseLocalEvent(uid, ref ev); + + return true; + } + + private string TrimPunctuation(string textInput) + { + var trimEnd = textInput.Length; + while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1])) + { + trimEnd--; + } + + var trimStart = 0; + while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart])) + { + trimStart++; + } + + return textInput[trimStart..trimEnd]; + } +} diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index ce91a38116d..36d9df91299 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,15 +1,23 @@ using System.Collections.Frozen; using System.Text.RegularExpressions; +using Content.Shared.ActionBlocker; +using Content.Shared.Chat.Prototypes; using Content.Shared.Popups; using Content.Shared.Radio; using Content.Shared.Speech; +using Content.Shared.Whitelist; using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Console; +using Robust.Shared.Network; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Shared.Chat; -public abstract class SharedChatSystem : EntitySystem +public abstract partial class SharedChatSystem : EntitySystem { public const char RadioCommonPrefix = ';'; public const char RadioChannelPrefix = ':'; @@ -29,9 +37,9 @@ public abstract class SharedChatSystem : EntitySystem 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 public static readonly SoundSpecifier DefaultAnnouncementSound - = new SoundPathSpecifier("/Audio/Imperial/Medieval/horn.ogg"); // Corvax-Announcements + = new SoundPathSpecifier("/Audio/Corvax/Announcements/announce.ogg"); // Imperial Corvax-Announcements public static readonly SoundPathSpecifier CentComAnnouncementSound - = new SoundPathSpecifier("/Audio/Corvax/Announcements/centcomm.ogg"); // Corvax-Announcements + = new SoundPathSpecifier("/Audio/Corvax/Announcements/centcomm.ogg"); // Imperial Corvax-Announcements public static readonly ProtoId CommonChannel = "Common"; @@ -40,6 +48,11 @@ public static readonly SoundPathSpecifier CentComAnnouncementSound [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly INetManager _net = default!; /// /// Cache of the keycodes for faster lookup. @@ -49,15 +62,21 @@ public static readonly SoundPathSpecifier CentComAnnouncementSound public override void Initialize() { base.Initialize(); + DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel)); + SubscribeLocalEvent(OnPrototypeReload); CacheRadios(); + CacheEmotes(); } protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj) { if (obj.WasModified()) CacheRadios(); + + if (obj.WasModified()) + CacheEmotes(); } private void CacheRadios() @@ -129,7 +148,7 @@ public void GetRadioKeycodePrefix(EntityUid source, /// The channel that was requested, if any /// Whether or not to generate an informative pop-up message. /// - public bool TryProccessRadioMessage( + public bool TryProcessRadioMessage( EntityUid source, string input, out string output, @@ -281,7 +300,10 @@ public static string InjectTagInsideTag(ChatMessage message, string outerTag, st public static string InjectTagAroundString(ChatMessage message, string targetString, string tag, string? tagParameter) { var rawmsg = message.WrappedMessage; + // TODO: Figure out if there's any way we can cache this, and if not then rewrite this to not use regex. +#pragma warning disable RA0026 rawmsg = Regex.Replace(rawmsg, "(?i)(" + targetString + ")(?-i)(?![^[]*])", $"[{tag}={tagParameter}]$1[/{tag}]"); +#pragma warning restore RA0026 return rawmsg; } @@ -295,12 +317,152 @@ public static string GetStringInsideTag(ChatMessage message, string tag) tagStart += tag.Length + 2; return rawmsg.Substring(tagStart, tagEnd - tagStart); } + + protected virtual void SendEntityEmote( + EntityUid source, + string action, + ChatTransmitRange range, + string? nameOverride, + bool hideLog = false, + bool checkEmote = true, + bool ignoreActionBlocker = false, + NetUserId? author = null + ) + { } + + /// + /// Sends an in-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Whether or not this message should appear in the chat window. + /// Whether or not this message should appear in the adminlog window. + /// + /// The player doing the speaking. + /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// Whether or not should be parsed with consideration of radio channel prefix text at start the start. + /// If set to true, action blocker will not be considered for whether an entity can send this message. + public virtual void TrySendInGameICMessage( + EntityUid source, + string message, + InGameICChatType desiredType, + bool hideChat, + bool hideLog = false, + IConsoleShell? shell = null, + ICommonSession? player = null, + string? nameOverride = null, + bool checkRadioPrefix = true, + bool ignoreActionBlocker = false, + Color? color = null, // Imperial Medieval Magic + Content.Shared.Imperial.Medieval.Language.LanguagePrototype? language = null) // Imperial Medieval Languages + { } + + /// + /// Sends an in-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... + /// Disables the admin log for this message if true. Used for entities that are not players, like vendors, cloning, etc. + /// + /// The player doing the speaking. + /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + /// If set to true, action blocker will not be considered for whether an entity can send this message. + public virtual void TrySendInGameICMessage( + EntityUid source, + string message, + InGameICChatType desiredType, + ChatTransmitRange range, + bool hideLog = false, + IConsoleShell? shell = null, + ICommonSession? player = null, + string? nameOverride = null, + bool checkRadioPrefix = true, + bool ignoreActionBlocker = false, + Color? color = null, // Imperial Medieval Magic + Content.Shared.Imperial.Medieval.Language.LanguagePrototype? language = null, // Imperial Medieval Languages + bool checkNrp = true) // Imperial Medieval NRP + { } + + /// + /// Sends an out-of-character chat message to relevant clients. + /// + /// The entity that is speaking. + /// The message being spoken or emoted. + /// The chat type. + /// Whether or not to show the message in the chat window. + /// + /// The player doing the speaking. + public virtual void TrySendInGameOOCMessage( + EntityUid source, + string message, + InGameOOCChatType type, + bool hideChat, + IConsoleShell? shell = null, + ICommonSession? player = null + ) + { } + + /// + /// Dispatches an announcement to all. + /// + /// The contents of the message. + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchGlobalAnnouncement( + string message, + string? sender = null, + bool playSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null + ) + { } + + /// + /// Dispatches an announcement to players selected by filter. + /// + /// Filter to select players who will recieve the announcement. + /// The contents of the message. + /// The entity making the announcement (used to determine the station). + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchFilteredAnnouncement( + Filter filter, + string message, + EntityUid? source = null, + string? sender = null, + bool playSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null) + { } + + /// + /// Dispatches an announcement on a specific station. + /// + /// The entity making the announcement (used to determine the station). + /// The contents of the message. + /// The sender (Communications Console in Communications Console Announcement). + /// Play the announcement sound. + /// Sound to play. + /// Optional color for the announcement message. + public virtual void DispatchStationAnnouncement( + EntityUid source, + string message, + string? sender = null, + bool playDefaultSound = true, + SoundSpecifier? announcementSound = null, + Color? colorOverride = null) + { } } -// imperial medieval languages: -// Перемещён ChatTransmitRange в Content.Shared /// -/// Controls transmission of chat. +/// Controls transmission of chat. /// public enum ChatTransmitRange : byte { @@ -313,3 +475,25 @@ public enum ChatTransmitRange : byte /// Ghosts can't hear or see it at all. Regular players can if in-range. NoGhosts } + +/// +/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. +/// +// ReSharper disable once InconsistentNaming +public enum InGameICChatType : byte +{ + Speak, + Emote, + Whisper +} + +/// +/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. +/// +public enum InGameOOCChatType : byte +{ + Looc, + Dead +} + +// Imperial Medieval: ChatTransmitRange already defined above diff --git a/Content.Shared/Chat/SharedSuicideSystem.cs b/Content.Shared/Chat/SharedSuicideSystem.cs index 4b9eaf24b7c..ed6eca98599 100644 --- a/Content.Shared/Chat/SharedSuicideSystem.cs +++ b/Content.Shared/Chat/SharedSuicideSystem.cs @@ -1,8 +1,10 @@ +using System.Linq; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.Mobs.Components; using Robust.Shared.Prototypes; -using System.Linq; namespace Content.Shared.Chat; @@ -27,7 +29,7 @@ public void ApplyLethalDamage(Entity target, DamageSpecifie // Mob thresholds are sorted from alive -> crit -> dead, // grabbing the last key will give us how much damage is needed to kill a target from zero // The exact lethal damage amount is adjusted based on their current damage taken - var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage; + var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - _damageableSystem.GetTotalDamage(target.AsNullable()); var totalDamage = appliedDamageSpecifier.GetTotal(); // Removing structural because it causes issues against entities that cannot take structural damage, @@ -40,7 +42,7 @@ public void ApplyLethalDamage(Entity target, DamageSpecifie appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage)); } - _damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target); + _damageableSystem.ChangeDamage(target.AsNullable(), appliedDamageSpecifier, true, origin: target); } /// @@ -54,7 +56,7 @@ public void ApplyLethalDamage(Entity target, ProtoId crit -> dead, // grabbing the last key will give us how much damage is needed to kill a target from zero // The exact lethal damage amount is adjusted based on their current damage taken - var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage; + var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - _damageableSystem.GetTotalDamage(target.AsNullable()); // We don't want structural damage for the same reasons listed above if (!_prototypeManager.TryIndex(damageType, out var damagePrototype) || damagePrototype.ID == "Structural") @@ -64,6 +66,6 @@ public void ApplyLethalDamage(Entity target, ProtoId -/// Denotes the solution that can be easily removed through any reagent container. -/// Think pouring this or draining from a water tank. +/// Denotes a specific solution contained within this entity that can can be +/// easily "drained". This means things with taps/spigots, or easily poured +/// items. /// [RegisterComponent, NetworkedComponent] public sealed partial class DrainableSolutionComponent : Component @@ -12,6 +13,12 @@ public sealed partial class DrainableSolutionComponent : Component /// /// Solution name that can be drained. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; + + /// + /// The drain doafter time required to transfer reagents from the solution. + /// + [DataField] + public TimeSpan DrainTime = TimeSpan.Zero; } diff --git a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs index 43fbe137b66..5fab79b6b42 100644 --- a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs @@ -3,8 +3,11 @@ namespace Content.Shared.Chemistry.Components; /// -/// Denotes the solution that can be easily dumped into (completely removed from the dumping container into this one) -/// Think pouring a container fully into this. +/// Denotes that there is a solution contained in this entity that can be +/// easily dumped into (that is, completely removed from the dumping container +/// into this one). Think pouring a container fully into this. The action for this is represented via drag & drop. +/// +/// To represent it being possible to controllably pour volumes into the entity, see . /// [RegisterComponent, NetworkedComponent] public sealed partial class DumpableSolutionComponent : Component @@ -12,12 +15,13 @@ public sealed partial class DumpableSolutionComponent : Component /// /// Solution name that can be dumped into. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// Whether the solution can be dumped into infinitely. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + /// Note that this is what makes the ChemMaster's buffer a stasis buffer as well! + [DataField] public bool Unlimited = false; } diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs deleted file mode 100644 index ca20e1c22fb..00000000000 --- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Content.Shared.FixedPoint; -using Robust.Shared.GameStates; -using Robust.Shared.Audio; - -namespace Content.Shared.Chemistry.Components; - -/// -/// Component that allows an entity instantly transfer liquids by interacting with objects that have solutions. -/// -[RegisterComponent, NetworkedComponent] -[AutoGenerateComponentState] -public sealed partial class HyposprayComponent : Component -{ - /// - /// Solution that will be used by hypospray for injections. - /// - [DataField] - public string SolutionName = "hypospray"; - - /// - /// Amount of the units that will be transfered. - /// - [AutoNetworkedField] - [DataField] - public FixedPoint2 TransferAmount = FixedPoint2.New(5); - - /// - /// Sound that will be played when injecting. - /// - [DataField] - public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg"); - - /// - /// Decides whether you can inject everything or just mobs. - /// - [AutoNetworkedField] - [DataField(required: true)] - public bool OnlyAffectsMobs = false; - - /// - /// If this can draw from containers in mob-only mode. - /// - [AutoNetworkedField] - [DataField] - public bool CanContainerDraw = true; - - /// - /// Whether or not the hypospray is able to draw from containers or if it's a single use - /// device that can only inject. - /// - [DataField] - public bool InjectOnly = false; -} diff --git a/Content.Shared/Chemistry/Components/InjectorComponent.cs b/Content.Shared/Chemistry/Components/InjectorComponent.cs index d3a0503c3c7..eb1890fbb54 100644 --- a/Content.Shared/Chemistry/Components/InjectorComponent.cs +++ b/Content.Shared/Chemistry/Components/InjectorComponent.cs @@ -1,10 +1,10 @@ using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Prototypes; using Content.Shared.Chemistry.Reagent; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.Components; @@ -14,11 +14,10 @@ namespace Content.Shared.Chemistry.Components; /// /// Can optionally support both /// injection and drawing or just injection. Can inject/draw reagents from solution -/// containers, and can directly inject into a mobs bloodstream. +/// containers, and can directly inject into a mob's bloodstream. /// -/// -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(InjectorSystem))] public sealed partial class InjectorComponent : Component { /// @@ -34,66 +33,44 @@ public sealed partial class InjectorComponent : Component public Entity? Solution = null; /// - /// Whether or not the injector is able to draw from containers or if it's a single use - /// device that can only inject. - /// - [DataField] - public bool InjectOnly; - - /// - /// Whether or not the injector is able to draw from or inject from mobs. + /// Amount to inject or draw on each usage. /// /// - /// For example: droppers would ignore mobs. + /// If its set null, this injector is marked to inject its entire contents upon usage. /// - [DataField] - public bool IgnoreMobs; + [DataField, AutoNetworkedField] + public FixedPoint2? CurrentTransferAmount = FixedPoint2.New(5); - /// - /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed. - /// - /// - /// For example: droppers can not inject into cans, but syringes can. - /// - [DataField] - public bool IgnoreClosed = true; /// - /// The transfer amounts for the set-transfer verb. + /// The mode that this injector starts with on MapInit. /// - [DataField] - public List TransferAmounts = new() { 1, 5, 10, 15 }; + [DataField(required: true), AutoNetworkedField] + public ProtoId ActiveModeProtoId; /// - /// Amount to inject or draw on each usage. If the injector is inject only, it will - /// attempt to inject it's entire contents upon use. + /// The possible that it can switch between. /// - [DataField, AutoNetworkedField] - public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5); + [DataField(required: true)] + public List> AllowedModes; /// - /// Injection delay (seconds) when the target is a mob. + /// Whether the injector is able to draw from or inject from mobs. /// - /// - /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or - /// in combat mode. - /// + /// + /// Droppers ignore mobs. + /// [DataField] - public TimeSpan Delay = TimeSpan.FromSeconds(5); + public bool IgnoreMobs; /// - /// Each additional 1u after first 5u increases the delay by X seconds. + /// Whether the injector is able to draw from or inject into containers that are closed/sealed. /// + /// + /// Droppers can't inject into closed cans. + /// [DataField] - public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1); - - /// - /// The state of the injector. Determines it's attack behavior. Containers must have the - /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should - /// only ever be set to Inject - /// - [DataField, AutoNetworkedField] - public InjectorToggleMode ToggleState = InjectorToggleMode.Draw; + public bool IgnoreClosed = true; /// /// Reagents that are allowed to be within this injector. @@ -101,44 +78,34 @@ public sealed partial class InjectorComponent : Component /// A null ReagentWhitelist indicates all reagents are allowed. /// [DataField] - public List>? ReagentWhitelist = null; + public List>? ReagentWhitelist; #region Arguments for injection doafter - /// + /// [DataField] public bool NeedHand = true; - /// + /// [DataField] public bool BreakOnHandChange = true; - /// + /// [DataField] public float MovementThreshold = 0.1f; + // Imperial Space + [DataField] + public bool PopupOnInjectFail = true; + // Imperial Space + #endregion } -/// -/// Possible modes for an . -/// -[Serializable, NetSerializable] -public enum InjectorToggleMode : byte +internal static class InjectorToggleModeExtensions { - /// - /// The injector will try to inject reagent into things. - /// - Inject, - - /// - /// The injector will try to draw reagent from things. - /// - Draw, + public static bool HasAnyFlag(this InjectorBehavior s1, InjectorBehavior s2) + { + return (s1 & s2) != 0; + } } - -/// -/// Raised on the injector when the doafter has finished. -/// -[Serializable, NetSerializable] -public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Chemistry/Components/MovespeedModifierMetabolismComponent.cs b/Content.Shared/Chemistry/Components/MovespeedModifierMetabolismComponent.cs deleted file mode 100644 index 62127ab5337..00000000000 --- a/Content.Shared/Chemistry/Components/MovespeedModifierMetabolismComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Chemistry.Components -{ - //TODO: refactor movement modifier component because this is a pretty poor solution - [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] - public sealed partial class MovespeedModifierMetabolismComponent : Component - { - [AutoNetworkedField, ViewVariables] - public float WalkSpeedModifier { get; set; } - - [AutoNetworkedField, ViewVariables] - public float SprintSpeedModifier { get; set; } - - /// - /// When the current modifier is expected to end. - /// - [AutoNetworkedField, ViewVariables] - public TimeSpan ModifierTimer { get; set; } = TimeSpan.Zero; - } -} - diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs index 245b7398a7e..41d6d429382 100644 --- a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs @@ -4,9 +4,11 @@ namespace Content.Shared.Chemistry.Components; /// -/// Reagents that can be added easily. For example like -/// pouring something into another beaker, glass, or into the gas -/// tank of a car. +/// Denotes that the entity has a solution contained which can be easily added +/// to in controlled volumes. This should go on things that are meant to be refilled, including +/// pouring things into a beaker. The action for this is represented via clicking. +/// +/// To represent it being possible to just dump entire volumes at once into an entity, see . /// [RegisterComponent, NetworkedComponent] public sealed partial class RefillableSolutionComponent : Component @@ -14,12 +16,18 @@ public sealed partial class RefillableSolutionComponent : Component /// /// Solution name that can added to easily. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// The maximum amount that can be transferred to the solution at once /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public FixedPoint2? MaxRefill = null; + + /// + /// The refill doafter time required to transfer reagents into the solution. + /// + [DataField] + public TimeSpan RefillTime = TimeSpan.Zero; } diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index b0b09bef0c1..fe7c693b411 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -38,7 +38,6 @@ public sealed partial class Solution : IEnumerable, ISerializat /// systems use this. /// [DataField("maxVol")] - [ViewVariables(VVAccess.ReadWrite)] public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero; public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float(); @@ -46,8 +45,7 @@ public sealed partial class Solution : IEnumerable, ISerializat /// /// If reactions will be checked for when adding reagents to the container. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("canReact")] + [DataField] public bool CanReact { get; set; } = true; /// @@ -59,8 +57,7 @@ public sealed partial class Solution : IEnumerable, ISerializat /// /// The temperature of the reagents in the solution. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("temperature")] + [DataField] public float Temperature { get; set; } = 293.15f; /// @@ -101,7 +98,7 @@ public void UpdateHeatCapacity(IPrototypeManager? protoMan) _heatCapacity = 0; foreach (var (reagent, quantity) in Contents) { - _heatCapacity += (float) quantity * + _heatCapacity += (float)quantity * protoMan.Index(reagent.Prototype).SpecificHeat; } @@ -149,7 +146,7 @@ public Solution(int capacity) /// /// The prototype ID of the reagent to add. /// The quantity in milli-units. - public Solution(string prototype, FixedPoint2 quantity, List? data = null) : this() + public Solution([ForbidLiteral] string prototype, FixedPoint2 quantity, List? data = null) : this() { AddReagent(new ReagentId(prototype, data), quantity); } @@ -171,7 +168,12 @@ public Solution(IEnumerable reagents, bool setMaxVol = true) public Solution(Solution solution) { - Contents = solution.Contents.ShallowClone(); + Contents = new(solution.Contents.Count); + foreach (var item in solution.Contents) + { + Contents.Add(item.Clone()); + } + Volume = solution.Volume; MaxVolume = solution.MaxVolume; Temperature = solution.Temperature; @@ -187,11 +189,16 @@ public Solution Clone() return new Solution(this); } + public override string ToString() + { + return string.Join("; ", Contents); + } + [AssertionMethod] public void ValidateSolution() { // sandbox forbids: [Conditional("DEBUG")] - #if DEBUG +#if DEBUG // Correct volume DebugTools.Assert(Contents.Select(x => x.Quantity).Sum() == Volume); @@ -209,7 +216,7 @@ public void ValidateSolution() UpdateHeatCapacity(null); DebugTools.Assert(MathHelper.CloseTo(_heatCapacity, cur, tolerance: 0.01)); } - #endif +#endif } void ISerializationHooks.AfterDeserialization() @@ -224,7 +231,7 @@ void ISerializationHooks.AfterDeserialization() MaxVolume = Volume; } - public bool ContainsPrototype(string prototype) + public bool ContainsPrototype([ForbidLiteral] string prototype) { foreach (var (reagent, _) in Contents) { @@ -246,7 +253,7 @@ public bool ContainsReagent(ReagentId id) return false; } - public bool ContainsReagent(string reagentId, List? data) + public bool ContainsReagent([ForbidLiteral] string reagentId, List? data) => ContainsReagent(new(reagentId, data)); public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity) @@ -306,7 +313,7 @@ public FixedPoint2 GetReagentQuantity(ReagentId id) /// If you only want the volume of a single reagent, use /// [Pure] - public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes) + public FixedPoint2 GetTotalPrototypeQuantity(params ProtoId[] prototypes) { var total = FixedPoint2.Zero; foreach (var (reagent, quantity) in Contents) @@ -318,7 +325,7 @@ public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes) return total; } - public FixedPoint2 GetTotalPrototypeQuantity(string id) + public FixedPoint2 GetTotalPrototypeQuantity(ProtoId id) { var total = FixedPoint2.Zero; foreach (var (reagent, quantity) in Contents) @@ -353,7 +360,7 @@ public FixedPoint2 GetTotalPrototypeQuantity(string id) /// /// The prototype ID of the reagent to add. /// The quantity in milli-units. - public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true) + public void AddReagent([ForbidLiteral] string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true) => AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap); /// @@ -478,6 +485,37 @@ public void ScaleSolution(float scale) ValidateSolution(); } + /// + /// Scales the amount of solution. + /// + /// The scalar to modify the solution by. + public void ScaleSolution(FixedPoint2 scale) + { + ScaleSolution(scale.Float()); + } + + /// + /// Scales the amount of solution to a given target. + /// + /// The volume the solutions should have after scaling. + public void ScaleTo(FixedPoint2 target) + { + if (Volume == FixedPoint2.Zero || Volume == target) + return; + + if (target == FixedPoint2.Zero) + { + RemoveAllSolution(); + return; + } + + var nearestIntScale = (int)Math.Ceiling(target.Float() / Volume.Float()); + ScaleSolution(nearestIntScale); + + var overflow = Volume - target; + RemoveSolution(overflow); + } + /// /// Attempts to remove an amount of reagent from the solution. /// @@ -649,7 +687,7 @@ public Solution SplitSolutionWithout(FixedPoint2 toTake, params ProtoId /// Splits a solution with only the specified reagent prototypes. /// - public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes) + public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params ProtoId[] includedPrototypes) { // First remove the non-included prototypes List excluded = new(); @@ -674,6 +712,12 @@ public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includ return sol; } + /// + /// Splits a solution into two by moving reagents from the given solution into a new one. + /// This modifies the original solution. + /// + /// The quantity of this solution to remove. + /// A new solution containing the removed reagents. public Solution SplitSolution(FixedPoint2 toTake) { if (toTake <= FixedPoint2.Zero) @@ -691,7 +735,7 @@ public Solution SplitSolution(FixedPoint2 toTake) var origVol = Volume; var effVol = Volume.Value; newSolution = new Solution(Contents.Count) { Temperature = Temperature }; - var remaining = (long) toTake.Value; + var remaining = (long)toTake.Value; for (var i = Contents.Count - 1; i >= 0; i--) // iterate backwards because of remove swap. { @@ -707,7 +751,7 @@ public Solution SplitSolution(FixedPoint2 toTake) continue; } - var splitQuantity = FixedPoint2.FromCents((int) split); + var splitQuantity = FixedPoint2.FromCents((int)split); var newQuantity = quantity - splitQuantity; DebugTools.Assert(newQuantity >= 0); @@ -754,7 +798,7 @@ public void RemoveSolution(FixedPoint2 toTake) var effVol = Volume.Value; Volume -= toTake; - var remaining = (long) toTake.Value; + var remaining = (long)toTake.Value; for (var i = Contents.Count - 1; i >= 0; i--)// iterate backwards because of remove swap. { var (reagent, quantity) = Contents[i]; @@ -769,7 +813,7 @@ public void RemoveSolution(FixedPoint2 toTake) continue; } - var splitQuantity = FixedPoint2.FromCents((int) split); + var splitQuantity = FixedPoint2.FromCents((int)split); var newQuantity = quantity - splitQuantity; if (newQuantity > FixedPoint2.Zero) @@ -842,7 +886,7 @@ public void AddSolution(Solution otherSolution, IPrototypeManager? protoMan) ValidateSolution(); } - public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without) + public Color GetColorWithout(IPrototypeManager? protoMan, params ProtoId[] without) { if (Volume == FixedPoint2.Zero) { @@ -885,7 +929,7 @@ public Color GetColor(IPrototypeManager? protoMan) return GetColorWithout(protoMan); } - public Color GetColorWithOnly(IPrototypeManager? protoMan, params string[] included) + public Color GetColorWithOnly(IPrototypeManager? protoMan, params ProtoId[] included) { if (Volume == FixedPoint2.Zero) { @@ -963,5 +1007,15 @@ public Dictionary GetReagentPrototypes(IPrototype } return dict; } + + public void SetReagentData(List? data) + { + for (var i = 0; i < Contents.Count; i++) + { + var old = Contents[i]; + Contents[i] = new ReagentQuantity(new ReagentId(old.Reagent.Prototype, data), old.Quantity); + } + ValidateSolution(); + } } } diff --git a/Content.Shared/Chemistry/Components/SolutionComponent.cs b/Content.Shared/Chemistry/Components/SolutionComponent.cs index 687a545b00b..847f8f03c91 100644 --- a/Content.Shared/Chemistry/Components/SolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/SolutionComponent.cs @@ -1,6 +1,8 @@ using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Materials; using Robust.Shared.GameStates; +using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.Components; @@ -11,12 +13,20 @@ namespace Content.Shared.Chemistry.Components; /// /// Once reagents and materials have been merged this component should be depricated in favor of using a combination of and . May require minor reworks to both. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedSolutionContainerSystem))] public sealed partial class SolutionComponent : Component { /// /// The reagents the entity is composed of and their temperature. /// - [DataField, AutoNetworkedField] + [DataField] public Solution Solution = new(); } + + +[Serializable, NetSerializable] +public sealed class SolutionComponentState(Solution solution) : ComponentState +{ + public Solution Solution = solution; +} diff --git a/Content.Shared/Chemistry/Components/SolutionTransferComponent.cs b/Content.Shared/Chemistry/Components/SolutionTransferComponent.cs index 2b862a83ac8..149fe3f643e 100644 --- a/Content.Shared/Chemistry/Components/SolutionTransferComponent.cs +++ b/Content.Shared/Chemistry/Components/SolutionTransferComponent.cs @@ -12,43 +12,36 @@ public sealed partial class SolutionTransferComponent : Component /// /// The amount of solution to be transferred from this solution when clicking on other solutions with it. /// - [DataField("transferAmount")] - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public FixedPoint2 TransferAmount { get; set; } = FixedPoint2.New(5); + [DataField, AutoNetworkedField] + public FixedPoint2 TransferAmount = FixedPoint2.New(5); /// /// The minimum amount of solution that can be transferred at once from this solution. /// - [DataField("minTransferAmount")] - [ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 MinimumTransferAmount { get; set; } = FixedPoint2.New(5); + [DataField("minTransferAmount"), AutoNetworkedField] + public FixedPoint2 MinimumTransferAmount = FixedPoint2.New(5); /// /// The maximum amount of solution that can be transferred at once from this solution. /// - [DataField("maxTransferAmount")] - [ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 MaximumTransferAmount { get; set; } = FixedPoint2.New(100); + [DataField("maxTransferAmount"), AutoNetworkedField] + public FixedPoint2 MaximumTransferAmount = FixedPoint2.New(100); /// /// Can this entity take reagent from reagent tanks? /// - [DataField("canReceive")] - [ViewVariables(VVAccess.ReadWrite)] - public bool CanReceive { get; set; } = true; + [DataField, AutoNetworkedField] + public bool CanReceive = true; /// /// Can this entity give reagent to other reagent containers? /// - [DataField("canSend")] - [ViewVariables(VVAccess.ReadWrite)] - public bool CanSend { get; set; } = true; + [DataField, AutoNetworkedField] + public bool CanSend = true; /// /// Whether you're allowed to change the transfer amount. /// - [DataField("canChangeTransferAmount")] - [ViewVariables(VVAccess.ReadWrite)] - public bool CanChangeTransferAmount { get; set; } = false; + [DataField, AutoNetworkedField] + public bool CanChangeTransferAmount = false; } diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs deleted file mode 100644 index 324858afd7b..00000000000 --- a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs +++ /dev/null @@ -1,277 +0,0 @@ -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.Hypospray.Events; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Forensics; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction.Events; -using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; -using Content.Shared.Popups; -using Content.Shared.Timing; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Audio.Systems; - -namespace Content.Shared.Chemistry.EntitySystems; - -public sealed class HypospraySystem : EntitySystem -{ - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnAttack); - SubscribeLocalEvent(OnUseInHand); - SubscribeLocalEvent>(AddToggleModeVerb); - } - - #region Ref events - private void OnUseInHand(Entity entity, ref UseInHandEvent args) - { - if (args.Handled) - return; - - args.Handled = TryDoInject(entity, args.User, args.User); - } - - private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target == null) - return; - - args.Handled = TryUseHypospray(entity, args.Target.Value, args.User); - } - - private void OnAttack(Entity entity, ref MeleeHitEvent args) - { - if (args.HitEntities is []) - return; - - TryDoInject(entity, args.HitEntities[0], args.User); - } - - #endregion - - #region Draw/Inject - private bool TryUseHypospray(Entity entity, EntityUid target, EntityUid user) - { - // if target is ineligible but is a container, try to draw from the container if allowed - if (entity.Comp.CanContainerDraw - && !EligibleEntity(target, entity) - && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _)) - { - return TryDraw(entity, target, drawableSolution.Value, user); - } - - return TryDoInject(entity, target, user); - } - - public bool TryDoInject(Entity entity, EntityUid target, EntityUid user) - { - var (uid, component) = entity; - - if (!EligibleEntity(target, component)) - return false; - - if (TryComp(uid, out UseDelayComponent? delayComp)) - { - if (_useDelay.IsDelayed((uid, delayComp))) - return false; - } - - string? msgFormat = null; - - // Self event - var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target); - RaiseLocalEvent(user, selfEvent); - - if (selfEvent.Cancelled) - { - _popup.PopupClient(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - target = selfEvent.TargetGettingInjected; - - if (!EligibleEntity(target, component)) - return false; - - // Target event - var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target); - RaiseLocalEvent(target, targetEvent); - - if (targetEvent.Cancelled) - { - _popup.PopupClient(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - target = targetEvent.TargetGettingInjected; - - if (!EligibleEntity(target, component)) - return false; - - // The target event gets priority for the overriden message. - if (targetEvent.InjectMessageOverride != null) - msgFormat = targetEvent.InjectMessageOverride; - else if (selfEvent.InjectMessageOverride != null) - msgFormat = selfEvent.InjectMessageOverride; - else if (target == user) - msgFormat = "hypospray-component-inject-self-message"; - - if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0) - { - _popup.PopupClient(Loc.GetString("hypospray-component-empty-message"), target, user); - return true; - } - - if (!_solutionContainers.TryGetInjectableSolution(target, out var targetSoln, out var targetSolution)) - { - _popup.PopupClient(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - _popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", Identity.Entity(target, EntityManager))), target, user); - - if (target != user) - { - _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target); - // TODO: This should just be using melee attacks... - // meleeSys.SendLunge(angle, user); - } - - _audio.PlayPredicted(component.InjectSound, target, user); - - // Medipens and such use this system and don't have a delay, requiring extra checks - // BeginDelay function returns if item is already on delay - if (delayComp != null) - _useDelay.TryResetDelay((uid, delayComp)); - - // Get transfer amount. May be smaller than component.TransferAmount if not enough room - var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume); - - if (realTransferAmount <= 0) - { - _popup.PopupClient(Loc.GetString("hypospray-component-transfer-already-full-message", ("owner", target)), target, user); - return true; - } - - // Move units from attackSolution to targetSolution - var removedSolution = _solutionContainers.SplitSolution(hypoSpraySoln.Value, realTransferAmount); - - if (!targetSolution.CanAddSolution(removedSolution)) - return true; - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - _solutionContainers.TryAddSolution(targetSoln.Value, removedSolution); - - var ev = new TransferDnaEvent { Donor = target, Recipient = uid }; - RaiseLocalEvent(target, ref ev); - - // same LogType as syringes... - _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(uid):using}"); - - return true; - } - - private bool TryDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) - { - if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, - out var solution) || solution.AvailableVolume == 0) - { - return false; - } - - // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector - var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume, - solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - _popup.PopupClient( - Loc.GetString("injector-component-target-is-empty-message", - ("target", Identity.Entity(target, EntityManager))), - entity.Owner, user); - return false; - } - - var removedSolution = _solutionContainers.Draw(target, targetSolution, realTransferAmount); - - if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution)) - { - return false; - } - - _popup.PopupClient(Loc.GetString("injector-component-draw-success-message", - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), entity.Owner, user); - return true; - } - - private bool EligibleEntity(EntityUid entity, HyposprayComponent component) - { - // TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag? - // In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else. - // But this is 14, we dont do what SS13 does just because SS13 does it. - return component.OnlyAffectsMobs - ? HasComp(entity) && - HasComp(entity) - : HasComp(entity); - } - - #endregion - - #region Verbs - - // - // Uses the OnlyMobs field as a check to implement the ability - // to draw from jugs and containers with the hypospray - // Toggleable to allow people to inject containers if they prefer it over drawing - // - private void AddToggleModeVerb(Entity entity, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null || entity.Comp.InjectOnly) - return; - - var user = args.User; - var verb = new AlternativeVerb - { - Text = Loc.GetString("hypospray-verb-mode-label"), - Act = () => - { - ToggleMode(entity, user); - } - }; - args.Verbs.Add(verb); - } - - private void ToggleMode(Entity entity, EntityUid user) - { - SetMode(entity, !entity.Comp.OnlyAffectsMobs); - var msg = (entity.Comp.OnlyAffectsMobs && entity.Comp.CanContainerDraw) ? "hypospray-verb-mode-inject-mobs-only" : "hypospray-verb-mode-inject-all"; - _popup.PopupClient(Loc.GetString(msg), entity, user); - } - - public void SetMode(Entity entity, bool onlyAffectsMobs) - { - if (entity.Comp.OnlyAffectsMobs == onlyAffectsMobs) - return; - - entity.Comp.OnlyAffectsMobs = onlyAffectsMobs; - Dirty(entity); - } - - #endregion -} diff --git a/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs new file mode 100644 index 00000000000..7abd29a8c4d --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs @@ -0,0 +1,769 @@ +using System.Linq; +using Content.Shared.Administration.Logs; +using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Events; +using Content.Shared.Chemistry.Prototypes; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Forensics.Systems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Stacks; +using Content.Shared.Standing; +using Content.Shared.Timing; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee.Events; +using JetBrains.Annotations; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Chemistry.EntitySystems; + +/// +/// This handles toggling injection modes, injections and drawings for all kinds of injectors. +/// +/// +/// +public sealed partial class InjectorSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedForensicsSystem _forensics = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly StandingStateSystem _standingState = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnInjectorUse); + SubscribeLocalEvent(OnInjectorAfterInteract); + SubscribeLocalEvent(OnInjectDoAfter); + SubscribeLocalEvent(OnAttack); + SubscribeLocalEvent>(AddVerbs); + } + + #region Events Handling + private void OnInjectorUse(Entity injector, ref UseInHandEvent args) + { + if (args.Handled + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto)) + return; + + if (activeProto.InjectOnUse) // Injectors that can't toggle transferAmounts will be used. + TryMobsDoAfter(injector, args.User, args.User); + else // Syringes toggle Draw/Inject. + ToggleMode(injector, args.User); + + args.Handled = true; + args.ApplyDelay = false; + } + + private void OnInjectorAfterInteract(Entity injector, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { Valid: true } target) + return; + + // Is the target a mob? If yes, use a do-after to give them time to respond. + if (HasComp(target)) + { + // Are use using an injector capable of targeting a mob? + if (injector.Comp.IgnoreMobs) + { + _popup.PopupClient(Loc.GetString("injector-component-ignore-mobs"), args.Target.Value, args.User); + return; + } + + args.Handled |= TryMobsDoAfter(injector, args.User, target); + return; + } + + // Draw from or inject into jugs, bottles, etc. + args.Handled |= TryContainerDoAfter(injector, args.User, target); + } + + private void OnInjectDoAfter(Entity injector, ref InjectorDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + args.Handled |= TryUseInjector(injector, args.Args.User, args.Args.Target.Value); + } + + private void OnAttack(Entity injector, ref MeleeHitEvent args) + { + if (args.HitEntities is []) + return; + + TryMobsDoAfter(injector, args.User, args.HitEntities[0]); + } + + /// + /// Give the user interaction verbs for their injector. + /// + /// + /// + /// + /// If they have multiple transferAmounts, they'll be able to switch between them via the verbs. + /// If they have multiple injector modes and don't toggle when used in hand, they can toggle the mode with the verbs too. + /// + private void AddVerbs(Entity injector, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return; + + var user = args.User; + var min = activeMode.TransferAmounts.Min(); + var max = activeMode.TransferAmounts.Max(); + var cur = injector.Comp.CurrentTransferAmount; + var toggleAmount = cur == max ? min : max; + + var priority = 0; + + if (activeMode.TransferAmounts.Count > 1) + { + AlternativeVerb toggleVerb = new() + { + Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + injector.Comp.CurrentTransferAmount = toggleAmount; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user); + Dirty(injector); + }, + + Priority = priority + }; + args.Verbs.Add(toggleVerb); + + priority -= 1; + + // Add specific transfer verbs for amounts defined in the component + foreach (var amount in activeMode.TransferAmounts) + { + AlternativeVerb verb = new() + { + Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + injector.Comp.CurrentTransferAmount = amount; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user); + Dirty(injector); + }, + + // we want to sort by size, not alphabetically by the verb text. + Priority = priority + }; + args.Verbs.Add(verb); + } + } + + // If the injector cannot toggle via using in hand, allow toggling via verb. + if (!activeMode.InjectOnUse || injector.Comp.AllowedModes.Count <= 1) + return; + + var toggleModeVerb = new AlternativeVerb + { + Text = Loc.GetString("injector-toggle-verb-text"), + Act = () => + { + ToggleMode(injector, user); + }, + Priority = priority, + }; + + args.Verbs.Add(toggleModeVerb); + } + #endregion Events Handling + + #region Mob Interaction + /// + /// Send informative pop-up messages and wait for a do-after to complete. + /// + private bool TryMobsDoAfter(Entity injector, EntityUid user, EntityUid target) + { + if (_useDelay.IsDelayed(injector.Owner) // Check for Delay. + || !GetMobsDoAfterTime(injector, user, target, out var doAfterTime, out var amount)) // Get the DoAfter time. + return false; + + if (!_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) + { + BreakOnMove = true, + BreakOnWeightlessMove = false, + BreakOnDamage = true, + NeedHand = injector.Comp.NeedHand, + BreakOnHandChange = injector.Comp.BreakOnHandChange, + MovementThreshold = injector.Comp.MovementThreshold, + })) + return false; + + // If the DoAfter was instant, don't send popups and logs indicating an attempt. + if (doAfterTime == TimeSpan.Zero) + return true; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution) + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + // Create a pop-up for the user. + _popup.PopupClient(Loc.GetString(activeMode.PopupUserAttempt), target, user); + + if (user == target) + { + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw)) + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to draw {amount} units from themselves."); + } + else + { + _adminLogger.Add(LogType.Ingestion, + $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}."); + } + } + else + { + // Create a popup to the target. + var userName = Identity.Entity(user, EntityManager); + var popup = Loc.GetString(activeMode.PopupTargetAttempt, ("user", userName)); + _popup.PopupEntity(popup, user, target); + + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw)) + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to draw {amount} units from {ToPrettyString(target):target}"); + } + else + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}"); + } + } + + return true; + } + + /// + /// Get the DoAfter Time for Mobs. + /// + /// The injector that is interacting with the mob. + /// The user using the injector. + /// The target mob. + /// The duration of the resulting doAfter. + /// The amount of the reagents transferred. + /// True if calculating the time was successful, false if not. + private bool GetMobsDoAfterTime(Entity injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime, out FixedPoint2 amount) + { + doAfterTime = TimeSpan.Zero; + amount = FixedPoint2.Zero; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution) + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + doAfterTime = activeMode.MobTime; + + // Can only draw blood with a draw mode and a transferAmount. + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw) && injector.Comp.CurrentTransferAmount != null) + { + // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount + amount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount.Value, injectorSolution.AvailableVolume); + } + else + { + // Check if we have anything to inject. + if (injectorSolution.Volume == 0) + { + _popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), target, user); + return false; + } + + // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount + // If CurrentTransferAmount is null, it'll want to inject its entire contents, e.g., epipens. + amount = injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume; + amount = FixedPoint2.Min(amount, injectorSolution.Volume); + } + + // Transfers over the IgnoreDelayForVolume amount take Xu times DelayPerVolume longer. + doAfterTime += activeMode.DelayPerVolume * FixedPoint2.Max(0, amount - activeMode.IgnoreDelayForVolume).Double(); + + // Check if the target is either the user or downed. + if (user == target) // Self-injections take priority. + doAfterTime *= activeMode.SelfModifier; + // Technically, both can be true, but that is probably a balance nightmare. + else if (_standingState.IsDown(target)) + doAfterTime *= activeMode.DownedModifier; + + return true; + } + #endregion Mob Interaction + + #region Container Interaction + private bool TryContainerDoAfter(Entity injector, EntityUid user, EntityUid target) + { + if (!GetContainerDoAfterTime(injector, user, target, out var doAfterTime)) + return false; + + return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) + { + BreakOnMove = true, + BreakOnWeightlessMove = false, + BreakOnDamage = true, + NeedHand = injector.Comp.NeedHand, + BreakOnHandChange = injector.Comp.BreakOnHandChange, + MovementThreshold = injector.Comp.MovementThreshold, + }); + } + + /// + /// Get the DoAfter Time for Containers and check if it is possible. + /// + /// The injector that is interacting with the container. + /// The user using the injector. + /// The target container, + /// The duration of the resulting DoAfter. + /// True if calculating the time was successful, false if not. + private bool GetContainerDoAfterTime(Entity injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime) + { + doAfterTime = TimeSpan.Zero; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + // Check if the Injector has a draw time, but only when drawing. + if (!activeMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic)) + return true; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.AvailableVolume == 0) + { + _popup.PopupClient(Loc.GetString("injector-component-cannot-toggle-draw-message"), user, user); + return false; // If already full, fail drawing. + } + + if (!_solutionContainer.TryGetDrawableSolution(target, out _, out var drawableSol)) + { + _popup.PopupClient(Loc.GetString("injector-component-cannot-draw-message", ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + if (drawableSol.Volume == 0) + { + _popup.PopupClient(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + doAfterTime = activeMode.ContainerDrawTime; + return true; + } + #endregion Container Interaction + + #region Injecting/Drawing + /// + /// Depending on the , this will deal with the result of the DoAfter and draw/inject accordingly. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// True if the injection/drawing was successful, false if not. + /// The injector has a different . + private bool TryUseInjector(Entity injector, EntityUid user, EntityUid target) + { + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target); + + LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message"; + + switch (activeMode.Behavior) + { + // Handle injecting/drawing for solutions + case InjectorBehavior.Inject: + { + if (isOpenOrIgnored && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) + return TryInject(injector, user, target, injectableSolution.Value, false); + + if (isOpenOrIgnored && _solutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _)) + return TryInject(injector, user, target, refillableSolution.Value, true); + break; + } + case InjectorBehavior.Draw: + { + // Draw from a bloodstream if the target has that + if (TryComp(target, out var stream) && + _solutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) + { + return TryDraw(injector, user, (target, stream), stream.BloodSolution.Value); + } + + // Draw from an object (food, beaker, etc) + if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) + return TryDraw(injector, user, target, drawableSolution.Value); + + msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message"; + _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); + break; + } + case InjectorBehavior.Dynamic: + { + // If it's a mob, inject. We're using injectableSolution so I don't have to code a sole method for injecting into bloodstreams. + if (HasComp(target) + && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) + { + return TryInject(injector, user, target, injectableSolution.Value, false); + } + + // Draw from an object (food, beaker, etc.) + if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) + return TryDraw(injector, user, target, drawableSolution.Value); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + /// + /// Attempt to inject the solution of the injector into the target. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the target. + /// Whether or not the solution is refillable or injectable. + /// True if the injection was successful, false if not. + private bool TryInject(Entity injector, EntityUid user, EntityUid target, Entity targetSolution, bool asRefill) + { + if (!_solutionContainer.ResolveSolution(injector.Owner, + injector.Comp.SolutionName, + ref injector.Comp.Solution, + out var injectorSolution) || injectorSolution.Volume == 0) + { + // If empty, show a popup. + _popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), user, user); + return false; + } + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + var selfEv = new SelfBeforeInjectEvent(user, injector, target); + RaiseLocalEvent(user, selfEv); + + if (selfEv.Cancelled) + { + // Clowns will now also fumble Syringes. + if (selfEv.OverrideMessage != null) + _popup.PopupPredicted(selfEv.OverrideMessage, user, user); + return true; + } + + target = selfEv.TargetGettingInjected; + + var ev = new TargetBeforeInjectEvent(user, injector, target); + RaiseLocalEvent(target, ref ev); + + // Jugsuit blocking Hyposprays when + if (ev.Cancelled) + { + var userMessage = Loc.GetString("injector-component-blocked-user"); + var otherMessage = Loc.GetString("injector-component-blocked-other", ("target", target), ("user", user)); + _popup.PopupPredicted(userMessage, otherMessage, target, user, PopupType.SmallCaution); + return true; + } + + // Get transfer amount. It may be smaller than _transferAmount if not enough room + var plannedTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume, injectorSolution.Volume); + var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, targetSolution.Comp.Solution.AvailableVolume); + + if (realTransferAmount <= 0) + { + LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message"; + _popup.PopupClient( + Loc.GetString(msg, + ("target", Identity.Entity(target, EntityManager))), + injector.Owner, + user); + return false; + } + + // Move units from attackSolution to targetSolution + Solution removedSolution; + if (TryComp(target, out var stack)) + removedSolution = _solutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count); + else + removedSolution = _solutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount); + + _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); + + if (!asRefill) + _solutionContainer.Inject(target, targetSolution, removedSolution); + else + _solutionContainer.Refill(target, targetSolution, removedSolution); + + LocId msgSuccess = target == user ? "injector-component-inject-success-message-self" : "injector-component-inject-success-message"; + + if (selfEv.OverrideMessage != null) + msgSuccess = selfEv.OverrideMessage; + else if (ev.OverrideMessage != null) + msgSuccess = ev.OverrideMessage; + + _popup.PopupClient(Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", Identity.Entity(target, EntityManager))), target, user); + + // it is IMPERATIVE that when an injector is instant, that it has a pop-up. + if (activeMode.InjectPopupTarget != null && target != user) + _popup.PopupClient(Loc.GetString(activeMode.InjectPopupTarget), target, target); + + // Some injectors like hyposprays have sound, some like syringes have not. + if (activeMode.InjectSound != null) + _audio.PlayPredicted(activeMode.InjectSound, injector, user); + + // Log what happened. + _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(injector):using}"); + + AfterInject(injector, user, target); + return true; + } + + /// + /// Attempt to draw reagents from a container. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the target. + /// True if the drawing was successful, false if not. + private bool TryDraw(Entity injector, EntityUid user, Entity target, Entity targetSolution) + { + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) || solution.AvailableVolume == 0) + { + _popup.PopupClient("injector-component-cannot-toggle-draw-message", user, user); + return false; + } + + var applicableTargetSolution = targetSolution.Comp.Solution; + // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily + var temporarilyRemovedSolution = new Solution(); + if (injector.Comp.ReagentWhitelist is { } reagentWhitelist) + { + temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray()); + } + + // If transferAmount is null, fallback to 5 units. + var plannedTransferAmount = injector.Comp.CurrentTransferAmount ?? FixedPoint2.New(5); + // Get transfer amount. It may be smaller than _transferAmount if not enough room, also make sure there's room in the injector + var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, + applicableTargetSolution.Volume, + solution.AvailableVolume); + + if (realTransferAmount <= 0) + { + LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message"; + var targetIdentity = Identity.Entity(target, EntityManager); + _popup.PopupClient(Loc.GetString(msg, ("target", targetIdentity)), injector.Owner, user); + return false; + } + + // We have some snowflaked behavior for streams. + if (target.Comp != null) + { + DrawFromBlood(injector, user, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount); + return true; + } + + // Move units from attackSolution to targetSolution + var removedSolution = _solutionContainer.Draw(target.Owner, targetSolution, realTransferAmount); + + // Add back non-whitelisted reagents to the target solution + _solutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution); + + if (!_solutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution)) + { + return false; + } + + LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; + var targetIdentitySuccess = Identity.Entity(target, EntityManager); + _popup.PopupClient( + Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", targetIdentitySuccess)), + target, + user); + + AfterDraw(injector, user, target); + return true; + } + + /// + /// Attempt to draw blood from a mob. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the injector. + /// The amount of blood to draw. + private void DrawFromBlood(Entity injector, + EntityUid user, + Entity target, + Entity injectorSolution, + FixedPoint2 transferAmount) + { + if (_solutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution)) + { + var bloodTemp = _solutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount); + _solutionContainer.TryAddSolution(injectorSolution, bloodTemp); + } + + LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; + var targetIdentity = Identity.Entity(target, EntityManager); + var finalMessage = Loc.GetString(msg, ("amount", transferAmount), ("target", targetIdentity)); + _popup.PopupClient(finalMessage, target, user); + + AfterDraw(injector, user, target); + } + + /// + /// This handles logic like DNA and Delays after injection. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + private void AfterInject(Entity injector, EntityUid user, EntityUid target) + { + // Leave some DNA from the injectee on it + _forensics.TransferDna(injector, target); + // Reset the delay, if present. + + _useDelay.TryResetDelay(injector); + + // Automatically set syringe to draw after completely draining it. + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.Volume != 0) + return; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode) + || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic)) + return; + + foreach (var mode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(mode, out var proto) + || !proto.Behavior.HasFlag(InjectorBehavior.Draw)) + continue; + + ToggleMode(injector, user, proto); + return; + } + } + + /// + /// This handles logic like DNA after drawing. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + private void AfterDraw(Entity injector, EntityUid user, EntityUid target) + { + // Leave some DNA from the drawee on it + _forensics.TransferDna(injector, target); + + // Automatically set the syringe to inject after completely filling it. + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.AvailableVolume != 0) + return; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode) + || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic)) + return; + + foreach (var mode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(mode, out var proto) + || !proto.Behavior.HasFlag(InjectorBehavior.Inject)) + continue; + + ToggleMode(injector, user, proto); + return; + } + } + #endregion Injecting/Drawing + + #region Mode Toggling + /// + /// Toggle modes of the injector if possible. + /// + /// The injector whose mode is to be toggled. + /// The user toggling the mode. + /// The desired mode. + /// This will still check if the injector can use that mode. + [PublicAPI] + public void ToggleMode(Entity injector, EntityUid user, InjectorModePrototype mode) + { + var index = injector.Comp.AllowedModes.FindIndex(nextMode => mode == nextMode); + + injector.Comp.ActiveModeProtoId = injector.Comp.AllowedModes[index]; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var newMode)) + return; + + var modeName = Loc.GetString(newMode.Name); + var message = Loc.GetString("injector-component-mode-changed-text", ("mode", modeName)); + _popup.PopupClient(message, user, user); + Dirty(injector); + } + + /// + /// Toggle the mode of the injector to the next allowed mode. + /// + /// The injector whose mode is to be toggled. + /// The user toggling the mode. + [PublicAPI] + public void ToggleMode(Entity injector, EntityUid user) + { + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto)) + return; + + string? errorMessage = null; + + foreach (var allowedMode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(allowedMode, out var proto) + || proto.Behavior.HasFlag(activeProto.Behavior) + || !_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) + continue; + + if (proto.Behavior.HasFlag(InjectorBehavior.Inject) && solution.Volume == 0) + { + errorMessage = "injector-component-cannot-toggle-inject-message"; + continue; + } + + if (proto.Behavior.HasFlag(InjectorBehavior.Draw) && solution.AvailableVolume == 0) + { + errorMessage = "injector-component-cannot-toggle-draw-message"; + continue; + } + + ToggleMode(injector, user, proto); + return; + } + if (errorMessage != null) + _popup.PopupClient(Loc.GetString(errorMessage), user, user); + } + #endregion Mode Toggling +} diff --git a/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs index aa217c60ba3..2bfa1c9de38 100644 --- a/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using Robust.Shared.Containers; +using Robust.Shared.Timing; namespace Content.Shared.Chemistry.EntitySystems; @@ -9,6 +10,7 @@ public sealed class ReactiveContainerSystem : EntitySystem [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { @@ -34,6 +36,10 @@ private void OnInserted(EntityUid uid, ReactiveContainerComponent comp, EntInser private void OnSolutionChange(EntityUid uid, ReactiveContainerComponent comp, SolutionContainerChangedEvent args) { + // The changes are already networked as part of the same game state. + if (_timing.ApplyingState) + return; + if (!_solutionContainerSystem.TryGetSolution(uid, comp.Solution, out _, out var solution)) return; if (solution.Volume == 0) diff --git a/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs b/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs index 0f5d51057f2..697605af1d6 100644 --- a/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Popups; using Robust.Shared.Network; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Shared.Chemistry.EntitySystems; @@ -16,6 +17,7 @@ public sealed class RehydratableSystem : EntitySystem [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; [Dependency] private readonly SharedTransformSystem _xform = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { @@ -26,6 +28,10 @@ public override void Initialize() private void OnSolutionChange(Entity ent, ref SolutionContainerChangedEvent args) { + // The changes are already networked as part of the same game state. + if (_timing.ApplyingState) + return; + var quantity = _solutions.GetTotalPrototypeQuantity(ent, ent.Comp.CatalystPrototype); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner)} was hydrated, now contains a solution of: {SharedSolutionContainerSystem.ToPrettyString(args.Solution)}."); if (quantity != FixedPoint2.Zero && quantity >= ent.Comp.CatalystMinimum) diff --git a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs index 86f9ffa3909..a40c28b5866 100644 --- a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs @@ -36,7 +36,7 @@ public bool TryScoop(Entity ent, EntityUid beaker, E !_solution.TryGetRefillableSolution(beaker, out var target, out _)) return false; - var scooped = _solutionTransfer.Transfer(user, ent, src.Value, beaker, target.Value, srcSolution.Volume); + var scooped = _solutionTransfer.Transfer(new SolutionTransferData(user, ent, src.Value, beaker, target.Value, srcSolution.Volume)); if (scooped == 0) return false; diff --git a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs deleted file mode 100644 index a39f8514577..00000000000 --- a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs +++ /dev/null @@ -1,546 +0,0 @@ -using System.Linq; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.CombatMode; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; -using Content.Shared.Forensics.Systems; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Popups; -using Content.Shared.Stacks; -using Content.Shared.Verbs; - -namespace Content.Shared.Chemistry.EntitySystems; - -public abstract class SharedInjectorSystem : EntitySystem -{ - [Dependency] private readonly SharedBloodstreamSystem _blood = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly OpenableSystem _openable = default!; - [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; - [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedForensicsSystem _forensics = default!; - [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!; - - public override void Initialize() - { - SubscribeLocalEvent>(AddSetTransferVerbs); - SubscribeLocalEvent(OnInjectorUse); - SubscribeLocalEvent(OnInjectorAfterInteract); - SubscribeLocalEvent(OnInjectDoAfter); - } - - private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null) - return; - - if (ent.Comp.TransferAmounts.Count <= 1) - return; // No options to cycle between - - var user = args.User; - - var min = ent.Comp.TransferAmounts.Min(); - var max = ent.Comp.TransferAmounts.Max(); - var cur = ent.Comp.CurrentTransferAmount; - var toggleAmount = cur == max ? min : max; - - var priority = 0; - AlternativeVerb toggleVerb = new() - { - Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)), - Category = VerbCategory.SetTransferAmount, - Act = () => - { - ent.Comp.CurrentTransferAmount = toggleAmount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user); - Dirty(ent); - }, - - Priority = priority - }; - args.Verbs.Add(toggleVerb); - - priority -= 1; - - // Add specific transfer verbs for amounts defined in the component - foreach (var amount in ent.Comp.TransferAmounts) - { - AlternativeVerb verb = new() - { - Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)), - Category = VerbCategory.SetTransferAmount, - Act = () => - { - ent.Comp.CurrentTransferAmount = amount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user); - Dirty(ent); - }, - - // we want to sort by size, not alphabetically by the verb text. - Priority = priority - }; - - priority -= 1; - - args.Verbs.Add(verb); - } - } - - private void OnInjectorUse(Entity ent, ref UseInHandEvent args) - { - if (args.Handled) - return; - - Toggle(ent, args.User); - args.Handled = true; - } - - private void OnInjectorAfterInteract(Entity ent, ref AfterInteractEvent args) - { - if (args.Handled || !args.CanReach) - return; - - //Make sure we have the attacking entity - if (args.Target is not { Valid: true } target || !HasComp(ent)) - return; - - // Is the target a mob? If yes, use a do-after to give them time to respond. - if (HasComp(target) || HasComp(target)) - { - // Are use using an injector capable of targeting a mob? - if (ent.Comp.IgnoreMobs) - return; - - InjectDoAfter(ent, target, args.User); - args.Handled = true; - return; - } - - // Instantly draw from or inject into jugs, bottles etc. - args.Handled = TryUseInjector(ent, target, args.User); - } - - private void OnInjectDoAfter(Entity ent, ref InjectorDoAfterEvent args) - { - if (args.Cancelled || args.Handled || args.Args.Target == null) - return; - - args.Handled = TryUseInjector(ent, args.Args.Target.Value, args.Args.User); - } - - /// - /// Send informative pop-up messages and wait for a do-after to complete. - /// - private void InjectDoAfter(Entity injector, EntityUid target, EntityUid user) - { - // Create a pop-up for the user - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - _popup.PopupClient(Loc.GetString("injector-component-drawing-user"), target, user); - } - else - { - _popup.PopupClient(Loc.GetString("injector-component-injecting-user"), target, user); - } - - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) - return; - - var actualDelay = injector.Comp.Delay; - FixedPoint2 amountToInject; - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount - amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.MaxVolume - solution.Volume); - } - else - { - // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount - amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.Volume); - } - - // Injections take 0.5 seconds longer per 5u of possible space/content - // First 5u(MinimumTransferAmount) doesn't incur delay - actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.TransferAmounts.Min()).Double(); - - // Ensure that minimum delay before incapacitation checks is 1 seconds - actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1)); - - if (user != target) // injecting someone else - { - // Create a pop-up for the target - var userName = Identity.Entity(user, EntityManager); - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - _popup.PopupEntity(Loc.GetString("injector-component-drawing-target", - ("user", userName)), user, target); - } - else - { - _popup.PopupEntity(Loc.GetString("injector-component-injecting-target", - ("user", userName)), user, target); - } - - - // Check if the target is incapacitated or in combat mode and modify time accordingly. - if (_mobState.IsIncapacitated(target)) - { - actualDelay /= 2.5f; - } - else if (_combatMode.IsInCombatMode(target)) - { - // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in - // combat with fast syringes & lag. - actualDelay += TimeSpan.FromSeconds(1); - } - - // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same. - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}"); - } - else - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from {ToPrettyString(target):target}"); - } - } - else // injecting yourself - { - // Self-injections take half as long. - actualDelay /= 2; - - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - _adminLogger.Add(LogType.Ingestion, - $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}."); - } - else - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from themselves."); - } - } - - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) - { - BreakOnMove = true, - BreakOnWeightlessMove = false, - BreakOnDamage = true, - NeedHand = injector.Comp.NeedHand, - BreakOnHandChange = injector.Comp.BreakOnHandChange, - MovementThreshold = injector.Comp.MovementThreshold, - }); - } - - private bool TryUseInjector(Entity injector, EntityUid target, EntityUid user) - { - var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target); - // Handle injecting/drawing for solutions - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - if (isOpenOrIgnored && SolutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) - return TryInject(injector, target, injectableSolution.Value, user, false); - - if (isOpenOrIgnored && SolutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _)) - return TryInject(injector, target, refillableSolution.Value, user, true); - - if (TryComp(target, out var bloodstream)) - return TryInjectIntoBloodstream(injector, (target, bloodstream), user); - - LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message"; - _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); - } - else if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - // Draw from a bloodstream, if the target has that - if (TryComp(target, out var stream) && - SolutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) - { - return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user); - } - - // Draw from an object (food, beaker, etc) - if (isOpenOrIgnored && SolutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) - return TryDraw(injector, target, drawableSolution.Value, user); - - LocId msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message"; - _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); - } - return false; - } - - private bool TryInject(Entity injector, EntityUid target, - Entity targetSolution, EntityUid user, bool asRefill) - { - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) || solution.Volume == 0) - return false; - - // Get transfer amount. May be smaller than _transferAmount if not enough room - var realTransferAmount = - FixedPoint2.Min(injector.Comp.CurrentTransferAmount, targetSolution.Comp.Solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, - user); - return false; - } - - // Move units from attackSolution to targetSolution - Solution removedSolution; - if (TryComp(target, out var stack)) - removedSolution = SolutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count); - else - removedSolution = SolutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount); - - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - - if (!asRefill) - SolutionContainer.Inject(target, targetSolution, removedSolution); - else - SolutionContainer.Refill(target, targetSolution, removedSolution); - - LocId msgSuccess = target == user ? "injector-component-transfer-success-message-self" : "injector-component-transfer-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterInject(injector, target); - return true; - } - - private bool TryInjectIntoBloodstream(Entity injector, Entity target, - EntityUid user) - { - // Get transfer amount. May be smaller than _transferAmount if not enough room - if (!SolutionContainer.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, - ref target.Comp.ChemicalSolution, out var chemSolution)) - { - LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, chemSolution.AvailableVolume); - if (realTransferAmount <= 0) - { - LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - // Move units from attackSolution to targetSolution - var removedSolution = SolutionContainer.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount); - - _blood.TryAddToChemicals(target.AsNullable(), removedSolution); - - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - - LocId msgSuccess = target.Owner == user ? "injector-component-inject-success-message-self" : "injector-component-inject-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterInject(injector, target); - return true; - } - - private bool TryDraw(Entity injector, Entity target, - Entity targetSolution, EntityUid user) - { - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) || solution.AvailableVolume == 0) - { - return false; - } - - var applicableTargetSolution = targetSolution.Comp.Solution; - // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily - var temporarilyRemovedSolution = new Solution(); - if (injector.Comp.ReagentWhitelist is { } reagentWhitelist) - { - temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray()); - } - - // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector - var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, applicableTargetSolution.Volume, - solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - // We have some snowflaked behavior for streams. - if (target.Comp != null) - { - DrawFromBlood(injector, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount, user); - return true; - } - - // Move units from attackSolution to targetSolution - var removedSolution = SolutionContainer.Draw(target.Owner, targetSolution, realTransferAmount); - - // Add back non-whitelisted reagents to the target solution - SolutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution); - - if (!SolutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution)) - { - return false; - } - - LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterDraw(injector, target); - return true; - } - - private void DrawFromBlood(Entity injector, Entity target, - Entity injectorSolution, FixedPoint2 transferAmount, EntityUid user) - { - var drawAmount = (float)transferAmount; - - if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, - ref target.Comp.ChemicalSolution)) - { - var chemTemp = SolutionContainer.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f); - SolutionContainer.TryAddSolution(injectorSolution, chemTemp); - drawAmount -= (float)chemTemp.Volume; - } - - if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, - ref target.Comp.BloodSolution)) - { - var bloodTemp = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, drawAmount); - SolutionContainer.TryAddSolution(injectorSolution, bloodTemp); - } - - LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("amount", transferAmount), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterDraw(injector, target); - } - - private void AfterInject(Entity injector, EntityUid target) - { - // Automatically set syringe to draw after completely draining it. - if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) && solution.Volume == 0) - { - SetMode(injector, InjectorToggleMode.Draw); - } - - // Leave some DNA from the injectee on it - _forensics.TransferDna(injector, target); - } - - private void AfterDraw(Entity injector, EntityUid target) - { - // Automatically set syringe to inject after completely filling it. - if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) && solution.AvailableVolume == 0) - { - SetMode(injector, InjectorToggleMode.Inject); - } - - // Leave some DNA from the drawee on it - _forensics.TransferDna(injector, target); - } - - /// - /// Toggle the injector between draw/inject state if applicable. - /// - public void Toggle(Entity injector, EntityUid user) - { - if (injector.Comp.InjectOnly) - return; - - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) - return; - - string msg; - - switch (injector.Comp.ToggleState) - { - case InjectorToggleMode.Inject: - if (solution.AvailableVolume > 0) // If solution has empty space to fill up, allow toggling to draw - { - SetMode(injector, InjectorToggleMode.Draw); - msg = "injector-component-drawing-text"; - } - else - { - msg = "injector-component-cannot-toggle-draw-message"; - } - break; - case InjectorToggleMode.Draw: - if (solution.Volume > 0) // If solution has anything in it, allow toggling to inject - { - SetMode(injector, InjectorToggleMode.Inject); - msg = "injector-component-injecting-text"; - } - else - { - msg = "injector-component-cannot-toggle-inject-message"; - } - break; - default: - throw new ArgumentOutOfRangeException(); - } - - _popup.PopupClient(Loc.GetString(msg), injector, user); - } - - /// - /// Set the mode of the injector to draw or inject. - /// - public void SetMode(Entity injector, InjectorToggleMode mode) - { - injector.Comp.ToggleState = mode; - Dirty(injector); - } -} diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs index c8e8e89ce53..3128623fc34 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerMixerSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Power.EntitySystems; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -21,6 +22,7 @@ public abstract class SharedSolutionContainerMixerSystem : EntitySystem [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly SharedPowerStateSystem _powerState = default!; /// public override void Initialize() @@ -74,6 +76,7 @@ public void TryStartMix(Entity entity, EntityUi comp.MixingSoundEntity = _audio.PlayPvs(comp.MixingSound, entity, comp.MixingSound?.Params.WithLoop(true)); comp.MixTimeEnd = _timing.CurTime + comp.MixDuration; _appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, true); + _powerState.SetWorkingState(entity.Owner, true); Dirty(uid, comp); } @@ -86,6 +89,7 @@ public void StopMix(Entity entity) _appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, false); comp.Mixing = false; comp.MixingSoundEntity = null; + _powerState.SetWorkingState(entity.Owner, false); Dirty(uid, comp); } diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Capabilities.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Capabilities.cs index cc511f8db92..aa4c59e89fb 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Capabilities.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Capabilities.cs @@ -41,7 +41,7 @@ public bool TryGetExtractableSolution(Entity entity, [NotNullWhen(true)] out Entity? soln, [NotNullWhen(true)] out Solution? solution) diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs index e6e96fa80a3..11122415c5e 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs @@ -8,19 +8,20 @@ namespace Content.Shared.Chemistry.EntitySystems; #region Events /// -/// This event alerts system that the solution was changed +/// Raised on the container of the solution entity when the contained solution is changed. +/// If you want to subscribe with the solution entity itself +/// then use instead. /// +/// +/// This is always raised on the client when handling the component state so that we can update UIs accordingly. +/// You might need an IGameTiming.ApplyingState guard to prevent mispredicts if the changes from your subscription are +/// networked with the same game state. +/// [ByRefEvent] -public record struct SolutionContainerChangedEvent +public record struct SolutionContainerChangedEvent(Solution Solution, string SolutionId) { - public readonly Solution Solution; - public readonly string SolutionId; - - public SolutionContainerChangedEvent(Solution solution, string solutionId) - { - SolutionId = solutionId; - Solution = solution; - } + public readonly Solution Solution = Solution; + public readonly string SolutionId = SolutionId; } /// @@ -89,10 +90,14 @@ protected virtual void OnSolutionChanged(Entity enti var (solutionId, solutionComp) = args.Solution; var solution = solutionComp.Solution; - UpdateAppearance(entity.Comp.Container, (solutionId, solutionComp, entity.Comp)); - var relayEvent = new SolutionContainerChangedEvent(solution, entity.Comp.ContainerName); RaiseLocalEvent(entity.Comp.Container, ref relayEvent); + + // The appearance changes are already networked as part of the same game state. + if (_timing.ApplyingState) + return; + + UpdateAppearance(entity.Comp.Container, (solutionId, solutionComp, entity.Comp)); } protected virtual void OnSolutionOverflow(Entity entity, ref SolutionOverflowEvent args) diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index fa4599da4f7..8f5010bcc5e 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; -using System.Text; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Reaction; @@ -10,17 +9,17 @@ using Content.Shared.Containers; using Content.Shared.Examine; using Content.Shared.FixedPoint; -using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Localizations; -using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Containers; +using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; using Robust.Shared.Utility; using Dependency = Robust.Shared.IoC.DependencyAttribute; using Content.Shared.Imperial.Medieval.ChemistryRandomization; @@ -29,9 +28,15 @@ namespace Content.Shared.Chemistry.EntitySystems; /// /// The event raised whenever a solution entity is modified. +/// Raised on the solution entity itself. +/// If you want to subscribe with the entity containing the solution entity +/// then use instead. /// /// /// Raised after chemcial reactions and are handled. +/// This is always raised on the client when handling the component state so that we can update UIs accordingly. +/// You might need an IGameTiming.ApplyingState guard to prevent mispredicts if the changes from your subscription are +/// networked with the same game state. /// /// The solution entity that has been modified. [ByRefEvent] @@ -74,6 +79,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; [Dependency] protected readonly MetaDataSystem MetaDataSys = default!; [Dependency] protected readonly INetManager NetManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { @@ -81,6 +87,8 @@ public override void Initialize() InitializeRelays(); + SubscribeLocalEvent(OnSolutionGetState); + SubscribeLocalEvent(OnSolutionHandleState); SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnSolutionStartup); SubscribeLocalEvent(OnSolutionShutdown); @@ -96,6 +104,22 @@ public override void Initialize() } } + private void OnSolutionGetState(Entity ent, ref ComponentGetState args) + { + args.State = new SolutionComponentState(ent.Comp.Solution); + } + + private void OnSolutionHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not SolutionComponentState cast) + return; + + ent.Comp.Solution = cast.Solution.Clone(); + + // Always raise the event on the client so that we can update UIs accordingly. + var changedEv = new SolutionChangedEvent(ent); + RaiseLocalEvent(ent, ref changedEv); + } /// /// Attempts to resolve a solution associated with an entity. @@ -435,6 +459,15 @@ public void SetCapacity(Entity soln, FixedPoint2 capacity) UpdateChemicals(soln); } + /// + /// Sets whether or not the given solution entity can react and dirties it. + /// + public void SetCanReact(Entity soln, bool canReact) + { + soln.Comp.Solution.CanReact = canReact; + UpdateChemicals(soln); + } + /// /// Adds reagent of an Id to the container. /// @@ -589,7 +622,7 @@ public bool TryTransferSolution(Entity soln, Solution source, /// Adds a solution to the container, if it can fully fit. /// /// entity holding targetSolution - /// entity holding targetSolution + /// entity holding targetSolution /// solution being added /// If the solution could be added. public bool TryAddSolution(Entity soln, Solution toAdd) @@ -607,40 +640,44 @@ public bool TryAddSolution(Entity soln, Solution toAdd) } /// - /// Adds as much of a solution to a container as can fit. + /// Adds as much of a solution to a container as can fit and updates the container. /// /// The entity containing /// The solution being added to. - /// The solution being added to + /// The solution being added to . This solution is not modified. /// The quantity of the solution actually added. public FixedPoint2 AddSolution(Entity soln, Solution toAdd) { - var (uid, comp) = soln; - var solution = comp.Solution; + var solution = soln.Comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return FixedPoint2.Zero; var quantity = FixedPoint2.Max(FixedPoint2.Zero, FixedPoint2.Min(toAdd.Volume, solution.AvailableVolume)); if (quantity < toAdd.Volume) - TryTransferSolution(soln, toAdd, quantity); + { + // TODO: This should be made into a function that directly transfers reagents. + // Currently this is quite inefficient. + solution.AddSolution(toAdd.Clone().SplitSolution(quantity), PrototypeManager); + } else - ForceAddSolution(soln, toAdd); + solution.AddSolution(toAdd, PrototypeManager); + UpdateChemicals(soln); return quantity; } /// /// Adds a solution to a container and updates the container. + /// This can exceed the maximum volume of the solution added to. /// /// The entity containing /// The solution being added to. - /// The solution being added to + /// The solution being added to . This solution is not modified. /// Whether any reagents were added to the solution. public bool ForceAddSolution(Entity soln, Solution toAdd) { - var (uid, comp) = soln; - var solution = comp.Solution; + var solution = soln.Comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return false; @@ -708,6 +745,7 @@ public Solution RemoveEachReagent(Entity soln, FixedPoint2 qu } // Thermal energy and temperature management. + // TODO: ENERGY CONSERVATION!!! Nuke this once we have HeatContainers and use methods which properly conserve energy and model heat transfer correctly! #region Thermal Energy and Temperature @@ -764,6 +802,26 @@ public void AddThermalEnergy(Entity soln, float thermalEnergy UpdateChemicals(soln); } + /// + /// Same as but clamps the value between two temperature values. + /// + /// Solution we're adjusting the energy of + /// Thermal energy we're adding or removing + /// Min desired temperature + /// Max desired temperature + public void AddThermalEnergyClamped(Entity soln, float thermalEnergy, float min, float max) + { + var solution = soln.Comp.Solution; + + if (thermalEnergy == 0.0f) + return; + + var heatCap = solution.GetHeatCapacity(PrototypeManager); + var deltaT = thermalEnergy / heatCap; + solution.Temperature = Math.Clamp(solution.Temperature + deltaT, min, max); + UpdateChemicals(soln); + } + #endregion Thermal Energy and Temperature #region Event Handlers @@ -1194,7 +1252,7 @@ private Entity SpawnSolutionUnini var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name }; AddComp(uid, relation); - MetaDataSys.SetEntityName(uid, $"solution - {name}"); + MetaDataSys.SetEntityName(uid, $"solution - {name}", raiseEvents: false); ContainerSystem.Insert(uid, container, force: true); return (uid, solution, relation); diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs index b0f55a32720..3928ff71790 100644 --- a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs @@ -1,19 +1,19 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; -using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Database; +using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Verbs; -using Robust.Shared.Network; -using Robust.Shared.Player; +using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.EntitySystems; /// -/// Allows an entity to transfer solutions with a customizable amount per click. -/// Also provides API for other systems. +/// Allows an entity to transfer solutions with a customizable amount -per click-. +/// Also provides , and API for other systems. /// public sealed class SolutionTransferSystem : EntitySystem { @@ -21,6 +21,10 @@ public sealed class SolutionTransferSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + + [Dependency] private readonly EntityQuery _refillableQuery = default!; + [Dependency] private readonly EntityQuery _drainableQuery = default!; /// /// Default transfer amounts for the set-transfer verb. @@ -32,28 +36,15 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent>(AddSetTransferVerbs); - SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnTransferAmountSetValueMessage); - } - - private void OnTransferAmountSetValueMessage(Entity ent, ref TransferAmountSetValueMessage message) - { - var (uid, comp) = ent; - - var newTransferAmount = FixedPoint2.Clamp(message.Value, comp.MinimumTransferAmount, comp.MaximumTransferAmount); - comp.TransferAmount = newTransferAmount; - - if (message.Actor is { Valid: true } user) - _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), uid, user); - - Dirty(uid, comp); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnSolutionDrainTransferDoAfter); + SubscribeLocalEvent(OnSolutionFillTransferDoAfter); } private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent args) { - var (uid, comp) = ent; - - if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null) + if (!args.CanAccess || !args.CanInteract || !ent.Comp.CanChangeTransferAmount || args.Hands == null) return; // Custom transfer verb @@ -66,7 +57,7 @@ private void AddSetTransferVerbs(Entity ent, ref GetV // TODO: remove server check when bui prediction is a thing Act = () => { - _ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User); + _ui.OpenUi(ent.Owner, TransferAmountUiKey.Key, @event.User); }, Priority = 1 }); @@ -76,7 +67,7 @@ private void AddSetTransferVerbs(Entity ent, ref GetV var user = args.User; foreach (var amount in DefaultTransferAmounts) { - if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount) + if (amount < ent.Comp.MinimumTransferAmount || amount > ent.Comp.MaximumTransferAmount) continue; AlternativeVerb verb = new(); @@ -84,11 +75,11 @@ private void AddSetTransferVerbs(Entity ent, ref GetV verb.Category = VerbCategory.SetTransferAmount; verb.Act = () => { - comp.TransferAmount = amount; + ent.Comp.TransferAmount = amount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user); + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), ent.Owner, user); - Dirty(uid, comp); + Dirty(ent.Owner, ent.Comp); }; // we want to sort by size, not alphabetically by the verb text. @@ -99,117 +90,301 @@ private void AddSetTransferVerbs(Entity ent, ref GetV } } + private void OnTransferAmountSetValueMessage(Entity ent, ref TransferAmountSetValueMessage message) + { + var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount); + ent.Comp.TransferAmount = newTransferAmount; + + if (message.Actor is { Valid: true } user) + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent.Owner, user); + + Dirty(ent.Owner, ent.Comp); + } + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) { if (!args.CanReach || args.Target is not {} target) return; - var (uid, comp) = ent; + // We have two cases for interaction: + // Held Drainable --> Target Refillable + // Held Refillable <-- Target Drainable - //Special case for reagent tanks, because normally clicking another container will give solution, not take it. - if (comp.CanReceive - && !HasComp(target) // target must not be refillable (e.g. Reagent Tanks) - && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable - && TryComp(uid, out var refill) - && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill)) + // In the case where the target has both Refillable and Drainable, Held --> Target takes priority. + + if (ent.Comp.CanSend + && _drainableQuery.TryComp(ent.Owner, out var heldDrainable) + && _refillableQuery.TryComp(target, out var targetRefillable) + && TryGetTransferrableSolutions((ent.Owner, heldDrainable), + (target, targetRefillable), + out var ownerSoln, + out var targetSoln, + out _)) { - var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank. + args.Handled = true; //If we reach this point, the interaction counts as handled. - // if the receiver has a smaller transfer limit, use that instead - if (refill?.MaxRefill is {} maxRefill) + var transferAmount = ent.Comp.TransferAmount; + if (targetRefillable.MaxRefill is {} maxRefill) transferAmount = FixedPoint2.Min(transferAmount, maxRefill); - var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount); - args.Handled = true; - if (transferred > 0) - { - var toTheBrim = ownerRefill.AvailableVolume == 0; - var msg = toTheBrim - ? "comp-solution-transfer-fill-fully" - : "comp-solution-transfer-fill-normal"; + var transferData = new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, target, targetSoln.Value, transferAmount); + var transferTime = targetRefillable.RefillTime + heldDrainable.DrainTime; - _popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User); - return; + if (transferTime > TimeSpan.Zero) + { + if (!CanTransfer(transferData)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionDrainTransferDoAfterEvent(transferAmount), ent.Owner, target) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + Hidden = true, + }; + _doAfter.TryStartDoAfter(doAfterArgs); } + else + { + DrainTransfer(transferData); + } + + return; } - // if target is refillable, and owner is drainable - if (comp.CanSend - && TryComp(target, out var targetRefill) - && _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _) - && _solution.TryGetDrainableSolution(uid, out ownerSoln, out _)) + if (ent.Comp.CanReceive + && _refillableQuery.TryComp(ent.Owner, out var heldRefillable) + && _drainableQuery.TryComp(target, out var targetDrainable) + && TryGetTransferrableSolutions((target, targetDrainable), + (ent.Owner, heldRefillable), + out targetSoln, + out ownerSoln, + out var solution)) { - var transferAmount = comp.TransferAmount; + args.Handled = true; //If we reach this point, the interaction counts as handled. - if (targetRefill?.MaxRefill is {} maxRefill) + var transferAmount = ent.Comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target drainable. + if (heldRefillable.MaxRefill is {} maxRefill) // if the receiver has a smaller transfer limit, use that instead transferAmount = FixedPoint2.Min(transferAmount, maxRefill); - var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount); - args.Handled = true; - if (transferred > 0) + var transferData = new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, transferAmount); + var transferTime = heldRefillable.RefillTime + targetDrainable.DrainTime; + + if (transferTime > TimeSpan.Zero) { - var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target)); - _popup.PopupClient(message, uid, args.User); + if (!CanTransfer(transferData)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionRefillTransferDoAfterEvent(transferAmount), ent.Owner, target) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + Hidden = true, + }; + _doAfter.TryStartDoAfter(doAfterArgs); } + else + { + RefillTransfer(transferData, solution); + } + } + } + + private void OnSolutionDrainTransferDoAfter(Entity ent, ref SolutionDrainTransferDoAfterEvent args) + { + if (args.Cancelled || args.Target is not { } target) + return; + + // Have to check again, in case something has changed. + if (CanSend(ent, target, out var ownerSoln, out var targetSoln)) + { + DrainTransfer(new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, args.Target.Value, targetSoln.Value, args.Amount)); } } + private void OnSolutionFillTransferDoAfter(Entity ent, ref SolutionRefillTransferDoAfterEvent args) + { + if (args.Cancelled || args.Target is not { } target) + return; + + // Have to check again, in case something has changed. + if (!CanRecieve(ent, target, out var ownerSoln, out var targetSoln, out var solution)) + return; + + RefillTransfer(new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, args.Amount), solution); + } + + private bool CanSend(Entity ent, + Entity target, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable) + { + drainable = null; + refillable = null; + + return ent.Comp1.CanReceive && TryGetTransferrableSolutions(ent.Owner, target, out drainable, out refillable, out _); + } + + private bool CanRecieve(Entity ent, + EntityUid source, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable, + [NotNullWhen(true)] out Solution? solution) + { + drainable = null; + refillable = null; + solution = null; + + return ent.Comp.CanReceive && TryGetTransferrableSolutions(source, ent.Owner, out drainable, out refillable, out solution); + } + + private bool TryGetTransferrableSolutions(Entity source, + Entity target, + [NotNullWhen(true)] out Entity? drainable, + [NotNullWhen(true)] out Entity? refillable, + [NotNullWhen(true)] out Solution? solution) + { + drainable = null; + refillable = null; + solution = null; + + if (!_drainableQuery.Resolve(source, ref source.Comp) || !_refillableQuery.Resolve(target, ref target.Comp)) + return false; + + if (!_solution.TryGetDrainableSolution(source, out drainable, out _)) + return false; + + if (!_solution.TryGetRefillableSolution(target, out refillable, out solution)) + return false; + + return true; + } + /// - /// Transfer from a solution to another, allowing either entity to cancel it and show a popup. + /// Attempt to drain a solution into another, such as pouring a bottle into a glass. + /// Includes a pop-up if the transfer failed or succeeded /// + /// The transfer data making up the transfer. /// The actual amount transferred. - public FixedPoint2 Transfer(EntityUid user, - EntityUid sourceEntity, - Entity source, - EntityUid targetEntity, - Entity target, - FixedPoint2 amount) + private void DrainTransfer(SolutionTransferData data) { - var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity); + var transferred = Transfer(data); + if (transferred <= 0) + return; + + var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", data.TargetEntity)); + _popup.PopupClient(message, data.SourceEntity, data.User); + } + + /// + /// Attempt to fill a solution from another container, such as tapping from a water tank. + /// Includes a pop-up if the transfer failed or succeeded. + /// + /// The transfer data making up the transfer. + /// The target solution,included for LoC pop-up purposes. + /// The actual amount transferred. + private void RefillTransfer(SolutionTransferData data, Solution targetSolution) + { + var transferred = Transfer(data); + if (transferred <= 0) + return; + + var toTheBrim = targetSolution.AvailableVolume == 0; + var msg = toTheBrim + ? "comp-solution-transfer-fill-fully" + : "comp-solution-transfer-fill-normal"; + + _popup.PopupClient(Loc.GetString(msg, ("owner", data.SourceEntity), ("amount", transferred), ("target", data.TargetEntity)), data.TargetEntity, data.User); + } + + /// + /// Transfer from a solution to another, allowing either entity to cancel. + /// Includes a pop-up if the transfer failed. + /// + /// The actual amount transferred. + public FixedPoint2 Transfer(SolutionTransferData data) + { + var sourceSolution = data.Source.Comp.Solution; + var targetSolution = data.Target.Comp.Solution; + + if (!CanTransfer(data)) + return FixedPoint2.Zero; + + var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); + + var solution = _solution.SplitSolution(data.Source, actualAmount); + _solution.AddSolution(data.Target, solution); + + var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount); + RaiseLocalEvent(data.TargetEntity, ref ev); + + _adminLogger.Add(LogType.Action, + LogImpact.Medium, + $"{ToPrettyString(data.User):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(data.TargetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}"); + + return actualAmount; + } + + /// + /// Check if the source solution can transfer the amount to the target solution, and display a pop-up if it fails. + /// + private bool CanTransfer(SolutionTransferData data) + { + var transferAttempt = new SolutionTransferAttemptEvent(data.SourceEntity, data.TargetEntity); // Check if the source is cancelling the transfer - RaiseLocalEvent(sourceEntity, ref transferAttempt); + RaiseLocalEvent(data.SourceEntity, ref transferAttempt); if (transferAttempt.CancelReason is {} reason) { - _popup.PopupClient(reason, sourceEntity, user); - return FixedPoint2.Zero; + _popup.PopupClient(reason, data.SourceEntity, data.User); + return false; } - var sourceSolution = source.Comp.Solution; + var sourceSolution = data.Source.Comp.Solution; if (sourceSolution.Volume == 0) { - _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user); - return FixedPoint2.Zero; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", data.SourceEntity)), data.SourceEntity, data.User); + return false; } // Check if the target is cancelling the transfer - RaiseLocalEvent(targetEntity, ref transferAttempt); + RaiseLocalEvent(data.TargetEntity, ref transferAttempt); if (transferAttempt.CancelReason is {} targetReason) { - _popup.PopupClient(targetReason, targetEntity, user); - return FixedPoint2.Zero; + _popup.PopupClient(targetReason, data.TargetEntity, data.User); + return false; } - var targetSolution = target.Comp.Solution; + var targetSolution = data.Target.Comp.Solution; if (targetSolution.AvailableVolume == 0) { - _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user); - return FixedPoint2.Zero; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", data.TargetEntity)), data.TargetEntity, data.User); + return false; } - var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); - - var solution = _solution.SplitSolution(source, actualAmount); - _solution.AddSolution(target, solution); - - var ev = new SolutionTransferredEvent(sourceEntity, targetEntity, user, actualAmount); - RaiseLocalEvent(targetEntity, ref ev); + return true; + } +} - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}"); - return actualAmount; - } +/// +/// A collection of data containing relevant entities and values for transferring reagents. +/// +/// The user performing the transfer. +/// The entity holding the solution container which reagents are being moved from. +/// The entity holding the solution from which reagents are being moved away from. +/// The entity holding the solution container which reagents are being moved to. +/// The entity holding the solution which reagents are being moved to +/// The amount being moved. +public struct SolutionTransferData(EntityUid user, EntityUid sourceEntity, Entity source, EntityUid targetEntity, Entity target, FixedPoint2 amount) +{ + public EntityUid User = user; + public EntityUid SourceEntity = sourceEntity; + public Entity Source = source; + public EntityUid TargetEntity = targetEntity; + public Entity Target = target; + public FixedPoint2 Amount = amount; } /// @@ -234,3 +409,35 @@ public void Cancel(string reason) /// [ByRefEvent] public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount); + +/// +/// Doafter event for solution transfers where the held item is drained into the target. Checks for validity both when initiating and when finishing the event. +/// +[Serializable, NetSerializable] +public sealed partial class SolutionDrainTransferDoAfterEvent : DoAfterEvent +{ + public FixedPoint2 Amount; + + public SolutionDrainTransferDoAfterEvent(FixedPoint2 amount) + { + Amount = amount; + } + + public override DoAfterEvent Clone() => this; +} + +/// +/// Doafter event for solution transfers where the held item is filled from the target. Checks for validity both when initiating and when finishing the event. +/// +[Serializable, NetSerializable] +public sealed partial class SolutionRefillTransferDoAfterEvent : DoAfterEvent +{ + public FixedPoint2 Amount; + + public SolutionRefillTransferDoAfterEvent(FixedPoint2 amount) + { + Amount = amount; + } + + public override DoAfterEvent Clone() => this; +} diff --git a/Content.Shared/Chemistry/Events/HyposprayEvents.cs b/Content.Shared/Chemistry/Events/HyposprayEvents.cs deleted file mode 100644 index 33293a40498..00000000000 --- a/Content.Shared/Chemistry/Events/HyposprayEvents.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Shared.Inventory; - -namespace Content.Shared.Chemistry.Hypospray.Events; - -public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent -{ - public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; - public EntityUid EntityUsingHypospray; - public readonly EntityUid Hypospray; - public EntityUid TargetGettingInjected; - public string? InjectMessageOverride; - - public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target) - { - EntityUsingHypospray = user; - Hypospray = hypospray; - TargetGettingInjected = target; - InjectMessageOverride = null; - } -} - -/// -/// This event is raised on the user using the hypospray before the hypospray is injected. -/// The event is triggered on the user and all their clothing. -/// -public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent -{ - public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } -} - -/// -/// This event is raised on the target before the hypospray is injected. -/// The event is triggered on the target itself and all its clothing. -/// -public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent -{ - public TargetBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } -} diff --git a/Content.Shared/Chemistry/Events/InjectorEvents.cs b/Content.Shared/Chemistry/Events/InjectorEvents.cs new file mode 100644 index 00000000000..ce4ccb88f17 --- /dev/null +++ b/Content.Shared/Chemistry/Events/InjectorEvents.cs @@ -0,0 +1,43 @@ +using Content.Shared.DoAfter; +using Content.Shared.Inventory; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry.Events; + +/// +/// Raised on the injector when the doafter has finished. +/// +[Serializable, NetSerializable] +public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent; + +/// +/// The base injection attempt event. It'll be raised on the user and target when attempting to inject the target. +/// +/// The user who is trying to inject the target. +/// The injector being used by the user. +/// The target who the user is trying to inject. +/// The resulting message that gets displayed per popup. +public abstract partial class BeforeInjectTargetEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public EntityUid EntityUsingInjector = user; + public readonly EntityUid UsedInjector = usedInjector; + public EntityUid TargetGettingInjected = target; + public string? OverrideMessage = overrideMessage; + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// This event is raised on the user using the injector before the injector is injected. +/// The event is triggered on the user and all their clothing. +/// +public sealed class SelfBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage); + +/// +/// This event is raised on the target before the injector is injected. +/// The event is triggered on the target itself and all its clothing. +/// +[ByRefEvent] +public sealed class TargetBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage); diff --git a/Content.Shared/Chemistry/MetabolismMovespeedModifierSystem.cs b/Content.Shared/Chemistry/MetabolismMovespeedModifierSystem.cs deleted file mode 100644 index 68a55dd468a..00000000000 --- a/Content.Shared/Chemistry/MetabolismMovespeedModifierSystem.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Movement.Systems; -using Robust.Shared.Timing; - -namespace Content.Shared.Chemistry -{ - // TODO CONVERT THIS TO A STATUS EFFECT!!!!!!!!!!!!!!!!!!!!!!!! - public sealed class MetabolismMovespeedModifierSystem : EntitySystem - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly MovementSpeedModifierSystem _movespeed = default!; - - private readonly List> _components = new(); - - public override void Initialize() - { - base.Initialize(); - - UpdatesOutsidePrediction = true; - - SubscribeLocalEvent(AddComponent); - SubscribeLocalEvent(OnRefreshMovespeed); - } - - private void OnRefreshMovespeed(EntityUid uid, MovespeedModifierMetabolismComponent component, RefreshMovementSpeedModifiersEvent args) - { - args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); - } - - private void AddComponent(Entity metabolism, ref ComponentStartup args) - { - _components.Add(metabolism); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var currentTime = _gameTiming.CurTime; - - for (var i = _components.Count - 1; i >= 0; i--) - { - var metabolism = _components[i]; - - if (metabolism.Comp.Deleted) - { - _components.RemoveAt(i); - continue; - } - - if (metabolism.Comp.ModifierTimer > currentTime) - continue; - - _components.RemoveAt(i); - RemComp(metabolism); - - _movespeed.RefreshMovementSpeedModifiers(metabolism); - } - } - } -} diff --git a/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs b/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs new file mode 100644 index 00000000000..0255d8798f5 --- /dev/null +++ b/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs @@ -0,0 +1,145 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared.Chemistry.Prototypes; + +/// +/// This defines the behavior of an injector. +/// Every injector requires this and it defines how much an injector injects, what transferamounts they can switch between, etc. +/// +[Prototype] +public sealed partial class InjectorModePrototype : IPrototype, IInheritingPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; + + /// + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + /// + [AbstractDataField, NeverPushInheritance] + public bool Abstract { get; private set; } + + /// + /// The name of the mode that will be shown on the label UI. + /// + [DataField(required: true)] + public LocId Name; + + /// + /// If true, it'll inject the user when used in hand (Default Key: Y/Z) + /// + [DataField] + public bool InjectOnUse; + + /// + /// The transfer amounts for the set-transfer verb. + /// + [DataField] + public List TransferAmounts = new() { 1, 5, 10, 15 }; + + /// + /// Injection/Drawing delay (seconds) when the target is a mob. + /// + [DataField] + public TimeSpan MobTime = TimeSpan.FromSeconds(5); + + /// + /// The delay to draw Reagents from Containers. + /// If set, RefillTime should probably have the same value. + /// + [DataField] + public TimeSpan ContainerDrawTime = TimeSpan.Zero; + + + /// + /// The number to multiply and if the target is the downed. + /// Downed counts as crouching, buckled on a bed or critical. + /// + [DataField] + public float DownedModifier = 0.5f; + + /// + /// The number to multiply and if the target is the user. + /// + [DataField] + public float SelfModifier = 0.5f; + + /// + /// This delay will increase the DoAfter time for each Xu above . + /// + [DataField] + public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1); + + /// + /// This works in tandem with . + /// + [DataField] + public FixedPoint2 IgnoreDelayForVolume = FixedPoint2.New(5); + + /// + /// What message will be displayed to the user when attempting to inject someone. + /// + /// + /// This is used for when you aren't injecting with a needle or an instant hypospray. + /// It would be weird if someone injects with a spray, but the popup says "needle". + /// + [DataField] + public LocId PopupUserAttempt = "injector-component-needle-injecting-user"; + + /// + /// What message will be displayed to the target when someone attempts to inject into them. + /// + [DataField] + public LocId PopupTargetAttempt = "injector-component-needle-injecting-target"; + + /// + /// The state of the injector. Determines its attack behavior. Containers must have the + /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should + /// only ever be set to Inject + /// + [DataField] + public InjectorBehavior Behavior = InjectorBehavior.Inject; + + /// + /// Sound that will be played when injecting. + /// + [DataField] + public SoundSpecifier? InjectSound; + + /// + /// A popup for the target upon a successful injection. + /// It's imperative that this is not null when is instant. + /// + [DataField] + public LocId? InjectPopupTarget; + +} + +/// +/// Possible modes for an . +/// +[Serializable, NetSerializable, Flags] +public enum InjectorBehavior +{ + /// + /// The injector will try to inject reagent into things. + /// + Inject = 1 << 0, + + /// + /// The injector will try to draw reagent from things. + /// + Draw = 1 << 1, + + /// + /// The injector will draw from containers and inject into mobs. + /// + Dynamic = 1 << 2, +} diff --git a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs index 678dc682dca..c3ada9801ca 100644 --- a/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/ChemicalReactionSystem.cs @@ -6,11 +6,11 @@ using Content.Shared.Database; using Content.Shared.EntityEffects; using Content.Shared.FixedPoint; -using Content.Shared.Imperial.Medieval.ChemistryRandomization; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Utility; +using Content.Shared.Imperial.Medieval.ChemistryRandomization; // Imperial Medieval namespace Content.Shared.Chemistry.Reaction @@ -32,7 +32,7 @@ public sealed class ChemicalReactionSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly SharedChemistryRandomizationSystem _chemRandom = default!; + [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; /// /// A cache of all reactions indexed by at most ONE of their required reactants. @@ -207,27 +207,13 @@ private List PerformReaction(Entity soln, ReactionPro private void OnReaction(Entity soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions) { - var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f); - var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos); _adminLogger.Add(LogType.ChemicalReaction, reaction.Impact, $"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}"); - foreach (var effect in reaction.Effects) - { - if (!effect.ShouldApply(args)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}"); - } - - effect.Effect(args); - } + // Imperial Medieval: ReactionData.Effects is List<>; public ApplyEffects expects array + _entityEffects.ApplyEffects(soln, reaction.Effects.ToArray(), unitReactions); // Someday, some brave soul will thread through an optional actor // argument in from every call of OnReaction up, all just to pass @@ -280,8 +266,6 @@ private bool ProcessReactions(Entity soln, SortedSet public void FullyReactSolution(Entity soln, ReactionMixerComponent? mixerComponent = null) { - _chemRandom.FullyReactSolution(soln, mixerComponent); // Imperial Medieval chemistry randomization - // construct the initial set of reactions to check. SortedSet reactions = new(); foreach (var reactant in soln.Comp.Solution.Contents) @@ -309,9 +293,10 @@ public void FullyReactSolution(Entity soln, ReactionMixerComp /// Some solution containers (e.g., bloodstream, smoke, foam) use this to block certain reactions from occurring. /// [ByRefEvent] - public record struct ReactionAttemptEvent(ReactionData Reaction, Entity Solution) // Imperial Medieval - ReactionData + // Imperial Medieval: ReactionData so randomized reactions (non-prototype) can raise the same event + public record struct ReactionAttemptEvent(ReactionData Reaction, Entity Solution) { - public readonly ReactionData Reaction = Reaction; // Imperial Medieval - ReactionData + public readonly ReactionData Reaction = Reaction; public readonly Entity Solution = Solution; public bool Cancelled = false; } diff --git a/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs b/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs index 8edfa44ce85..247b203e218 100644 --- a/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs +++ b/Content.Shared/Chemistry/Reaction/ReactionMixerComponent.cs @@ -1,48 +1,61 @@ -using Content.Shared.Chemistry.Components; using Content.Shared.DoAfter; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.Reaction; -[RegisterComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ReactionMixerComponent : Component { /// - /// A list of IDs for categories of reactions that can be mixed (i.e. HOLY for a bible, DRINK for a spoon) + /// A list of IDs for categories of reactions that can be mixed (i.e. HOLY for a bible, DRINK for a spoon). /// - [ViewVariables] - [DataField] - public List> ReactionTypes = default!; + [DataField, AutoNetworkedField] + public List> ReactionTypes = new(); /// - /// A string which identifies the string to be sent when successfully mixing a solution + /// The popup message when successfully mixing a solution. /// - [ViewVariables] - [DataField] + [DataField, AutoNetworkedField] public LocId MixMessage = "default-mixing-success"; /// - /// Defines if interacting is enough to mix with this component + /// The sound to play when mixing. /// - [ViewVariables] [DataField] - public bool MixOnInteract = true; + public SoundSpecifier? MixingSound; /// - /// How long it takes to mix with this + /// Defines if interacting is enough to mix with this component. /// - [ViewVariables] - [DataField] + [DataField, AutoNetworkedField] + public ReactionMixerType MixerType = ReactionMixerType.Machine; + + /// + /// How long it takes to mix with this. + /// + [DataField, AutoNetworkedField] public TimeSpan TimeToMix = TimeSpan.Zero; + + // Used to cancel the played sound. + public EntityUid? AudioStream; +} + +[Serializable, NetSerializable] +public enum ReactionMixerType +{ + None, // Mixing is handled by its own system. + Machine, // Mixing is handled via interaction. + Handheld // Mixing is handled via using in hand } [ByRefEvent] public record struct MixingAttemptEvent(EntityUid Mixed, bool Cancelled = false); +[ByRefEvent] public readonly record struct AfterMixingEvent(EntityUid Mixed, EntityUid Mixer); [Serializable, NetSerializable] -public sealed partial class ReactionMixDoAfterEvent : SimpleDoAfterEvent -{ -} +public sealed partial class ReactionMixDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Chemistry/Reaction/ReactionMixerSystem.cs b/Content.Shared/Chemistry/Reaction/ReactionMixerSystem.cs new file mode 100644 index 00000000000..34c94f2cb75 --- /dev/null +++ b/Content.Shared/Chemistry/Reaction/ReactionMixerSystem.cs @@ -0,0 +1,155 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.DoAfter; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Interaction.Events; +using Content.Shared.Popups; +using Robust.Shared.Audio.Systems; + +namespace Content.Shared.Chemistry.Reaction; + +public sealed partial class ReactionMixerSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnAfterInteract, before: [typeof(IngestionSystem)]); + SubscribeLocalEvent(OnShake); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnUseInHand(Entity ent, ref UseInHandEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.MixerType != ReactionMixerType.Handheld) + return; + + args.Handled = true; + + if (!CanMix(ent.AsNullable(), ent)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, + args.User, + ent.Comp.TimeToMix, + new ReactionMixDoAfterEvent(), + ent, + ent, + ent) + { + NeedHand = true, + BreakOnDamage = true, + BreakOnDropItem = true, + BreakOnHandChange = true, + BreakOnMove = true + }; + + if (_doAfter.TryStartDoAfter(doAfterArgs)) + ent.Comp.AudioStream = _audio.PlayPredicted(ent.Comp.MixingSound, ent, args.User)?.Entity ?? ent.Comp.AudioStream; + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (!args.Target.HasValue || !args.CanReach || ent.Comp.MixerType != ReactionMixerType.Machine) + return; + + if (!CanMix(ent.AsNullable(), args.Target.Value)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, ent.Comp.TimeToMix, new ReactionMixDoAfterEvent(), ent, args.Target.Value, ent); + + if (_doAfter.TryStartDoAfter(doAfterArgs)) + ent.Comp.AudioStream = _audio.PlayPredicted(ent.Comp.MixingSound, ent, args.User)?.Entity ?? ent.Comp.AudioStream; + + args.Handled = true; + } + + private void OnDoAfter(Entity ent, ref ReactionMixDoAfterEvent args) + { + ent.Comp.AudioStream = _audio.Stop(ent.Comp.AudioStream); + + if (args.Cancelled) + return; + + if (args.Target == null) + return; + + if (!TryMix(ent.AsNullable(), args.Target.Value)) + return; + + _popup.PopupClient( + Loc.GetString(ent.Comp.MixMessage, + ("mixed", Identity.Entity(args.Target.Value, EntityManager)), + ("mixer", Identity.Entity(ent.Owner, EntityManager))), + args.User, + args.User); + } + + private void OnShake(Entity ent, ref ShakeEvent args) + { + TryMix(ent.AsNullable(), ent); + } + + /// + /// Returns true if given reaction mixer is able to mix the solution inside the target entity, false otherwise. + /// + /// The reaction mixer used to cause the reaction. + /// The target solution container with a . + public bool CanMix(Entity ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) // The used entity needs the component to be able to mix a solution + return false; + + if (!_solutionContainer.TryGetMixableSolution(target, out _, out var mixableSolution)) + return false; + + // Can't mix nothing. + if (mixableSolution.Volume <= 0) + return false; + + var mixAttemptEvent = new MixingAttemptEvent(ent); + RaiseLocalEvent(ent, ref mixAttemptEvent); + if (mixAttemptEvent.Cancelled) + return false; + + return true; + } + + /// + /// Attempts to mix the solution inside the target entity using the given reaction mixer. + /// + /// The reaction mixer used to cause the reaction. + /// The target solution container with a . + /// If the reaction mixer was able to mix the solution. This does not necessarily mean a reaction took place. + public bool TryMix(Entity ent, EntityUid target) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + var mixAttemptEvent = new MixingAttemptEvent(ent); + RaiseLocalEvent(ent, ref mixAttemptEvent); + if (mixAttemptEvent.Cancelled) + return false; + + if (!_solutionContainer.TryGetMixableSolution(target, out var solutionEnt, out _)) + return false; + + _solutionContainer.UpdateChemicals(solutionEnt.Value, true, ent.Comp); + + var afterMixingEvent = new AfterMixingEvent(ent, target); + RaiseLocalEvent(ent, ref afterMixingEvent); + + return true; + } +} diff --git a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs index cabdee93c1a..89fcca900eb 100644 --- a/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs +++ b/Content.Shared/Chemistry/Reaction/ReactiveComponent.cs @@ -34,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry public HashSet? Reagents = null; [DataField("effects", required: true)] - public List Effects = default!; + public EntityEffect[] Effects = default!; [DataField("groups", readOnly: true, serverOnly: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer, ReactiveGroupPrototype>))] diff --git a/Content.Shared/Chemistry/ReactiveSystem.cs b/Content.Shared/Chemistry/ReactiveSystem.cs index 6306537324e..2ffb848f8a7 100644 --- a/Content.Shared/Chemistry/ReactiveSystem.cs +++ b/Content.Shared/Chemistry/ReactiveSystem.cs @@ -1,108 +1,35 @@ -using Content.Shared.Administration.Logs; using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Database; -using Content.Shared.EntityEffects; +using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Chemistry; [UsedImplicitly] public sealed class ReactiveSystem : EntitySystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method) { foreach (var reagent in solution.Contents.ToArray()) { - ReactionEntity(uid, method, reagent, solution); + ReactionEntity(uid, method, reagent); } } - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source) + public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity) { - // We throw if the reagent specified doesn't exist. - var proto = _prototypeManager.Index(reagentQuantity.Reagent.Prototype); - ReactionEntity(uid, method, proto, reagentQuantity, source); - } + if (reagentQuantity.Quantity == FixedPoint2.Zero) + return; - public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto, - ReagentQuantity reagentQuantity, Solution? source) - { - if (!TryComp(uid, out ReactiveComponent? reactive)) + // We throw if the reagent specified doesn't exist. + if (!_proto.Resolve(reagentQuantity.Reagent.Prototype, out var proto)) return; - // custom event for bypassing reactivecomponent stuff - var ev = new ReactionEntityEvent(method, proto, reagentQuantity, source); + var ev = new ReactionEntityEvent(method, reagentQuantity, proto); RaiseLocalEvent(uid, ref ev); - - // If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified. - var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f); - - // First, check if the reagent wants to apply any effects. - if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null) - { - foreach (var (key, val) in proto.ReactiveEffects) - { - if (!val.Methods.Contains(method)) - continue; - - if (!reactive.ReactiveGroups.ContainsKey(key)) - continue; - - if (!reactive.ReactiveGroups[key].Contains(method)) - continue; - - foreach (var effect in val.Effects) - { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); - } - } - } - - // Then, check if the prototype has any effects it can apply as well. - if (reactive.Reactions != null) - { - foreach (var entry in reactive.Reactions) - { - if (!entry.Methods.Contains(method)) - continue; - - if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID)) - continue; - - foreach (var effect in entry.Effects) - { - if (!effect.ShouldApply(args, _robustRandom)) - continue; - - if (effect.ShouldLog) - { - var entity = args.TargetEntity; - _adminLogger.Add(LogType.ReagentEffect, effect.LogImpact, - $"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}"); - } - - effect.Effect(args); - } - } - } } } public enum ReactionMethod @@ -113,9 +40,4 @@ public enum ReactionMethod } [ByRefEvent] -public readonly record struct ReactionEntityEvent( - ReactionMethod Method, - ReagentPrototype Reagent, - ReagentQuantity ReagentQuantity, - Solution? Source -); +public readonly record struct ReactionEntityEvent(ReactionMethod Method, ReagentQuantity ReagentQuantity, ReagentPrototype Reagent); diff --git a/Content.Shared/Chemistry/Reagent/DNAData.cs b/Content.Shared/Chemistry/Reagent/DNAData.cs index e75e994eceb..5a88c0b41cf 100644 --- a/Content.Shared/Chemistry/Reagent/DNAData.cs +++ b/Content.Shared/Chemistry/Reagent/DNAData.cs @@ -1,4 +1,3 @@ -using Content.Shared.FixedPoint; using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.Reagent; @@ -7,9 +6,15 @@ namespace Content.Shared.Chemistry.Reagent; public sealed partial class DnaData : ReagentData { [DataField] - public string DNA = String.Empty; + public string DNA = string.Empty; - public override ReagentData Clone() => this; + public override ReagentData Clone() + { + return new DnaData + { + DNA = DNA, + }; + } public override bool Equals(ReagentData? other) { diff --git a/Content.Shared/Chemistry/Reagent/ReagentMetabolismsSerializer.cs b/Content.Shared/Chemistry/Reagent/ReagentMetabolismsSerializer.cs new file mode 100644 index 00000000000..cf1cbd57880 --- /dev/null +++ b/Content.Shared/Chemistry/Reagent/ReagentMetabolismsSerializer.cs @@ -0,0 +1,31 @@ +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Content.Shared.Chemistry.Reagent; + +[TypeSerializer] +public sealed class ReagentMetabolismsSerializer : ITypeInheritanceHandler +{ + public MappingDataNode PushInheritance( + ISerializationManager serializationManager, + MappingDataNode child, + MappingDataNode parent, + IDependencyCollection dependencies, + ISerializationContext? context) + { + var result = child.Copy(); + + foreach (var (k, v) in parent) + { + if (result.TryAddCopy(k, v)) + continue; + + result[k] = serializationManager.CombineMappings( + (MappingDataNode)result[k], + (MappingDataNode)v); + } + + return result; + } +} diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index 9101b4d3f4d..ad0bfcf68ea 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -2,23 +2,20 @@ using System.Linq; using Content.Shared.FixedPoint; using System.Text.Json.Serialization; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using Content.Shared.Contraband; using Content.Shared.EntityEffects; -using Content.Shared.Database; +using Content.Shared.Localizations; +using Content.Shared.Metabolism; using Content.Shared.Nutrition; -using Content.Shared.Prototypes; using Content.Shared.Roles; using Content.Shared.Slippery; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; using Robust.Shared.Utility; namespace Content.Shared.Chemistry.Reagent @@ -174,8 +171,8 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype [DataField] public bool WorksOnTheDead; - [DataField] - public FrozenDictionary, ReagentEffectsEntry>? Metabolisms; + [DataField, AlwaysPushInheritance] + public ReagentMetabolisms? Metabolisms; [DataField] public Dictionary, ReactiveReagentEffectEntry>? ReactiveEffects; @@ -198,6 +195,7 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype [DataField] public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepPuddle"); + // TODO: Reaction tile doesn't work properly and destroys reagents way too quickly public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager, List? data) { var removed = FixedPoint2.Zero; @@ -219,33 +217,32 @@ public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityMa return removed; } - public void ReactionPlant(EntityUid? plantHolder, - ReagentQuantity amount, - Solution solution, - EntityManager entityManager, - IRobustRandom random, - ISharedAdminLogManager logger) + public IEnumerable GuidebookReagentEffectsDescription(IPrototypeManager prototype, IEntitySystemManager entSys, IEnumerable effects, FixedPoint2 metabolism) { - if (plantHolder == null) - return; + return effects.Select(x => GuidebookReagentEffectDescription(prototype, entSys, x, metabolism)) + .Where(x => x is not null) + .Select(x => x!) + .ToArray(); + } - var args = new EntityEffectReagentArgs(plantHolder.Value, entityManager, null, solution, amount.Quantity, this, null, 1f); - foreach (var plantMetabolizable in PlantMetabolisms) - { - if (!plantMetabolizable.ShouldApply(args, random)) - continue; - - if (plantMetabolizable.ShouldLog) - { - var entity = args.TargetEntity; - logger.Add( - LogType.ReagentEffect, - plantMetabolizable.LogImpact, - $"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID} applied on entity {entity}"); - } - - plantMetabolizable.Effect(args); - } + public string? GuidebookReagentEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys, EntityEffect effect, FixedPoint2 metabolism) + { + if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description) + return null; + + var quantity = (double)(effect.MinScale * metabolism); + + return Loc.GetString( + "guidebook-reagent-effect-description", + ("reagent", LocalizedName), + ("quantity", quantity), + ("effect", description), + ("chance", effect.Probability), + ("conditionCount", effect.Conditions?.Length ?? 0), + ("conditions", + ContentLocalizationManager.FormatList( + effect.Conditions?.Select(x => x.EntityConditionGuidebookText(prototype)).ToList() ?? new List() + ))); } } @@ -254,27 +251,30 @@ public struct ReagentGuideEntry { public string ReagentPrototype; - public Dictionary, ReagentEffectsGuideEntry>? GuideEntries; + public Dictionary, ReagentEffectsGuideEntry>? GuideEntries; public List? PlantMetabolisms = null; public ReagentGuideEntry(ReagentPrototype proto, IPrototypeManager prototype, IEntitySystemManager entSys) { ReagentPrototype = proto.ID; - GuideEntries = proto.Metabolisms? - .Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys))) + GuideEntries = proto.Metabolisms?.Metabolisms + .Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys, proto))) .ToDictionary(x => x.Key, x => x.Item2); if (proto.PlantMetabolisms.Count > 0) { - PlantMetabolisms = new List(proto.PlantMetabolisms - .Select(x => x.GuidebookEffectDescription(prototype, entSys)) - .Where(x => x is not null) - .Select(x => x!) - .ToArray()); + PlantMetabolisms = + new List(proto.GuidebookReagentEffectsDescription(prototype, entSys, proto.PlantMetabolisms, FixedPoint2.New(1f))); } } } + [DataDefinition] + public sealed partial class ReagentMetabolisms + { + [IncludeDataField(customTypeSerializer: typeof(DictionarySerializer, ReagentEffectsEntry>))] + public Dictionary, ReagentEffectsEntry> Metabolisms; + } [DataDefinition] public sealed partial class ReagentEffectsEntry @@ -283,24 +283,27 @@ public sealed partial class ReagentEffectsEntry /// Amount of reagent to metabolize, per metabolism cycle. /// [JsonPropertyName("rate")] - [DataField("metabolismRate")] + [DataField] public FixedPoint2 MetabolismRate = FixedPoint2.New(0.5f); /// /// A list of effects to apply when these reagents are metabolized. /// [JsonPropertyName("effects")] - [DataField("effects", required: true)] - public EntityEffect[] Effects = default!; + [DataField] + public EntityEffect[] Effects = Array.Empty(); - public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys) + /// + /// Ratio of this reagent to metabolites for transfer to the next solution by a metabolizer + /// + [DataField] + public Dictionary, FixedPoint2>? Metabolites; + + public string EntityEffectFormat => "guidebook-reagent-effect-description"; + + public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys, ReagentPrototype proto) { - return new ReagentEffectsGuideEntry(MetabolismRate, - Effects - .Select(x => x.GuidebookEffectDescription(prototype, entSys)) // hate. - .Where(x => x is not null) - .Select(x => x!) - .ToArray()); + return new ReagentEffectsGuideEntry(MetabolismRate, proto.GuidebookReagentEffectsDescription(prototype, entSys, Effects, MetabolismRate).ToArray(), Metabolites); } } @@ -311,10 +314,13 @@ public struct ReagentEffectsGuideEntry public string[] EffectDescriptions; - public ReagentEffectsGuideEntry(FixedPoint2 metabolismRate, string[] effectDescriptions) + public Dictionary, FixedPoint2>? Metabolites; + + public ReagentEffectsGuideEntry(FixedPoint2 metabolismRate, string[] effectDescriptions, Dictionary, FixedPoint2>? metabolites) { MetabolismRate = metabolismRate; EffectDescriptions = effectDescriptions; + Metabolites = metabolites; } } diff --git a/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs b/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs index cc5b52fbe2e..22c1a3e0e55 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentQuantity.cs @@ -8,9 +8,9 @@ namespace Content.Shared.Chemistry.Reagent; /// [Serializable, NetSerializable] [DataDefinition] -public partial struct ReagentQuantity : IEquatable +public partial struct ReagentQuantity : IEquatable, IRobustCloneable { - [DataField("Quantity", required:true)] + [DataField("Quantity", required: true)] public FixedPoint2 Quantity { get; private set; } [IncludeDataField] @@ -28,6 +28,28 @@ public ReagentQuantity(ReagentId reagent, FixedPoint2 quantity) Quantity = quantity; } + public ReagentQuantity(ReagentQuantity reagentQuantity) + { + Quantity = reagentQuantity.Quantity; + if (reagentQuantity.Reagent.Data is not { } data) + { + Reagent = new ReagentId(reagentQuantity.Reagent.Prototype, null); + return; + } + + List copy = new(data.Count); + foreach (var item in data) + { + copy.Add(item.Clone()); + } + Reagent = new ReagentId(reagentQuantity.Reagent.Prototype, copy); + } + + public readonly ReagentQuantity Clone() + { + return new ReagentQuantity(this); + } + public ReagentQuantity() : this(default, default) { } diff --git a/Content.Shared/Chemistry/SharedChemMaster.cs b/Content.Shared/Chemistry/SharedChemMaster.cs index cabee465eaa..8a71cc49f64 100644 --- a/Content.Shared/Chemistry/SharedChemMaster.cs +++ b/Content.Shared/Chemistry/SharedChemMaster.cs @@ -83,6 +83,12 @@ public ChemMasterOutputToBottleMessage(uint dosage, string label) } } + [Serializable, NetSerializable] + public sealed class ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource drawSource) : BoundUserInterfaceMessage + { + public readonly ChemMasterDrawSource DrawSource = drawSource; + } + public enum ChemMasterMode { Transfer, @@ -115,6 +121,12 @@ public enum ChemMasterReagentAmount All, } + public enum ChemMasterDrawSource + { + Internal, + External, + } + public static class ChemMasterReagentAmountToFixedPoint { public static FixedPoint2 GetFixedPoint(this ChemMasterReagentAmount amount) @@ -184,10 +196,12 @@ public sealed class ChemMasterBoundUserInterfaceState : BoundUserInterfaceState public readonly bool UpdateLabel; + public readonly ChemMasterDrawSource DrawSource; + public ChemMasterBoundUserInterfaceState( ChemMasterMode mode, ChemMasterSortingType sortingType, ContainerInfo? inputContainerInfo, ContainerInfo? outputContainerInfo, IReadOnlyList bufferReagents, FixedPoint2 bufferCurrentVolume, - uint selectedPillType, uint pillDosageLimit, bool updateLabel) + uint selectedPillType, uint pillDosageLimit, bool updateLabel, ChemMasterDrawSource drawSource) { InputContainerInfo = inputContainerInfo; OutputContainerInfo = outputContainerInfo; @@ -198,6 +212,7 @@ public ChemMasterBoundUserInterfaceState( SelectedPillType = selectedPillType; PillDosageLimit = pillDosageLimit; UpdateLabel = updateLabel; + DrawSource = drawSource; } } diff --git a/Content.Shared/Chemistry/SharedTransferAmount.cs b/Content.Shared/Chemistry/SharedTransferAmount.cs index 1d95d2d687d..9b47e2f0e6d 100644 --- a/Content.Shared/Chemistry/SharedTransferAmount.cs +++ b/Content.Shared/Chemistry/SharedTransferAmount.cs @@ -3,28 +3,16 @@ namespace Content.Shared.Chemistry { + /// + /// Sent by the client when setting the transfer amount using the BUI. + /// [Serializable, NetSerializable] - public sealed class TransferAmountBoundInterfaceState : BoundUserInterfaceState + public sealed class TransferAmountSetValueMessage(FixedPoint2 value) : BoundUserInterfaceMessage { - public FixedPoint2 Max; - public FixedPoint2 Min; - - public TransferAmountBoundInterfaceState(FixedPoint2 max, FixedPoint2 min) - { - Max = max; - Min = min; - } - } - - [Serializable, NetSerializable] - public sealed class TransferAmountSetValueMessage : BoundUserInterfaceMessage - { - public FixedPoint2 Value; - - public TransferAmountSetValueMessage(FixedPoint2 value) - { - Value = value; - } + /// + /// The new transfer amount. + /// + public FixedPoint2 Value = value; } [Serializable, NetSerializable] diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index 9d205c5097e..a94bded9fd8 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -2,7 +2,7 @@ using Content.Shared.Buckle.Components; using Content.Shared.Climbing.Components; using Content.Shared.Climbing.Events; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.DragDrop; using Content.Shared.Hands.Components; @@ -43,21 +43,18 @@ public sealed partial class ClimbSystem : VirtualController [Dependency] private readonly SharedStunSystem _stunSystem = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!; + [Dependency] private readonly EntityQuery _climbableQuery = default!; + [Dependency] private readonly EntityQuery _fixturesQuery = default!; + [Dependency] private readonly EntityQuery _xformQuery = default!; + private const string ClimbingFixtureName = "climb"; private const int ClimbingCollisionGroup = (int) (CollisionGroup.TableLayer | CollisionGroup.LowImpassable); - private EntityQuery _climbableQuery; - private EntityQuery _fixturesQuery; - private EntityQuery _xformQuery; public override void Initialize() { base.Initialize(); - _climbableQuery = GetEntityQuery(); - _fixturesQuery = GetEntityQuery(); - _xformQuery = GetEntityQuery(); - SubscribeLocalEvent(OnMoveAttempt); SubscribeLocalEvent(OnParentChange); SubscribeLocalEvent(OnDoAfter); diff --git a/Content.Shared/Cloning/CloningSettingsPrototype.cs b/Content.Shared/Cloning/CloningSettingsPrototype.cs index b422f7188b9..ffc4dd06ddc 100644 --- a/Content.Shared/Cloning/CloningSettingsPrototype.cs +++ b/Content.Shared/Cloning/CloningSettingsPrototype.cs @@ -1,15 +1,13 @@ using Content.Shared.Inventory; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; namespace Content.Shared.Cloning; /// -/// Settings for cloning a humanoid. -/// Used to decide which components should be copied. +/// Settings for cloning a humanoid. +/// Used to decide which components should be copied. /// [Prototype] public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype @@ -18,64 +16,79 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr [IdDataField] public string ID { get; private set; } = default!; + /// [ParentDataField(typeof(PrototypeIdArraySerializer))] public string[]? Parents { get; private set; } + /// [AbstractDataField] [NeverPushInheritance] public bool Abstract { get; private set; } /// - /// Determines if cloning can be prevented by traits etc. + /// Determines if cloning can be prevented by traits etc. /// [DataField] public bool ForceCloning = true; /// - /// Which inventory slots will receive a copy of the original's clothing. - /// Disabled when null. + /// Which inventory slots will receive a copy of the original's clothing. + /// Disabled when null. /// [DataField] public SlotFlags? CopyEquipment = SlotFlags.All; /// - /// Whether or not to copy slime storage and storage implant contents. + /// Whether or not to copy slime storage and storage implant contents. /// [DataField] public bool CopyInternalStorage = true; /// - /// Whether or not to copy implants. + /// Whether or not to copy implants. /// [DataField] public bool CopyImplants = true; /// - /// Whitelist for the equipment allowed to be copied. + /// Should infinite status effects (which are used for some traits) applied to an entity be copied or not? + /// + [DataField] + public bool CopyStatusEffects = true; + + /// + /// Should the event for renaming the clone be raised? + /// This will also set the clone's id card and PDA name if they have one equipped. + /// + [DataField] + public bool RaiseEntityRenamedEvent = true; + + /// + /// Whitelist for the equipment allowed to be copied. /// [DataField] public EntityWhitelist? Whitelist; /// - /// Blacklist for the equipment allowed to be copied. + /// Blacklist for the equipment allowed to be copied. /// [DataField] public EntityWhitelist? Blacklist; /// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709 /// - /// Components to copy from the original to the clone using CopyComp. - /// This makes a deepcopy of all datafields, including information the clone might not own! - /// If you need to exclude data or do additional component initialization, then subscribe to CloningEvent instead! - /// Components in this list that the orginal does not have will be removed from the clone. + /// Components to copy from the original to the clone using CopyComp. + /// This makes a deepcopy of all datafields, including information the clone might not own! + /// If you need to exclude data or do additional component initialization, then subscribe to CloningEvent instead! + /// Components in this list that the orginal does not have will be removed from the clone. /// [DataField] [AlwaysPushInheritance] public HashSet Components = new(); /// - /// Components to remove from the clone and copy over manually using a CloneEvent raised on the original. - /// Use this when the component cannot be copied using CopyComp, for example when having an Uid as a datafield. + /// Components to remove from the clone and copy over manually using a CloneEvent raised on the original. + /// Use this when the component cannot be copied using CopyComp, for example when having an EntityUid as a datafield. /// [DataField] [AlwaysPushInheritance] diff --git a/Content.Shared/Cloning/SharedCloningSystem.cs b/Content.Shared/Cloning/SharedCloningSystem.cs index d8ab8a2aa19..ca21d686cf1 100644 --- a/Content.Shared/Cloning/SharedCloningSystem.cs +++ b/Content.Shared/Cloning/SharedCloningSystem.cs @@ -1,14 +1,137 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Implants.Components; +using Content.Shared.Inventory; +using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + namespace Content.Shared.Cloning; public abstract partial class SharedCloningSystem : EntitySystem { + [Dependency] private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; //TODO: This system has to support both the old and new status effect systems, until the old is able to be fully removed. + [Dependency] private readonly EntityQuery _cloneableEffectQuery = default!; + + /// + /// Spawns a clone of the given humanoid mob at the specified location or in nullspace. + /// + public virtual bool TryCloning( + EntityUid original, + MapCoordinates? coords, + ProtoId settingsId, + [NotNullWhen(true)] out EntityUid? clone) + { + clone = null; + return false; + } + + /// + /// Copy components from one entity to another based on a CloningSettingsPrototype. + /// + /// The orignal Entity to clone components from. + /// The target Entity to clone components to. + /// The clone settings prototype id containing the list of components to clone. + public virtual void CloneComponents( + EntityUid original, + EntityUid clone, + ProtoId settings) + { + } + /// /// Copy components from one entity to another based on a CloningSettingsPrototype. /// /// The orignal Entity to clone components from. /// The target Entity to clone components to. /// The clone settings prototype containing the list of components to clone. - public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings) + public virtual void CloneComponents( + EntityUid original, + EntityUid clone, + CloningSettingsPrototype settings) { } + + /// + /// Copies the equipment the original has to the clone. + /// This uses the original prototype of the items, so any changes to components that are done after spawning are lost! + /// + public virtual void CopyEquipment( + Entity original, + Entity clone, + SlotFlags slotFlags, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) + { + } + + /// + /// Copies an item and its storage recursively, placing all items at the same position in grid storage. + /// This uses the original prototype of the items, so any changes to components that are done after spawning are lost! + /// + /// + /// This is not perfect and only considers item in storage containers. + /// Some components have their own additional spawn logic on map init, so we cannot just copy all containers. + /// + public virtual EntityUid? CopyItem( + EntityUid original, + EntityCoordinates coords, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) + { + return null; + } + + /// + /// Copies an item's storage recursively to another storage. + /// The storage grids should have the same shape or it will drop on the floor. + /// Basically the same as CopyItem, but we don't copy the outermost container. + /// + public virtual void CopyStorage( + Entity original, + Entity target, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) + { + } + + /// + /// Copies all implants from one mob to another. + /// Might result in duplicates if the target already has them. + /// Can copy the storage inside a storage implant according to a whitelist and blacklist. + /// + /// Entity to copy implants from. + /// Entity to copy implants to. + /// If true will copy storage of the implants (E.g storage implant) + /// Whitelist for the storage copy (If copyStorage is true) + /// Blacklist for the storage copy (If copyStorage is true) + public virtual void CopyImplants( + Entity original, + EntityUid target, + bool copyStorage = false, + EntityWhitelist? whitelist = null, + EntityWhitelist? blacklist = null) + { + } + + /// + /// Scans all permanent status effects applied to the original entity and transfers them to the clone. + /// + public void CopyStatusEffects(Entity original, Entity target) + { + foreach (var effect in _statusEffects.EnumerateStatusEffects(original, _cloneableEffectQuery)) + { + // We are not interested in temporary effects, only permanent ones. + if (effect.Comp1.EndEffectTime is not null) + continue; + + var effectProto = Prototype(effect); + + if (effectProto is null) + continue; + + _statusEffects.TrySetStatusEffectDuration(target, effectProto); + } + } } diff --git a/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs b/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs index 8dc496ec25d..37f0a83eaa6 100644 --- a/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs +++ b/Content.Shared/Clothing/ClothingSpeedModifierComponent.cs @@ -5,7 +5,7 @@ namespace Content.Shared.Clothing; /// /// Modifies speed when worn and activated. -/// Supports ItemToggleComponent. +/// Supports . /// [RegisterComponent, NetworkedComponent, Access(typeof(ClothingSpeedModifierSystem))] public sealed partial class ClothingSpeedModifierComponent : Component @@ -16,6 +16,13 @@ public sealed partial class ClothingSpeedModifierComponent : Component [DataField] public float SprintModifier = 1.0f; + /// + /// Defines if the speed modifier requires activation to apply. + /// This will have no effect without an on the entity. + /// + [DataField] + public bool RequireActivated = true; + /// /// An optional required standing state. /// Set to true if you need to be standing, false if you need to not be standing, null if you don't care. diff --git a/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs b/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs index 961ec231f32..88290a70cfa 100644 --- a/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs +++ b/Content.Shared/Clothing/ClothingSpeedModifierSystem.cs @@ -57,7 +57,7 @@ private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent compone private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, InventoryRelayedEvent args) { - if (!_toggle.IsActivated(uid)) + if (component.RequireActivated && !_toggle.IsActivated(uid)) return; if (component.Standing != null && !_standing.IsMatchingState(args.Owner, component.Standing.Value)) @@ -128,6 +128,9 @@ private void OnClothingVerbExamine(EntityUid uid, ClothingSpeedModifierComponent private void OnToggled(Entity ent, ref ItemToggledEvent args) { + if (!ent.Comp.RequireActivated) + return; + // make sentient boots slow or fast too _movementSpeed.RefreshMovementSpeedModifiers(ent); diff --git a/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs b/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs index 5caddda18be..4c61c67c6dd 100644 --- a/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ChameleonClothingComponent.cs @@ -21,15 +21,19 @@ public sealed partial class ChameleonClothingComponent : Component public SlotFlags Slot; /// - /// EntityPrototype id that chameleon item is trying to mimic. + /// The currently selected EntityPrototype ID that chameleon item is trying to mimic. /// + /// + /// TODO: Rename this, the name "Default" is misleading. + /// Also should not be required, just make null use its original sprites. + /// [DataField(required: true), AutoNetworkedField] public EntProtoId? Default; /// /// Current user that wears chameleon clothing. /// - [ViewVariables] + [DataField, AutoNetworkedField] public EntityUid? User; /// @@ -38,6 +42,18 @@ public sealed partial class ChameleonClothingComponent : Component [DataField] public string? RequireTag; + /// + /// Can this item have its prototype changed by a ? + /// + [DataField] + public bool CanBeSetByController = true; + + /// + /// Show a verb for toggling the UI? + /// + [DataField] + public bool ShowVerb = true; + /// /// Will component owner be affected by EMP pulses? /// diff --git a/Content.Shared/Clothing/Components/ClothingComponent.cs b/Content.Shared/Clothing/Components/ClothingComponent.cs index 2d576637a60..b009c55201f 100644 --- a/Content.Shared/Clothing/Components/ClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ClothingComponent.cs @@ -87,6 +87,15 @@ public sealed partial class ClothingComponent : Component [DataField] public TimeSpan UnequipDelay = TimeSpan.Zero; + /// + /// Only works when EquipDelay or UnequipDelay > 0. + /// Prevents clothing from being put on or taken off while moving. + /// Some clothing can logically be put on while running (hats), + /// while other types of clothing may require stopping (shoes, hard suits). + /// + [DataField] + public bool EquipWhileMoving = false; + /// /// Offset for the strip time for an entity with this component. /// Only applied when it is being equipped or removed by another player. @@ -97,7 +106,6 @@ public sealed partial class ClothingComponent : Component /// /// A scale applied to all layers. /// - /// [DataField] public Vector2 Scale = Vector2.One; } diff --git a/Content.Shared/Clothing/Components/FleetingClothingComponent.cs b/Content.Shared/Clothing/Components/FleetingClothingComponent.cs new file mode 100644 index 00000000000..0790d476720 --- /dev/null +++ b/Content.Shared/Clothing/Components/FleetingClothingComponent.cs @@ -0,0 +1,64 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// +/// Makes a clothing item despawn when unequipped, stripped or removed in any other way. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class FleetingClothingComponent : Component +{ + /// + /// The sound to play when unequipping. + /// + [DataField] + public SoundSpecifier? RemovedSound; + + /// + /// Should the sound also be played when the player wearing the item unequips it themselves? + /// + [DataField] + public bool PlaySoundOnSelfUnequip = true; + + /// + /// The popup to show to the wearer if they unequip this item themselves. + /// + [DataField] + public LocId? SelfUnquipPopupWearer = "fleeting-clothing-component-default-popup"; + + /// + /// The popup to show to others if the wearer unequips this item themselves. + /// + [DataField] + public LocId? SelfUnquipPopupOthers = "fleeting-clothing-component-default-popup"; + + /// + /// The popup to show to everone if this item was removed by any other means. + /// + /// + /// We can't split this one up into wearer/others popups because EntGotRemovedFromContainerEvent does not have the user passed in. + /// + [DataField] + public LocId? RemovedPopup = "fleeting-clothing-component-default-popup"; + + /// + /// Examine text shown to the wearer. + /// + [DataField] + public LocId? ExamineWearer = "fleeting-clothing-component-default-examine"; + + /// + /// Examine text shown to others. + /// + [DataField] + public LocId? ExamineOthers = "fleeting-clothing-component-default-examine"; + + /// + /// If true this entity will use rather than simply be deleted. + /// Use this if you need to do stuff before deleting it, for example emptying storage containers so that the contents don't get deleted with with. + /// If false the clothing item will just be deleted instead. + /// + [DataField] + public bool DestroyOnUnequip = true; +} diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index 6cb093472ff..6c7b024b292 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -15,7 +15,6 @@ public abstract class ClothingSystem : EntitySystem [Dependency] private readonly SharedItemSystem _itemSys = default!; [Dependency] private readonly InventorySystem _invSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly HideLayerClothingSystem _hideLayer = default!; public override void Initialize() { @@ -89,22 +88,22 @@ protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, if ((component.Slots & args.SlotFlags) == SlotFlags.NONE) return; - var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component); + var gotEquippedEvent = new ClothingGotEquippedEvent(args.EquipTarget, component); RaiseLocalEvent(uid, ref gotEquippedEvent); var didEquippedEvent = new ClothingDidEquippedEvent((uid, component)); - RaiseLocalEvent(args.Equipee, ref didEquippedEvent); + RaiseLocalEvent(args.EquipTarget, ref didEquippedEvent); } protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) { if ((component.Slots & args.SlotFlags) != SlotFlags.NONE) { - var gotUnequippedEvent = new ClothingGotUnequippedEvent(args.Equipee, component); + var gotUnequippedEvent = new ClothingGotUnequippedEvent(args.EquipTarget, component); RaiseLocalEvent(uid, ref gotUnequippedEvent); var didUnequippedEvent = new ClothingDidUnequippedEvent((uid, component)); - RaiseLocalEvent(args.Equipee, ref didUnequippedEvent); + RaiseLocalEvent(args.EquipTarget, ref didUnequippedEvent); } component.InSlot = null; @@ -140,6 +139,21 @@ private void OnItemStripped(Entity ent, ref BeforeItemStrippe #region Public API + /// + /// Returns true if this clothing item is currently inside an inventory slot AND that slot is considered valid for equipping. + /// For example putting shoes into your pockets does not count as being equipped. + /// + public bool IsEquipped(Entity item) + { + if (!Resolve(item, ref item.Comp, false)) + return false; + + if ((item.Comp.Slots & item.Comp.InSlotFlag) != SlotFlags.NONE) + return true; + + return false; + } + public void SetEquippedPrefix(EntityUid uid, string? prefix, ClothingComponent? clothing = null) { if (!Resolve(uid, ref clothing, false)) diff --git a/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs index 76b7b9aa66d..5c0e936bdf1 100644 --- a/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/FactionClothingSystem.cs @@ -22,8 +22,8 @@ public override void Initialize() private void OnEquipped(Entity ent, ref GotEquippedEvent args) { - TryComp(args.Equipee, out var factionComp); - var faction = (args.Equipee, factionComp); + TryComp(args.EquipTarget, out var factionComp); + var faction = (args.EquipTarget, factionComp); ent.Comp.AlreadyMember = _faction.IsMember(faction, ent.Comp.Faction); _faction.AddFaction(faction, ent.Comp.Faction); @@ -37,6 +37,6 @@ private void OnUnequipped(Entity ent, ref GotUnequippe return; } - _faction.RemoveFaction(args.Equipee, ent.Comp.Faction); + _faction.RemoveFaction(args.EquipTarget, ent.Comp.Faction); } } diff --git a/Content.Shared/Clothing/EntitySystems/FleetingClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/FleetingClothingSystem.cs new file mode 100644 index 00000000000..e9a61dd8b39 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/FleetingClothingSystem.cs @@ -0,0 +1,117 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Destructible; +using Content.Shared.Examine; +using Content.Shared.Inventory.Events; +using Content.Shared.Popups; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Timing; + +namespace Content.Shared.Clothing.EntitySystems; + +public sealed class FleetingClothingSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ClothingSystem _clothing = default!; + [Dependency] private readonly SharedDestructibleSystem _destructibleSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnBeforeGettingUnequipped); + SubscribeLocalEvent(OnGotUnequipped); + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + string? examineText = null; + // check if the item is clothing, is currently equipped and if the wearer is the examiner + if (_clothing.IsEquipped(ent.Owner) && Transform(ent.Owner).ParentUid == args.Examiner) + { + // text to show to the wearer only + if (ent.Comp.ExamineWearer != null) + examineText = Loc.GetString(ent.Comp.ExamineWearer); + } + else + { + // text to show to everyone else + if (ent.Comp.ExamineOthers != null) + examineText = Loc.GetString(ent.Comp.ExamineOthers); + } + + if (!string.IsNullOrEmpty(examineText)) + args.PushMarkup(examineText); + } + + // Raised before the item is being unequipped. + // We have to use QueueDel instead of Del because directly deleting the entity while an event is being raised on it will cause errors. + // We can't do this in.GotUnequippedEvent because container events don't include the user. + private void OnBeforeGettingUnequipped(Entity ent, ref BeforeGettingUnequippedEvent args) + { + if (ent.Comp.DestroyOnUnequip) + _destructibleSystem.DestroyEntity(ent.Owner); // Empty containers first. + else + PredictedQueueDel(ent.Owner); // Poof! + + // Use coords because the entity will be deleted. + var coords = Transform(ent).Coordinates; + if (ent.Comp.PlaySoundOnSelfUnequip || args.User != args.EquipTarget) + _audio.PlayPredicted(ent.Comp.RemovedSound, coords, args.User); + + if (args.User == args.EquipTarget) + { + // If the wearer removes the item themselves show specific messages to them and others. + string? selfMessage = null; + string? othersMessage = null; + if (ent.Comp.SelfUnquipPopupWearer != null) + selfMessage = Loc.GetString(ent.Comp.SelfUnquipPopupWearer, ("item", ent.Owner)); + if (ent.Comp.SelfUnquipPopupOthers != null) + othersMessage = Loc.GetString(ent.Comp.SelfUnquipPopupOthers, ("item", ent.Owner)); + + // Use the wearer for the popup location because the item item itself will get deleted. + _popup.PopupPredicted(selfMessage, othersMessage, args.EquipTarget, args.User, PopupType.LargeCaution); + } + else + { + // Show the same popup message for everyone. + if (ent.Comp.RemovedPopup != null) + _popup.PopupPredicted(Loc.GetString(ent.Comp.RemovedPopup, ("item", ent.Owner)), args.EquipTarget, args.User, PopupType.LargeCaution); + } + } + + // In case the item was somehow removed by any other means not using TryUnequip. + private void OnGotUnequipped(Entity ent, ref GotUnequippedEvent args) + { + if (_timing.ApplyingState) + return; // The results of the container change were already networked as part of the same game state. + + if (Terminating(ent.Owner) || EntityManager.IsQueuedForDeletion(ent.Owner)) + return; // Don't do the popups or sound twice. + + if (Terminating(args.EquipTarget)) + return; // Don't do anything if the item is removed due to the wearer getting deleted. + + if (ent.Comp.DestroyOnUnequip) + _destructibleSystem.DestroyEntity(ent.Owner); // Empty containers first. + else + PredictedQueueDel(ent.Owner); // Poof! + + // Can't predict the popup or sound without a user. + // TODO: Make the popup and sound API sane and remove this guard. + if (_net.IsClient) + return; + + // Use the wearer for the popup location because the item item itself will get deleted. + _audio.PlayPvs(ent.Comp.RemovedSound, args.EquipTarget); + if (ent.Comp.RemovedPopup != null) + _popup.PopupEntity(Loc.GetString(ent.Comp.RemovedPopup, ("item", ent.Owner)), args.EquipTarget, PopupType.LargeCaution); + } +} diff --git a/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs index 44d72b248b4..ee0f3932d6c 100644 --- a/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs @@ -8,7 +8,7 @@ namespace Content.Shared.Clothing.EntitySystems; public sealed class HideLayerClothingSystem : EntitySystem { - [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly SharedHideableHumanoidLayersSystem _hideableHumanoidLayers = default!; [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() @@ -36,7 +36,7 @@ private void OnHideGotUnequipped(Entity ent, ref Clo private void SetLayerVisibility( Entity clothing, - Entity user, + Entity user, bool hideLayers) { if (_timing.ApplyingState) @@ -51,7 +51,6 @@ private void SetLayerVisibility( hideLayers &= IsEnabled(clothing!); - var hideable = user.Comp.HideLayersOnEquip; var inSlot = clothing.Comp2.InSlotFlag ?? SlotFlags.NONE; // This method should only be getting called while the clothing is equipped (though possibly currently in @@ -60,18 +59,13 @@ private void SetLayerVisibility( DebugTools.AssertNotNull(clothing.Comp2.InSlotFlag); DebugTools.AssertNotEqual(inSlot, SlotFlags.NONE); - var dirty = false; - // iterate the HideLayerClothingComponent's layers map and check that // the clothing is (or was)equipped in a matching slot. foreach (var (layer, validSlots) in clothing.Comp1.Layers) { - if (!hideable.Contains(layer)) - continue; - // Only update this layer if we are currently equipped to the relevant slot. if (validSlots.HasFlag(inSlot)) - _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty); + _hideableHumanoidLayers.SetLayerOcclusion(user, layer, hideLayers, inSlot); } // Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any @@ -82,13 +76,9 @@ private void SetLayerVisibility( { foreach (var layer in slots) { - if (hideable.Contains(layer)) - _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty); + _hideableHumanoidLayers.SetLayerOcclusion(user, layer, hideLayers, inSlot); } } - - if (dirty) - Dirty(user!); } private bool IsEnabled(Entity clothing) diff --git a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs index 30e00faf0ac..0c53349934b 100644 --- a/Content.Shared/Clothing/EntitySystems/MaskSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/MaskSystem.cs @@ -62,27 +62,7 @@ private void OnToggleMask(Entity ent, ref ToggleMaskEvent args) private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args) { - if (!mask.IsToggled || !mask.IsToggleable) - return; - - mask.IsToggled = false; - ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true); - } - - /// - /// Called after setting IsToggled, raises events and dirties. - /// - private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false) - { - Dirty(uid, mask); - if (mask.ToggleActionEntity is { } action) - _actionSystem.SetToggled(action, mask.IsToggled); - - var maskEv = new ItemMaskToggledEvent((uid, mask), wearer); - RaiseLocalEvent(uid, ref maskEv); - - var wearerEv = new WearerMaskToggledEvent((uid, mask)); - RaiseLocalEvent(wearer, ref wearerEv); + SetToggled(uid, false); } private void OnFolded(Entity ent, ref FoldedEvent args) diff --git a/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs index 49df7aee943..9890832ee79 100644 --- a/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs @@ -63,7 +63,7 @@ private void OnEquipped(Entity entity, ref GotEquipped if (!isCorrectSlot) return; - entity.Comp.Wearer = args.Equipee; + entity.Comp.Wearer = args.EquipTarget; Dirty(entity); // Attempt to setup control link, if Pilot and Wearer are both present. diff --git a/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs index ab0c41c5c7b..e7227352677 100644 --- a/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs @@ -23,7 +23,7 @@ private void OnUnequip(Entity selfUnremovableC if (TryComp(selfUnremovableClothing, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE) return; - if (args.UnEquipTarget == args.Unequipee) + if (args.UnEquipTarget == args.User) { args.Cancel(); } diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs index 18b79bf52bb..ccd0e04953f 100644 --- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs @@ -2,13 +2,16 @@ using Content.Shared.Access.Components; using Content.Shared.Clothing.Components; using Content.Shared.Contraband; +using Content.Shared.Emp; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Lock; using Content.Shared.Tag; using Content.Shared.Verbs; +using Robust.Shared.Network; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -23,8 +26,11 @@ public abstract class SharedChameleonClothingSystem : EntitySystem [Dependency] private readonly SharedItemSystem _itemSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TagSystem _tag = default!; - [Dependency] protected readonly IGameTiming _timing = default!; + [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly LockSystem _lock = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; + [Dependency] private readonly INetManager _net = default!; private static readonly SlotFlags[] IgnoredSlots = { @@ -32,12 +38,12 @@ public abstract class SharedChameleonClothingSystem : EntitySystem SlotFlags.PREVENTEQUIP, SlotFlags.NONE }; + private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray(); private readonly Dictionary> _data = new(); public readonly Dictionary> ValidVariants = new(); - [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; private static readonly ProtoId WhitelistChameleonTag = "WhitelistChameleon"; @@ -47,6 +53,7 @@ public override void Initialize() SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnEmpPulse); SubscribeLocalEvent(OnPrototypeReload); PrepareAllVariants(); @@ -59,12 +66,20 @@ private void OnPrototypeReload(EntityUid uid, ChameleonClothingComponent compone private void OnGotEquipped(EntityUid uid, ChameleonClothingComponent component, GotEquippedEvent args) { - component.User = args.Equipee; + if (Timing.ApplyingState) + return; // Already networked as part of the same gamestate + + component.User = args.EquipTarget; + Dirty(uid, component); } private void OnGotUnequipped(EntityUid uid, ChameleonClothingComponent component, GotUnequippedEvent args) { + if (Timing.ApplyingState) + return; // Already networked as part of the same gamestate + component.User = null; + Dirty(uid, component); } // Updates chameleon visuals and meta information. @@ -97,21 +112,21 @@ protected void UpdateVisuals(EntityUid uid, ChameleonClothingComponent component // clothing sprite logic if (TryComp(uid, out ClothingComponent? clothing) && - proto.TryGetComponent("Clothing", out ClothingComponent? otherClothing)) + proto.TryGetComponent(out ClothingComponent? otherClothing, Factory)) { _clothingSystem.CopyVisuals(uid, otherClothing, clothing); } // appearance data logic if (TryComp(uid, out AppearanceComponent? appearance) && - proto.TryGetComponent("Appearance", out AppearanceComponent? appearanceOther)) + proto.TryGetComponent(out AppearanceComponent? appearanceOther, Factory)) { _appearance.AppendData(appearanceOther, uid); Dirty(uid, appearance); } // properly mark contraband - if (proto.TryGetComponent("Contraband", out ContrabandComponent? contra)) + if (proto.TryGetComponent(out ContrabandComponent? contra, Factory)) { EnsureComp(uid, out var current); _contraband.CopyDetails(uid, contra, current); @@ -127,6 +142,9 @@ private void OnVerb(Entity ent, ref GetVerbsEvent ent, ref GetVerbsEvent @@ -157,8 +193,9 @@ public bool IsValidTarget(EntityPrototype proto, SlotFlags chameleonSlot = SlotF return false; // check if it's valid clothing - if (!proto.TryGetComponent("Clothing", out ClothingComponent? clothing)) + if (!proto.TryGetComponent(out ClothingComponent? clothing, Factory)) return false; + if (!clothing.Slots.HasFlag(chameleonSlot)) return false; @@ -187,6 +224,14 @@ public IEnumerable GetValidTargets(SlotFlags slot, string? tag = nul return validTargets; } + /// + /// Get a random prototype for a given slot. + /// + public string GetRandomValidPrototype(SlotFlags slot, string? tag = null) + { + return _random.Pick(GetValidTargets(slot, tag).ToList()); + } + protected void PrepareAllVariants() { _data.Clear(); @@ -215,4 +260,17 @@ protected void PrepareAllVariants() } } } + + // TODO: Predict and use component states for the UI + /// + /// Change chameleon items name, description and sprite to mimic other entity prototype. + /// + /// The entity who's appearance to swap. + /// The target protoId of the target appearance. + /// Whether to force update appearance, even if the same one was selected. + /// Whether to validate if the target prototype is a valid chameleon target. + /// The of the entity we are updating. + public virtual void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdate = false, bool validate = true, + ChameleonClothingComponent? component = null) + { } } diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index a2b7d016410..ce167e06f4e 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -160,7 +160,7 @@ private void OnToggleableUnequip(EntityUid uid, ToggleableClothingComponent comp // This should maybe double check that the entity currently in the slot is actually the attached clothing, but // if its not, then something else has gone wrong already... if (component.Container != null && component.Container.ContainedEntity == null && component.ClothingUid != null) - _inventorySystem.TryUnequip(args.Equipee, component.Slot, force: true, triggerHandContact: true); + _inventorySystem.TryUnequip(args.EquipTarget, component.Slot, force: true, triggerHandContact: true); } private void OnRemoveToggleable(EntityUid uid, ToggleableClothingComponent component, ComponentRemove args) diff --git a/Content.Shared/Clothing/LoadoutSystem.cs b/Content.Shared/Clothing/LoadoutSystem.cs index dfe6d1377ba..84df71cfa48 100644 --- a/Content.Shared/Clothing/LoadoutSystem.cs +++ b/Content.Shared/Clothing/LoadoutSystem.cs @@ -1,5 +1,5 @@ using System.Linq; -using Content.Shared.Body.Systems; +using Content.Shared.Body; using Content.Shared.Clothing.Components; using Content.Shared.Humanoid; using Content.Shared.Preferences; @@ -29,7 +29,7 @@ public override void Initialize() base.Initialize(); // Wait until the character has all their organs before we give them their loadout - SubscribeLocalEvent(OnMapInit, after: [typeof(SharedBodySystem)]); + SubscribeLocalEvent(OnMapInit, after: [typeof(InitialBodySystem)]); } public static string GetJobPrototype(string? loadout) @@ -177,11 +177,8 @@ public void GearEquipped(EntityUid uid) public HumanoidCharacterProfile GetProfile(EntityUid? uid) { - if (TryComp(uid, out HumanoidAppearanceComponent? appearance)) - { - return HumanoidCharacterProfile.DefaultWithSpecies(appearance.Species); - } - - return HumanoidCharacterProfile.Random(); + return TryComp(uid, out var profile) + ? HumanoidCharacterProfile.DefaultWithSpecies(profile.Species, profile.Sex) + : HumanoidCharacterProfile.Random(); } } diff --git a/Content.Shared/Clothing/SharedCursedMaskSystem.cs b/Content.Shared/Clothing/SharedCursedMaskSystem.cs index 8ba83be151c..3cd700f4102 100644 --- a/Content.Shared/Clothing/SharedCursedMaskSystem.cs +++ b/Content.Shared/Clothing/SharedCursedMaskSystem.cs @@ -1,8 +1,10 @@ using Content.Shared.Clothing.Components; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Inventory; using Content.Shared.Movement.Systems; +using Content.Shared.Random.Helpers; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -60,7 +62,7 @@ private void OnModifyDamage(Entity ent, ref InventoryRelaye protected void RandomizeCursedMask(Entity ent, EntityUid wearer) { - var random = new System.Random((int) _timing.CurTick.Value); + var random = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent)); ent.Comp.CurrentState = random.Pick(Enum.GetValues()); _appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState); _movementSpeedModifier.RefreshMovementSpeedModifiers(wearer); diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs index 2506359c256..54064c62bd8 100644 --- a/Content.Shared/Clumsy/ClumsySystem.cs +++ b/Content.Shared/Clumsy/ClumsySystem.cs @@ -1,8 +1,8 @@ using Content.Shared.CCVar; -using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Chemistry.Events; using Content.Shared.Climbing.Components; using Content.Shared.Climbing.Events; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.IdentityManagement; using Content.Shared.Medical; using Content.Shared.Popups; @@ -31,7 +31,7 @@ public sealed class ClumsySystem : EntitySystem public override void Initialize() { - SubscribeLocalEvent(BeforeHyposprayEvent); + SubscribeLocalEvent(BeforeHyposprayEvent); SubscribeLocalEvent(BeforeDefibrillatorZapsEvent); SubscribeLocalEvent(BeforeGunShotEvent); SubscribeLocalEvent(OnCatchAttempt); @@ -40,7 +40,7 @@ public override void Initialize() // If you add more clumsy interactions add them in this section! #region Clumsy interaction events - private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeHyposprayInjectsEvent args) + private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeInjectEvent args) { // Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof... @@ -48,15 +48,12 @@ private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeHyp if (!ent.Comp.ClumsyHypo) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; - args.TargetGettingInjected = args.EntityUsingHypospray; - args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage); - _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray); + args.TargetGettingInjected = args.EntityUsingInjector; + args.OverrideMessage = Loc.GetString(ent.Comp.HypoFailedMessage); + _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingInjector); } private void BeforeDefibrillatorZapsEvent(Entity ent, ref SelfBeforeDefibrillatorZapsEvent args) @@ -67,10 +64,7 @@ private void BeforeDefibrillatorZapsEvent(Entity ent, ref SelfB if (!ent.Comp.ClumsyDefib) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; args.DefibTarget = args.EntityUsingDefib; @@ -86,16 +80,13 @@ private void OnCatchAttempt(Entity ent, ref CatchAttemptEvent a if (!ent.Comp.ClumsyCatching) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Item).Id }); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; args.Cancelled = true; // fail to catch if (ent.Comp.CatchingFailDamage != null) - _damageable.TryChangeDamage(ent, ent.Comp.CatchingFailDamage, origin: args.Item); + _damageable.ChangeDamage(ent.Owner, ent.Comp.CatchingFailDamage, origin: args.Item); // Collisions don't work properly with PopupPredicted or PlayPredicted. // So we make this server only. @@ -120,14 +111,11 @@ private void BeforeGunShotEvent(Entity ent, ref SelfBeforeGunSh if (args.Gun.Comp.ClumsyProof) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id }); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; if (ent.Comp.GunShootFailDamage != null) - _damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent); + _damageable.ChangeDamage(ent.Owner, ent.Comp.GunShootFailDamage, origin: ent); _stun.TryUpdateParalyzeDuration(ent, ent.Comp.GunShootFailStunTime); @@ -145,10 +133,8 @@ private void OnBeforeClimbEvent(Entity ent, ref SelfBeforeClimb if (!ent.Comp.ClumsyVaulting) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id }); - var rand = new System.Random(seed); - if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!_cfg.GetCVar(CCVars.GameTableBonk) + && !SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; HitHeadClumsy(ent, args.BeingClimbedOn); @@ -199,7 +185,7 @@ public void HitHeadClumsy(Entity target, EntityUid table) { stunTime = bonkComp.BonkTime; if (bonkComp.BonkDamage != null) - _damageable.TryChangeDamage(target, bonkComp.BonkDamage, true); + _damageable.ChangeDamage(target.Owner, bonkComp.BonkDamage, true); } _stun.TryUpdateParalyzeDuration(target, stunTime); diff --git a/Content.Shared/Cluwne/CluwneComponent.cs b/Content.Shared/Cluwne/CluwneComponent.cs index c9f96d030b3..77d14431f53 100644 --- a/Content.Shared/Cluwne/CluwneComponent.cs +++ b/Content.Shared/Cluwne/CluwneComponent.cs @@ -1,6 +1,9 @@ using Robust.Shared.Audio; using Content.Shared.Chat.Prototypes; +using Content.Shared.Damage; +using Content.Shared.Roles; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Cluwne; @@ -12,22 +15,74 @@ public sealed partial class CluwneComponent : Component /// /// timings for giggles and knocks. /// - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan DamageGiggleCooldown = TimeSpan.FromSeconds(2); - [ViewVariables(VVAccess.ReadWrite)] + /// + /// Amount of genetic damage dealt when they revert + /// + [DataField] + public DamageSpecifier RevertDamage = new() + { + DamageDict = new() + { + { "Cellular", 300.0 }, + }, + }; + + /// + /// Chance that the Cluwne will be knocked over and paralyzed. + /// + [DataField] public float KnockChance = 0.05f; - [ViewVariables(VVAccess.ReadWrite)] + /// + /// Chance that the Cluwne will randomly giggle + /// + [DataField] public float GiggleRandomChance = 0.1f; - [DataField("emoteId", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? EmoteSoundsId = "Cluwne"; + /// + /// Enable random emoting? + /// + [DataField] + public bool RandomEmote = true; + + /// + /// Emote sound collection that the Cluwne should use. + /// + [DataField("emoteId")] + public ProtoId? EmoteSoundsId = "Cluwne"; + + /// + /// Emote to use for the Cluwne Giggling + /// + [DataField] + public ProtoId? AutoEmoteId = "CluwneGiggle"; + + /// + /// Message to popup when the Cluwne is transformed + /// + [DataField] + public LocId TransformMessage = "cluwne-transform"; + + /// + /// Name prefix for the Cluwne. + /// Example "Urist McHuman" will be "Cluwned Urist McHuman" + /// + [DataField] + public LocId NamePrefix = "cluwne-name-prefix"; + + /// + /// Outfit ID that the cluwne will spawn with. + /// + [DataField] + public ProtoId OutfitId = "CluwneGear"; /// /// Amount of time cluwne is paralyzed for when falling over. /// - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public float ParalyzeTime = 2f; /// @@ -36,6 +91,21 @@ public sealed partial class CluwneComponent : Component [DataField("spawnsound")] public SoundSpecifier SpawnSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); - [DataField("knocksound")] + /// + /// Emote to use for the cluwne giggling + /// + [DataField] + public LocId GiggleEmote = "cluwne-giggle-emote"; + + /// + /// Sound to play when the Cluwne is knocked over and paralyzed + /// + [DataField] public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Items/airhorn.ogg"); + + /// + /// Emote thats used when the cluwne getting knocked over + /// + [DataField] + public LocId KnockEmote = "cluwne-knock-emote"; } diff --git a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs index 6bc32c5b96c..22fbb53a93f 100644 --- a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs +++ b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Content.Shared.Throwing; +using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Timing; @@ -65,6 +66,10 @@ private void OnShootAttempt(Entity ent, ref ShotAttemptedEven if (HasComp(args.Used)) return; + if (TryComp(args.Used, out var component)) + if (component.FireModes[component.CurrentFireMode].PacifismAllowedMode) + return; + // Disallow firing guns in all cases. ShowPopup(ent, args.Used, "pacified-cannot-fire-gun"); args.Cancel(); diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs index 4c82a2f8190..3878e7fa2ea 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.MouseRotator; using Content.Shared.Movement.Components; +using Content.Shared.NPC.Systems; using Content.Shared.Popups; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -18,6 +19,7 @@ public abstract class SharedCombatModeSystem : EntitySystem [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedNPCSystem _npc = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly INetManager _netMan = default!; // Imperial medieval edit end [Dependency] private readonly MobStateSystem _mobStateSystem = default!; @@ -101,7 +103,7 @@ public virtual void SetInCombatMode(EntityUid entity, bool value, CombatModeComp _actionsSystem.SetToggled(component.CombatToggleActionEntity, component.IsInCombatMode); // Change mouse rotator comps if flag is set - if (!component.ToggleMouseRotator || IsNpc(entity) && !_mind.TryGetMind(entity, out _, out _)) + if (!component.ToggleMouseRotator || _npc.IsNpc(entity) && !_mind.TryGetMind(entity, out _, out _)) return; SetMouseRotatorComponents(entity, value); @@ -120,9 +122,6 @@ private void SetMouseRotatorComponents(EntityUid uid, bool value) RemComp(uid); } } - - // todo: When we stop making fucking garbage abstract shared components, remove this shit too. - protected abstract bool IsNpc(EntityUid uid); } public sealed partial class ToggleCombatActionEvent : InstantActionEvent diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs index d53a100acc6..495dddfe693 100644 --- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs +++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; using Content.Shared.Examine; using Content.Shared.Construction.Components; @@ -33,7 +34,7 @@ public sealed partial class AnchorableSystem : EntitySystem [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - private EntityQuery _physicsQuery; + [Dependency] private readonly EntityQuery _physicsQuery = default!; public readonly ProtoId Unstackable = "Unstackable"; @@ -41,8 +42,6 @@ public override void Initialize() { base.Initialize(); - _physicsQuery = GetEntityQuery(); - SubscribeLocalEvent(OnInteractUsing, before: new[] { typeof(ItemSlotsSystem) }, after: new[] { typeof(SharedConstructionSystem) }); SubscribeLocalEvent(OnAnchorComplete); @@ -234,12 +233,8 @@ private void TryAnchor(EntityUid uid, EntityUid userUid, EntityUid usingUid, // Log anchor attempt (server only) _adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to anchor {ToPrettyString(uid):entity} to {transform.Coordinates:targetlocation}"); - if (TryComp(uid, out var anchorBody) && - !TileFree(transform.Coordinates, anchorBody)) - { - _popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, userUid); + if (!CanAnchorAt(uid, transform.Coordinates, userUid)) return; - } if (AnyUnstackable(uid, transform.Coordinates)) { @@ -284,6 +279,23 @@ private bool Valid( return !attempt.Cancelled; } + public bool CanAnchorAt(Entity entity, EntityUid? user = null) + { + return CanAnchorAt(entity, Transform(entity).Coordinates, user); + } + + public bool CanAnchorAt(Entity entity, EntityCoordinates coordinates, EntityUid? user = null) + { + if (!Resolve(entity, ref entity.Comp)) + return true; + + if (TileFree(coordinates, entity.Comp)) + return true; + + _popup.PopupClient(Loc.GetString("anchorable-occupied"), entity, user); + return false; + } + /// /// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body. /// diff --git a/Content.Shared/Construction/MachinePartSystem.cs b/Content.Shared/Construction/MachinePartSystem.cs index 7ac9fde94c7..4783c2a332a 100644 --- a/Content.Shared/Construction/MachinePartSystem.cs +++ b/Content.Shared/Construction/MachinePartSystem.cs @@ -58,11 +58,11 @@ private void OnMachineBoardExamined(EntityUid uid, MachineBoardComponent compone } } - public Dictionary GetMachineBoardMaterialCost(Entity entity, int coefficient = 1) + public bool TryGetMachineBoardMaterialCost(Entity entity, out Dictionary materials, int coefficient = 1) { var (_, comp) = entity; - var materials = new Dictionary(); + materials = new Dictionary(); foreach (var (stackId, amount) in comp.StackRequirements) { @@ -89,9 +89,14 @@ public Dictionary GetMachineBoardMaterialCost(Entity GetMachineBoardMaterialCost(Entity public override void Initialize() @@ -76,17 +80,18 @@ private void OnFlatpackInteractUsing(Entity ent, ref Interact return; } + if (!PrototypeManager.Resolve(comp.Entity, out var proto) || + !proto.TryGetComponent(out var fixture, EntityManager.ComponentFactory)) + { + return; + } + + var (layer, mask) = SharedPhysicsSystem.GetHardCollision(fixture); var buildPos = _map.TileIndicesFor(grid, gridComp, xform.Coordinates); - var coords = _map.ToCenterCoordinates(grid, buildPos); - // TODO FLATPAK - // Make this logic smarter. This should eventually allow for shit like building microwaves on tables and such. - // Also: make it ignore ghosts - if (_entityLookup.AnyEntitiesIntersecting(coords, LookupFlags.Dynamic | LookupFlags.Static)) + if (!_anchorable.TileFree((grid, gridComp), buildPos, layer, mask)) { - // this popup is on the server because the predicts on the intersection is crazy - if (_net.IsServer) - _popup.PopupEntity(Loc.GetString("flatpack-unpack-no-room"), uid, args.User); + _popup.PopupPredicted(Loc.GetString("flatpack-unpack-no-room"), uid, args.User); return; } @@ -125,14 +130,34 @@ protected void SetupFlatpack(Entity ent, EntProtoId proto, E Appearance.SetData(ent, FlatpackVisuals.Machine, MetaData(board).EntityPrototype?.ID ?? string.Empty); } + /// + /// Returns the prototype from a board that the flatpacker will create. + /// + public bool TryGetFlatpackResultPrototype(EntityUid board, [NotNullWhen(true)] out EntProtoId? prototype) + { + prototype = null; + + if (TryComp(board, out var machine)) + prototype = machine.Prototype; + else if (TryComp(board, out var computer)) + prototype = computer.Prototype; + return prototype is not null; + } + + /// + /// Tries to get the cost to produce an item, fails if unable to produce it. + /// + /// The flatpacking machine /// The machine board to pack. If null, this implies we are packing a computer board - public Dictionary GetFlatpackCreationCost(Entity entity, Entity? machineBoard) + /// Cost to produce + public bool TryGetFlatpackCreationCost(Entity entity, EntityUid machineBoard, out Dictionary cost) { - Dictionary cost = new(); + cost = new(); Dictionary, int> baseCost; - if (machineBoard is not null) + if (TryComp(machineBoard, out var machineBoardComp)) { - cost = MachinePart.GetMachineBoardMaterialCost(machineBoard.Value, -1); + if (!MachinePart.TryGetMachineBoardMaterialCost((machineBoard, machineBoardComp), out cost, -1)) + return false; baseCost = entity.Comp.BaseMachineCost; } else @@ -144,6 +169,6 @@ public Dictionary GetFlatpackCreationCost(Entity + - - $(TargetFramework) - 12 - false false RA0032;nullable - enable + + + + + + - - false - - - - false - - - false - + diff --git a/Content.Shared/Content.Shared.csproj.lscache b/Content.Shared/Content.Shared.csproj.lscache new file mode 100644 index 00000000000..cbe7a911974 --- /dev/null +++ b/Content.Shared/Content.Shared.csproj.lscache @@ -0,0 +1,4631 @@ +version=1 + +# This file caches language service data to improve the performance of C# Dev Kit. +# It is not intended for manual editing. It can safely be deleted and will be +# regenerated automatically. For more information, see https://aka.ms/lscache +# +# To control where cache files are stored, use the following VS Code setting: +# "dotnet.projectsystem.cacheInProjectFolder": true + +[project] +language=C# +primary +lastDtbSucceeded + +[properties] +AssemblyName=Content.Shared +CommandLineArgsForDesignTimeEvaluation=-langversion:14 -define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS +CompilerGeneratedFilesOutputPath= +MaxSupportedLangVersion=14.0 +ProjectAssetsFile=obj/project.assets.json +RootNamespace=Content.Shared +RunAnalyzers= +RunAnalyzersDuringLiveAnalysis= +SolutionPath=../SpaceStation14.slnx +TargetFrameworkIdentifier=.NETCoreApp +TargetPath=bin/Debug/Content.Shared.dll +TargetRefPath=obj/Debug/ref/Content.Shared.dll +TemporaryDependencyNodeTargetIdentifier=net10.0 + +[commandLineArguments] +/noconfig +/unsafe- +/checked- +/nowarn:1701,1702,1701,1702 +/fullpaths +/nostdlib+ +/errorreport:prompt +/warn:10 +/define:DEBUG;TRACE;WINDOWS;DEVELOPMENT;CLIENT_SCRIPTING;TOOLS;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER +/highentropyva+ +/nullable:enable +/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated" +/debug+ +/debug:portable +/filealign:512 +/optimize- +/out:obj\Debug\Content.Shared.dll +/refout:obj\Debug\refint\Content.Shared.dll +/target:library +/warnaserror- +/utf8output +/deterministic+ +/langversion:14 +/warnaserror+:RA0032,nullable,SYSLIB0011 + +[sourceFiles] +Abilities/Goliath/ + GoliathSummonTentacleAction.cs + GoliathTentacleSystem.cs +Abilities/Mime/ + MimePowers.cs + MimePowersComponent.cs + MimePowersSystem.cs +Access/ + AccessGroupPrototype.cs + AccessLevelPrototype.cs + Components/ + AccessComponent.cs + AccessOverriderComponent.cs + AccessReaderComponent.cs + AccessToggleComponent.cs + ActivatableUIRequiresAccessComponent.cs + ExpireIdCardComponent.cs + IdCardComponent.cs + IdCardConsoleComponent.cs + IdExaminableComponent.cs + ShowAccessReaderSettingsComponent.cs + SharedAccessWire.cs + SharedAgentIDCardSystem.cs + Systems/ + AccessReaderSystem.cs + AccessToggleSystem.cs + ActivatableUIRequiresAccessSystem.cs + IdExaminableSystem.cs + SharedAccessOverriderSystem.cs + SharedAccessSystem.cs + SharedIdCardConsoleSystem.cs + SharedIdCardSystem.cs + SharedJobStatusSystem.cs +ActionBlocker/ActionBlockerSystem.cs +Actions/ + ActionContainerSystem.cs + ActionEvents.cs + ActionGrantComponent.cs + ActionGrantSystem.cs + ActionUpgradeSystem.cs + Components/ + ActionComponent.cs + ActionContainerComponent.cs + ActionsComponent.cs + ActionUpgradeComponent.cs + ConfirmableActionComponent.cs + EntityTargetActionComponent.cs + InstantActionComponent.cs + TargetActionComponent.cs + WorldTargetActionComponent.cs + ConfirmableActionSystem.cs + Events/ + ActionAttemptEvent.cs + ActionComponentChangeEvent.cs + ActionDoAfterEvent.cs + ActionGetEventEvent.cs + ActionPerformedEvent.cs + ActionSetEventEvent.cs + ActionSetTargetEvent.cs + ActionUpgradeEvent.cs + ActionValidateEvent.cs + DisarmAttemptEvent.cs + EggLayInstantActionEvent.cs + FireStarterActionEvent.cs + InvisibleWallActionEvent.cs + ItemActionGrantComponent.cs + SharedActionsSystem.cs + SharedActionsSystem.DoAfter.cs +Administration/ + AdminAnnounceEuiState.cs + AdminCameraEuiState.cs + AdminCommandPermissions.cs + AdminData.cs + AdminFlags.cs + AdminFlagsHelper.cs + AdminFrozenComponent.cs + AdminFrozenSystem.cs + AnyCommandAttribute.cs + BanList/ + BanListEuiState.cs + SharedBan.cs + SharedUnban.cs + BanPanelEuiState.cs + Components/ + AdminMinigunComponent.cs + BufferingComponent.cs + DisarmProneComponent.cs + HeadstandComponent.cs + KillSignComponent.cs + MarkerComponent.cs + StationInfiniteBatteryTargetComponent.cs + EditSolutionsEuiState.cs + Events/ + FullPlayerListEvent.cs + PanicBunkerChangedEvent.cs + PlayerInfoChangedEvent.cs + Logs/ + AdminLogsEuiState.cs + DateOrder.cs + IAdminLogsPlayerValue.cs + ISharedAdminLogManager.cs + LogStringHandler.cs + SharedAdminLog.cs + SharedAdminLogManager.cs + Managers/ISharedAdminManager.cs + MsgUpdateAdminStatus.cs + Notes/ + AdminMessageEuiState.cs + AdminNotesEuiState.cs + SharedAdminNote.cs + UserNotesEuiState.cs + PermissionsEuiState.cs + PlayerInfo.cs + PlayerPanelEuiState.cs + QuickDialogOpenEvent.cs + SetOutfitEuiState.cs + SharedBwoinkSystem.cs + SpawnExplosionEuiMsg.cs + Systems/ + AdminGunSystem.cs + RejuvenateSystem.cs + SharedBufferingSystem.cs +Advertise/ + Components/SpeakOnUIClosedComponent.cs + Systems/SharedSpeakOnUIClosedSystem.cs +Alert/ + AlertAutoRemoveComponent.cs + AlertCategoryPrototype.cs + AlertKey.cs + AlertOrderPrototype.cs + AlertPrototype.cs + AlertsComponent.cs + AlertsSystem.cs + AlertState.cs + AlertSyncEvent.cs + ClickAlertEvent.cs + Components/GenericCounterAlertComponent.cs +AlertLevel/ + AlertLevelDisplayComponent.cs + SharedAlertLevelDisplay.cs +Ame/Components/ + AmeFuelContainerComponent.cs + SharedAmeControllerComponent.cs + SharedAmeShieldComponent.cs +Ame/EntitySystems/AmeFuelContainerSystem.cs +Animals/ + Components/ParrotMemoryComponent.cs + Systems/SharedParrotMemorySystem.cs + UdderComponent.cs + UdderSystem.cs + WoolyComponent.cs + WoolySystem.cs +Anomaly/ + AnomalyCoreVisuals.cs + AnomalySynchronizerSystem.cs + Components/ + AnomalousParticleComponent.cs + AnomalyComponent.cs + AnomalyCoreComponent.cs + AnomalyPulsingComponent.cs + AnomalyScannerComponent.cs + AnomalySupercriticalComponent.cs + AnomalySynchronizerComponent.cs + CorePoweredThrowerComponent.cs + InnerBodyAnomalyComponent.cs + InnerBodyAnomalyInjectorComponent.cs + Effects/Components/ + ElectricityAnomalyComponent.cs + EntitySpawnAnomalyComponent.cs + GravityAnomalyComponent.cs + PyroclasticAnomalyComponent.cs + TileSpawnAnomalyComponent.cs + Effects/ + SharedEntityAnomalySystem.cs + SharedGravityAnomalySystem.cs + SharedInnerBodyAnomalySystem.cs + SharedTileAnomalySystem.cs + Prototypes/AnomalyBehaviorPrototype.cs + ScannerDoAfterEvent.cs + SharedAnomaly.cs + SharedAnomalyCoreSystem.cs + SharedAnomalyScannerSystem.cs + SharedAnomalySystem.cs +Antag/ + AntagAcceptability.cs + ShowAntagIconsComponent.cs +APC/SharedApc.cs +Arcade/ + BlockGameBlock.cs + BlockGameMessages.cs + BlockGamePlayerAction.cs + BlockGameUiKey.cs + SharedSpaceVillainArcadeComponent.cs +Armable/ + ArmableComponent.cs + ArmableSystem.cs +Armor/ + AllowSuitStorageComponent.cs + ArmorComponent.cs + SharedArmorSystem.cs +Atmos/ + AtmosCommandUtils.cs + AtmosDirection.cs + AtmosExposedEvents.cs + Atmospherics.cs + Components/ + AtmosDeviceComponent.cs + AtmosPipeLayersComponent.cs + BreathToolComponent.cs + DeltaPressureComponent.cs + ExtinguishableSetCollisionWakeComponent.cs + ExtinguishOnInteractComponent.cs + FirestarterComponent.cs + FlammableComponent.cs + GasAnalyzerComponent.cs + GasMaxPressureHolderComponent.cs + GasMinerComponent.cs + GasPipeSensorComponent.cs + GasPressurePumpComponent.cs + GasTankComponent.cs + GasTileOverlayComponent.cs + GridAtmosphereComponent.cs + IGasMaxPressureHolder.cs + MapAtmosphereComponent.cs + MovedByPressureComponent.cs + PipeAppearanceComponent.cs + SharedGasTankComponent.cs + SharedMapAtmosphereComponent.cs + Consoles/Components/ + AtmosAlertsComputerComponent.cs + AtmosAlertsDeviceComponent.cs + AtmosMonitoringConsoleComponent.cs + AtmosMonitoringConsoleDeviceComponent.cs + Consoles/ + SharedAtmosAlertsComputerSystem.cs + SharedAtmosMonitoringConsoleSystem.cs + EntitySystems/ + ExtinguishableSetCollisionWakeSystem.cs + GasMaxPressureSystem.cs + SharedAtmosDebugOverlaySystem.cs + SharedAtmosphereSystem.API.cs + SharedAtmosphereSystem.BreathTool.cs + SharedAtmosphereSystem.cs + SharedAtmosphereSystem.CVars.cs + SharedAtmosphereSystem.Gases.cs + SharedAtmosPipeAppearanceSystem.cs + SharedAtmosPipeLayersSystem.cs + SharedDeltaPressureSystem.cs + SharedFirestarterSystem.cs + SharedGasMinerSystem.cs + SharedGasPressurePumpSystem.cs + SharedGasPressureRegulatorSystem.cs + SharedGasTankSystem.cs + SharedGasTileOverlaySystem.cs + ExcitedGroup.cs + FireEvents.cs + FireVisuals.cs + GasArraySerializer.cs + GasMixture.cs + GasMixtureStringRepresentation.cs + GasOverlayChunk.cs + GetFireProtectionEvent.cs + Hotspot.cs + IGasMixtureHolder.cs + Monitor/ + AtmosAlarmThreshold.cs + AtmosAlarmType.cs + AtmosMonitorAlarmWireActionKeys.cs + AtmosSensorData.cs + Components/SharedAirAlarmComponent.cs + MonstermosInfo.cs + PipeDirection.cs + Piping/Binary/Components/ + GasPressureRegulatorComponent.cs + GasValveComponent.cs + GasVolumePumpComponent.cs + SharedGasCanisterComponent.cs + SharedGasPressurePumpComponent.cs + SharedGasPressureRegulatorComponent.cs + SharedGasVolumePumpComponent.cs + Piping/Binary/Systems/ + SharedGasValveSystem.cs + SharedGasVolumePumpSystem.cs + Piping/ + Binary/Visuals/GasVolumePumpVisuals.cs + Components/ + AtmosDeviceDisabledEvent.cs + AtmosDeviceEnabledEvent.cs + AtmosDeviceTileChangedEvent.cs + EnabledAtmosDeviceVisuals.cs + PipeColorVisuals.cs + Trinary/Components/ + SharedGasFilterComponent.cs + SharedGasMixerComponent.cs + Unary/Components/ + GasCanisterComponent.cs + GasThermoMachineComponent.cs + SharedGasThermomachineComponent.cs + SharedVentPumpComponent.cs + SharedVentScrubberComponent.cs + Unary/Systems/ + SharedGasCanisterSystem.cs + SharedGasThermoMachineSystem.cs + Unary/ + VentScrewedDoAfterEvent.cs + Visuals/ + ScrubberVisuals.cs + VentPumpVisuals.cs + Portable/Components/SharedSpaceHeaterComponent.cs + Prototypes/GasPrototype.cs + Reactions/GasReactionEnums.cs + Rotting/ + AntiRotOnBuckleComponent.cs + AntiRottingContainerComponent.cs + BeginRottingEvent.cs + PerishableComponent.cs + ProRottingContainerComponent.cs + RotIntoComponent.cs + RottingComponent.cs + SharedRottingSystem.cs + Serialization/TileAtmosCollectionSerializer.cs + TileAtmosphere.cs + TileFireEvent.cs + Visuals/ + PortableScrubberVisuals.cs + SpaceHeaterVisuals.cs +Audio/ + AmbientMusicPrototype.cs + AmbientSoundComponent.cs + AudioHelpers.cs + Events/LobbyPlaylistChangedEvent.cs + Jukebox/ + JukeboxComponent.cs + JukeboxPrototype.cs + JukeboxUi.cs + SharedJukeboxSystem.cs + SharedAmbientSoundSystem.cs + SharedContentAudioSystem.cs + SharedGlobalSoundSystem.cs + SoundWhileAliveComponent.cs +BarSign/ + BarSignComponent.cs + BarSignPrototype.cs + BarSignSystem.cs +Beam/ + Components/SharedBeamComponent.cs + SharedBeamSystem.cs +Bed/ + BedSystem.cs + Components/ + HealOnBuckleComponent.cs + HealOnBuckleHealing.cs + StasisBedBuckledComponent.cs + StasisBedComponent.cs + Cryostorage/ + CanEnterCryostorageComponent.cs + CryostorageComponent.cs + CryostorageContainedComponent.cs + SharedCryostorageSystem.cs + Sleep/ + ForcedSleepingStatusEffectComponent.cs + SleepEmitSoundComponent.cs + SleepingComponent.cs + SleepingSystem.cs + SnoringComponent.cs + TryingToSleepEvent.cs +Beeper/ + BeeperEvents.cs + Components/ + BeeperComponent.cs + ProximityBeeperComponent.cs + Systems/ + BeeperSystem.cs + ProximityBeeperSystem.cs +Bible/SummonActionEvent.cs +Blocking/ + BlockingSystem.cs + BlockingSystem.User.cs + Components/ + BlockingComponent.cs + BlockingUserComponent.cs +Body/ + BodyComponent.cs + BodySystem.Compatibility.cs + BodySystem.cs + BodySystem.Relay.cs + Components/ + BloodstreamComponent.cs + BrainComponent.cs + InternalsComponent.cs + LungComponent.cs + StomachComponent.cs + Events/ + BleedModifierEvent.cs + MetabolismExclusionEvent.cs + MetabolizerEvents.cs + ShiverAttemptEvent.cs + SweatAttemptEvent.cs + GibbableOrganComponent.cs + GibbableOrganSystem.cs + HandOrganComponent.cs + HandOrganSystem.cs + InitialBodyComponent.cs + InitialBodySystem.cs + OrganCategoryPrototype.cs + OrganComponent.cs + SharedVisualBodySystem.cs + SharedVisualBodySystem.Initial.cs + SharedVisualBodySystem.Modifiers.cs + Systems/ + BrainSystem.cs + LungSystem.cs + SharedBloodstreamSystem.cs + SharedInternalsSystem.cs + StomachSystem.cs + VisualBodyComponent.cs + VisualOrganComponent.cs + VisualOrganMarkingsComponent.cs +Botany/Components/ + SharedProduceComponent.cs + SharedSeedComponent.cs +Botany/ + PlantHolderVisuals.cs + ProduceVisuals.cs +Bql/ToolshedVisualizeEuiState.cs +Buckle/ + BuckleDoafterEvent.cs + Components/ + ActiveIgniteOnBuckleComponent.cs + BuckleComponent.cs + IgniteOnBuckleComponent.cs + StrapComponent.cs + SharedBuckleSystem.Buckle.cs + SharedBuckleSystem.cs + SharedBuckleSystem.Interaction.cs + SharedBuckleSystem.Strap.cs +Burial/ + BurialSystem.cs + Components/ + GraveComponent.cs + ShovelComponent.cs + GraveDiggingDoAfterEvent.cs +Cabinet/ + ItemCabinetComponent.cs + ItemCabinetSystem.cs +Camera/ + CameraRecoilComponent.cs + GetEyeOffsetEvent.cs + GetEyePvsScaleEvent.cs + SharedCameraRecoilSystem.cs +CardboardBox/ + Components/CardboardBoxComponent.cs + SharedCardboardBoxSystem.cs +Cargo/BUI/ + CargoConsoleInterfaceState.cs + CargoPalletConsoleInterfaceState.cs + CargoShuttleConsoleBoundUserInterfaceState.cs +Cargo/ + CargoBountyData.cs + CargoBountyHistoryData.cs + CargoOrderData.cs + Components/ + CargoBountyConsoleComponent.cs + CargoOrderConsoleComponent.cs + CargoShuttleComponent.cs + CargoSlipComponent.cs + CashComponent.cs + FundingAllocationConsoleComponent.cs + OverrideSellComponent.cs + PriceGunComponent.cs + SharedCargoTelepadComponent.cs + StationBankAccountComponent.cs + TradeStationComponent.cs + Events/ + CargoConsoleAddOrderMessage.cs + CargoConsoleApproveOrderMessage.cs + CargoConsoleRemoveOrderMessage.cs + CargoPalletAppraiseMessage.cs + CargoPalletSellMessage.cs + PriceCalculationEvent.cs + Prototypes/ + CargoAccountPrototype.cs + CargoBountyGroupPrototype.cs + CargoBountyPrototype.cs + CargoMarketPrototype.cs + CargoProductPrototype.cs + SharedCargoSystem.cs + SharedPriceGunSystem.cs +CartridgeLoader/ + CartridgeComponent.cs + CartridgeLoaderComponent.cs + CartridgeLoaderUiMessage.cs + CartridgeLoaderUiState.cs + CartridgeLoaderVisuals.cs + Cartridges/ + CrewManifestUiState.cs + LogProbePrintMessage.cs + LogProbeUiState.cs + NanoTaskCartridgeComponent.cs + NanoTaskPrintedComponent.cs + NanoTaskUiMessageEvent.cs + NanoTaskUiState.cs + NetProbeUiState.cs + NewsReaderUiMessageEvent.cs + NewsReaderUiState.cs + NotekeeperUiMessageEvent.cs + NotekeeperUiState.cs + SharedNanoTaskCartridgeSystem.cs + WantedListUiState.cs + CartridgeUiMessage.cs + SharedCartridgeLoaderSystem.cs +CCVar/ + CCVars.Accessibility.cs + CCVars.Admin.Ahelp.cs + CCVars.Admin.cs + CCVars.Admin.Logs.cs + CCVars.Admin.Rules.cs + CCVars.Atmos.cs + CCVars.Audio.cs + CCVars.Cargo.cs + CCVars.Chat.Admin.cs + CCVars.Chat.cs + CCVars.Chat.Looc.cs + CCVars.Chat.Ooc.cs + CCVars.Config.cs + CCVars.Console.cs + CCVars.Crewmanifest.cs + CCVars.cs + CCVars.Database.cs + CCVars.Debug.cs + CCVars.Discord.cs + CCVars.Events.cs + CCVars.Explosion.cs + CCVars.Feedback.cs + CCVars.Game.cs + CCVars.Game.Infolinks.cs + CCVars.Ghost.cs + CCVars.Hud.cs + CCVars.Ic.cs + CCVars.Interactions.cs + CCVars.Interface.cs + CCVars.Lighting.cs + CCVars.Mapping.cs + CCVars.Midi.cs + CCVars.Misc.cs + CCVars.Movement.cs + CCVars.Net.cs + CCVars.NPC.cs + CCVars.Parallax.cs + CCVars.Physics.cs + CCVars.Playtest.cs + CCVars.Radiation.cs + CCVars.Replays.cs + CCVars.Salvage.cs + CCVars.Server.cs + CCVars.Shuttle.cs + CCVars.Sounds.cs + CCVars.Tips.cs + CCVars.Viewport.cs + CCVars.Vote.cs + CCVars.Whitelist.cs + CVarAccess/CVarControl.cs +Changeling/ + ChangelingDevourEvents.cs + ChangelingTransformEvents.cs + Components/ + ChangelingClonerComponent.cs + ChangelingDevourComponent.cs + ChangelingDevouredComponent.cs + ChangelingFleshClothingAbilityComponent.cs + ChangelingFleshClothingComponent.cs + ChangelingIdentityComponent.cs + ChangelingStoredIdentityComponent.cs + ChangelingTransformComponent.cs + Systems/ + ChangelingClonerSystem.cs + ChangelingDevourSystem.cs + ChangelingFleshClothingSystem.cs + ChangelingTransformSystem.cs + ChangelingTransformSystem.UI.cs + SharedChangelingIdentitySystem.cs +ChangeNameInContainer/ + ChangeNameInContainerComponent.cs + ChangeNameInContainerSystem.cs +CharacterInfo/SharedCharacterInfoSystem.cs +Charges/Components/ + AutoRechargeComponent.cs + LimitedChargesComponent.cs +Charges/Systems/SharedChargesSystem.cs +Chasm/ + ChasmComponent.cs + ChasmFallingComponent.cs + ChasmSystem.cs +Chat/ + ChatChannel.cs + ChatChannelExtensions.cs + ChatSelectChannel.cs + EmotesEvents.cs + InGameOocMessageAttemptEvent.cs + ISharedChatManager.cs + MsgChatMessage.cs + MsgDeleteChatMessagesBy.cs + Prototypes/ + AutoEmotePrototype.cs + ChatNotificationPrototype.cs + EmotePrototype.cs + EmoteSoundsPrototype.cs + SharedChatEvents.cs + SharedChatSystem.cs + SharedChatSystem.Emote.cs + SharedSuicideSystem.cs + TypingIndicator/ + SharedTypingIndicatorSystem.cs + TypingIndicatorClothingComponent.cs + TypingIndicatorComponent.cs + TypingIndicatorEvents.cs + TypingIndicatorPrototype.cs + TypingIndicatorState.cs + TypingIndicatorVisuals.cs + V2/Moderation/ + ChatCensor.cs + RegexCensor.cs + SimpleCensor.cs + V2/ + Repository/Types.cs + Types.cs +Chemistry/Components/ + DrainableSolutionComponent.cs + DumpableSolutionComponent.cs + FitsInDispenserComponent.cs + InjectorComponent.cs + MixableSolutionComponent.cs + PillComponent.cs + ReactiveContainerComponent.cs + ReagentTankComponent.cs + RefillableSolutionComponent.cs + RehydratableComponent.cs + ScoopableSolutionComponent.cs + SlotBasedConnectedContainerComponent.cs + SmokeAffectedComponent.cs + SmokeComponent.cs + Solution.cs + SolutionComponent.cs + SolutionContainerMixerComponent.cs + SolutionContainerVisualsComponent.cs + SolutionManager/ + ContainedSolutionComponent.cs + DrawableSolutionComponent.cs + ExaminableSolutionComponent.cs + InjectableSolutionComponent.cs + SolutionContainerManagerComponent.cs + SolutionPurgeComponent.cs + SolutionRegenerationComponent.cs + SolutionScannerComponent.cs + SolutionSpikerComponent.cs + SolutionTransferComponent.cs +Chemistry/EntitySystems/ + InjectorSystem.cs + ReactiveContainerSystem.cs + RehydratableSystem.cs + ScoopableSolutionSystem.cs + SharedSolutionContainerMixerSystem.cs + SharedSolutionContainerSystem.Capabilities.cs + SharedSolutionContainerSystem.cs + SharedSolutionContainerSystem.Relays.cs + SolutionPurgeSystem.cs + SolutionRegenerationSystem.cs + SolutionSpikerSystem.cs + SolutionTransferSystem.cs +Chemistry/ + Events/InjectorEvents.cs + InjectOverTimeEvent.cs + Prototypes/InjectorModePrototype.cs + Reaction/ + ChemicalReactionSystem.cs + ITileReaction.cs + MixingCategoryPrototype.cs + ReactionMixerComponent.cs + ReactionMixerSystem.cs + ReactionPrototype.cs + ReactiveComponent.cs + ReactiveGroupPrototype.cs + ReactiveSystem.cs + Reagent/ + DNAData.cs + ReagentData.cs + ReagentId.cs + ReagentMetabolismsSerializer.cs + ReagentPrototype.cs + ReagentQuantity.cs + SharedChemistryGuideDataSystem.cs + SharedChemMaster.cs + SharedReagentDispenser.cs + SharedSolutionHeater.cs + SharedTransferAmount.cs + SolutionScannerSystem.cs + SolutionVisuals.cs +Climbing/Components/ + BonkableComponent.cs + ClimbableComponent.cs + ClimbingComponent.cs + GlassTableComponent.cs +Climbing/Events/ + AttemptClimbEvent.cs + BeforeClimbEvents.cs + ClimbedOnEvent.cs + EndClimbEvent.cs + StartClimbEvent.cs +Climbing/Systems/ClimbSystem.cs +Clock/ + ClockComponent.cs + GlobalTimeManagerComponent.cs + SharedClockSystem.cs +Cloning/ + AcceptCloningEuiMessage.cs + CloningEvents.cs + CloningPodComponent.cs + CloningSettingsPrototype.cs + SharedCloningConsole.cs + SharedCloningSystem.cs +Clothing/ + ClothingEvents.cs + ClothingSpeedModifierComponent.cs + ClothingSpeedModifierSystem.cs + Components/ + AntiGravityClothingComponent.cs + AttachedClothingComponent.cs + ChameleonClothingComponent.cs + ClothingComponent.cs + CursedMaskComponent.cs + FactionClothingComponent.cs + FireProtectionComponent.cs + FleetingClothingComponent.cs + FoldableClothingComponent.cs + HideLayerClothingComponent.cs + LoadoutComponent.cs + MaskComponent.cs + PilotedByClothingComponent.cs + PilotedClothingComponent.cs + SelfUnremovableClothingComponent.cs + SkatesComponent.cs + SpeedModifierContactCapClothingComponent.cs + ToggleableClothingComponent.cs + ToggleClothingComponent.cs + ToggleClothingPrefixComponent.cs + EntitySystems/ + AntiGravityClothingSystem.cs + ClothingSystem.cs + FactionClothingSystem.cs + FireProtectionSystem.cs + FleetingClothingSystem.cs + FoldableClothingSystem.cs + HideLayerClothingSystem.cs + MaskSystem.cs + PilotedClothingSystem.cs + SelfUnremovableClothingSystem.cs + SharedChameleonClothingSystem.cs + SkatesSystem.cs + SpeedModifierContactCapClothingSystem.cs + ToggleableClothingSystem.cs + ToggleClothingPrefixSystem.cs + ToggleClothingSystem.cs + LoadoutSystem.cs + MagbootsComponent.cs + MagbootsSystem.cs + SharedCursedMaskSystem.cs +Clumsy/ + ClumsyComponent.cs + ClumsySystem.cs +Cluwne/CluwneComponent.cs +CombatMode/ + CombatModeComponent.cs + DisarmedEvent.cs + DisarmMalusComponent.cs + Pacification/ + PacificationSystem.cs + PacifiedComponent.cs + PacifismAllowedGunComponent.cs + PacifismDangerousAttackComponent.cs + SharedCombatModeSystem.cs +Communications/ + CommsHackerComponent.cs + SharedCommsHackerSystem.cs + SharedCommunicationsConsoleComponent.cs +ComponentTable/ + ComponentTableComponent.cs + SharedComponentTableSystem.cs +Computer/SharedComputerComponent.cs +Configurable/ + ConfigurationComponent.cs + SharedConfigurationSystem.cs +Construction/Components/ + AnchorableComponent.cs + BlockAnchorOnComponent.cs + ComputerBoardComponent.cs + ElectronicsBoardComponent.cs + FlatpackComponent.cs + FlatpackCreatorComponent.cs + MachineBoardComponent.cs + PartAssemblyComponent.cs +Construction/Conditions/ + EmptyOrWindowValidInTile.cs + EntityWhitelistCondition.cs + IConstructionCondition.cs + NoUnstackableInTile.cs + NoWindowsInTile.cs + TileNotBlocked.cs + TileType.cs + WallmountCondition.cs +Construction/ + ConstructionGraphEdge.cs + ConstructionGraphNode.cs + ConstructionGuide.cs + EntitySystems/ + AnchorableSystem.cs + BlockAnchorOnSystem.cs + Enums.cs + Events.cs + IGraphAction.cs + IGraphCondition.cs + IGraphNodeEntity.cs + IGraphTransform.cs + MachineDeconstructedEvent.cs + MachinePartSystem.cs + NodeEntities/ + BoardNodeEntity.cs + NullNodeEntity.cs + StaticNodeEntity.cs + PartAssemblySystem.cs + Prototypes/ + ConstructionGraphPrototype.cs + ConstructionPrototype.cs + Serialization/GraphNodeEntitySerializer.cs + SharedCanBuildWindowOnTopComponent.cs + SharedConstructionSystem.cs + SharedFlatpackSystem.cs + Steps/ + ArbitraryInsertConstructionGraphStep.cs + ComponentConstructionGraphStep.cs + ConstructionGraphStep.cs + ConstructionGraphStepTypeSerializer.cs + EntityInsertConstructionGraphStep.cs + MaterialConstructionGraphStep.cs + MultipleTagsConstructionGraphStep.cs + PartAssemblyConstructionGraphStep.cs + TagConstructionGraphStep.cs + TemperatureConstructionGraphStep.cs + ToolConstructionGraphStep.cs +ContainerHeld/ + ContainerHeldComponent.cs + ContainerHeldSystem.cs +Containers/ + ContainerCompComponent.cs + ContainerCompSystem.cs + ContainerFillComponent.cs + ContainerFillSystem.cs + DragInsertContainerComponent.cs + DragInsertContainerSystem.cs + EntityTableContainerFillComponent.cs + ExitContainerOnMoveComponent.cs + ExitContainerOnMoveSystem.cs + ItemSlot/ + ItemSlotEvents.cs + ItemSlotsComponent.cs + ItemSlotsLockComponent.cs + ItemSlotsSystem.cs + ItemSlotsSystem.Lock.cs + SharedThrowInsertContainerSystem.cs + SlotBasedConnectedContainerSystem.cs +Contraband/ + ContrabandComponent.cs + ContrabandSeverityPrototype.cs + ContrabandSystem.cs + ShowContrabandDetailsComponent.cs + ShowContrabandSystem.cs +Conveyor/ + ConveyedComponent.cs + ConveyorComponent.cs +Coordinates/ + EntityCoordinatesExtensions.cs + Helpers/SnapgridHelper.cs +Crayon/ + CrayonComponent.cs + SharedCrayonSystem.cs +CrewManifest/SharedCrewManifestSystem.cs +CriminalRecords/Components/ + CriminalRecordsConsoleComponent.cs + CriminalRecordsHackerComponent.cs +CriminalRecords/ + CriminalRecord.cs + CriminalRecordsUi.cs + Systems/ + SharedCriminalRecordsConsoleSystem.cs + SharedCriminalRecordsHackerSystem.cs + SharedCriminalRecordsSystem.cs +Cuffs/Components/ + CuffableComponent.cs + HandcuffComponent.cs +Cuffs/SharedCuffableSystem.cs +Damage/Components/ + ActiveStaminaComponent.cs + ClothingSlowOnDamageModifierComponent.cs + DamageableComponent.cs + DamageContactsComponent.cs + DamagedByContactComponent.cs + DamageExaminableComponent.cs + DamageModifierStatusEffectComponent.cs + DamageOnAttackedComponent.cs + DamageOnAttackedProtectionComponent.cs + DamageOnHighSpeedImpactComponent.cs + DamageOnHoldingComponent.cs + DamageOnInteractComponent.cs + DamageOnInteractProtectionComponent.cs + DamageOtherOnHitComponent.cs + DamagePopupComponent.cs + GodmodeComponent.cs + IgnoreSlowOnDamageComponent.cs + PassiveDamageComponent.cs + RequireProjectileTargetComponent.cs + SlowOnDamageComponent.cs + StaminaComponent.cs + StaminaDamageOnCollideComponent.cs + StaminaDamageOnEmbedComponent.cs + StaminaDamageOnHitComponent.cs + StaminaModifierStatusEffectComponent.cs + StaminaResistanceComponent.cs +Damage/ + DamageModifierSet.cs + DamageSpecifier.cs + DamageVisualizerKeys.cs + Events/ + BeforeForceSayEvent.cs + BeforeStaminaDamageEvent.cs + DamageExamineEvent.cs + RefreshStaminaCritThresholdEvent.cs + StaminaDamageOnHitAttemptEvent.cs + StaminaMeleeHitEvent.cs + ForceSay/ + AllowNextCritSpeechComponent.cs + DamageForceSayComponent.cs + DamageForceSayEvent.cs + Prototypes/ + DamageContainerPrototype.cs + DamageGroupPrototype.cs + DamageModifierSetPrototype.cs + DamageTypePrototype.cs + Systems/ + DamageableSystem.API.cs + DamageableSystem.BenchmarkHelpers.cs + DamageableSystem.cs + DamageableSystem.Events.cs + DamageContactsSystem.cs + DamageExamineSystem.cs + DamageModifierStatusEffect.cs + DamageOnAttackedSystem.cs + DamageOnHighSpeedImpactSystem.cs + DamageOnHoldingSystem.cs + DamageOnInteractSystem.cs + DamagePopupSystem.cs + PassiveDamageSystem.cs + RequireProjectileTargetSystem.cs + SharedDamageOtherOnHitSystem.cs + SharedGodmodeSystem.cs + SharedStaminaSystem.cs + SharedStaminaSystem.Modifier.cs + SharedStaminaSystem.Resistance.cs + SlowOnDamageSystem.cs +Dataset/ + DatasetPrototype.cs + LocalizedDatasetPrototype.cs +Decals/ + ColorPalettePrototype.cs + Decal.cs + DecalChunkUpdateEvent.cs + DecalGridChunkCollectionTypeSerializer.cs + DecalGridComponent.cs + DecalPrototype.cs + PlaceDecalActionEvent.cs + SharedDecalSystem.cs +Defusable/SharedDefusableSystem.cs +Delivery/ + DeliveryBombComponent.cs + DeliveryComponent.cs + DeliveryFragileComponent.cs + DeliveryModifierSystem.cs + DeliveryPriorityComponent.cs + DeliveryRandomMultiplierComponent.cs + DeliverySpawnerComponent.cs + DeliveryVisuals.cs + PrimedDeliveryBombComponent.cs + SharedDeliverySystem.cs +Destructible/ + SharedDestructibleSystem.cs + ThresholdActs.cs + Thresholds/MinMax.cs + Triggers/ + AndTrigger.cs + DamageGroupTrigger.cs + DamageTrigger.cs + DamageTypeTrigger.cs + IThresholdTrigger.cs + OrTrigger.cs +DetailExaminable/ + DetailExaminableComponent.cs + DetailExaminableystem.cs +DeviceLinking/Components/ + RandomGateComponent.cs + TwoWayLeverComponent.cs +DeviceLinking/ + DeviceLinkSinkComponent.cs + DeviceLinkSourceComponent.cs + DevicePortPrototype.cs + Events/ + DeviceLinkOverloadedEvent.cs + LinkAttemptEvent.cs + NewLinkEvent.cs + PortDisconnectedEvent.cs + SignalReceivedEvent.cs + SharedDeviceLinkSystem.cs + SharedLogicGate.cs + SharedRandomGateEvents.cs + Systems/ + SharedRandomGateSystem.cs + TwoWayLeverSystem.cs + TwoWayLeverSignal.cs +DeviceNetwork/Components/ + DeviceListComponent.cs + DeviceNetworkComponent.cs + DeviceNetworkJammerComponent.cs + NetworkConfiguratorComponent.cs +DeviceNetwork/ + DeviceFrequencyPrototype.cs + DeviceNet.cs + DeviceNetworkConstants.cs + Events/ + BeforeBroadcastAttemptEvent.cs + BeforePacketSentEvent.cs + DeviceNetworkPacketEvent.cs + NetworkConfiguratorUIMessages.cs + NetworkConfiguratorUserInterfaceState.cs + NetworkPayload.cs + Systems/ + SharedDeviceListSystem.cs + SharedDeviceNetworkJammerSystem.cs + SharedDeviceNetworkSystem.cs + SharedNetworkConfiguratorSystem.cs +Devour/ + Components/DevourerComponent.cs + DevourSystem.cs +Dice/ + DiceComponent.cs + SharedDiceSystem.cs +Directions/SharedDirectionExtensions.cs +DisplacementMap/DisplacementData.cs +Disposal/Mailing/ + MailingUnitComponent.cs + MailingUnitUiMessages.cs + SharedDisposalRouterComponent.cs + SharedDisposalTaggerComponent.cs + SharedMailingUnitSystem.cs +Disposal/Tube/ + DisposalEntryComponent.cs + SharedDisposalTubeComponent.cs +Disposal/Unit/ + BeforeDisposalFlushEvent.cs + DisposalUnitComponent.cs + SharedDisposalTubeSystem.cs + SharedDisposalUnitSystem.cs +DoAfter/ + ActiveDoAfterComponent.cs + DoAfter.cs + DoAfterArgs.cs + DoAfterArgsComponent.cs + DoAfterComponent.cs + DoAfterEvent.cs + SharedDoAfterSystem.cs + SharedDoAfterSystem.Update.cs +Doors/ + AirlockWireStatus.cs + Components/ + AirlockComponent.cs + DoorBoltComponent.cs + DoorComponent.cs + FirelockComponent.cs + TurnstileComponent.cs + DoorEvents.cs + Electronics/DoorElectronicsComponent.cs + Systems/ + SharedAirlockSystem.cs + SharedDoorSystem.Bolts.cs + SharedDoorSystem.cs + SharedFirelockSystem.cs + SharedTurnstileSystem.cs +DragDrop/ + DragDropRequestEvent.cs + DraggableEvents.cs + SharedDragDropSystem.cs +Dragon/ + DragonEvents.cs + DragonRiftComponentState.cs + SharedDragonRiftComponent.cs +DrawDepth/DrawDepth.cs +Drowsiness/ + DrowsinessStatusEffectComponent.cs + DrowsinessSystem.cs +Drugs/SeeingRainbowsComponent.cs +Drunk/ + DrunkStatusEffectComponent.cs + SharedDrunkSystem.cs +Effects/ + ColorFlashEffectComponent.cs + ColorFlashEffectEvent.cs + SharedColorFlashEffectSystem.cs +Electrocution/Components/ + ElectrifiedComponent.cs + ElectrocutionHUDVisualsComponent.cs + ShowElectrocutionHUDComponent.cs +Electrocution/ + ElectrocutedComponent.cs + ElectrocutionEvents.cs + InsulatedComponent.cs + InsulatedSystem.cs + SharedElectrocution.cs + SharedElectrocutionSystem.cs +Emag/Components/ + EmagComponent.cs + EmaggedComponent.cs +Emag/Systems/EmagSystem.cs +Emoting/ + EmoteEvents.cs + EmoteSystem.cs + EmotingComponent.cs +Emp/ + EmpDisabledComponent.cs + EmpResistanceComponent.cs + SharedEmpSystem.cs +Engineering/Components/ + DisassembleOnAltVerbComponent.cs + InflatableSafeDisassemblyComponent.cs +Engineering/Systems/ + DisassembleOnAltVerbSystem.cs + InflatableSafeDisassemblySystem.cs +Ensnaring/Components/ + EnsnareableComponent.cs + EnsnaringComponent.cs +Ensnaring/ + EnsnareableVisuals.cs + SharedEnsnareableSystem.cs +EntityConditions/Conditions/Body/ + BreathingEntityCondition.cs + HungerEntityConditionSystem.cs + InternalsEntityConditionSystem.cs + MetabolizerTypeEntityConditionSystem.cs + MobStateEntityConditionSystem.cs +EntityConditions/Conditions/ + DamageGroupEntityConditionSystem.cs + DamageTypeEntityConditionSystem.cs + JobEntityConditionSystem.cs + NestedCondition.cs + ReagentEntityConditionSystem.cs + Tags/ + HasAllTagsEntityConditionSystem.cs + HasAnyTagEntityConditionSystem.cs + HasTagEntityConditionSystem.cs + TemperatureEntityConditionSystem.cs + TemplateEntityConditionSystem.cs + TotalDamageEntityConditionSystem.cs +EntityConditions/ + EntityConditionPrototype.cs + SharedEntityConditionsSystem.cs + SharedEntityConditionsSystem.Nested.cs +EntityEffects/Effects/ + AdjustAlertEntityEffectSystem.cs + AdjustTemperatureEntityEffectsSystem.cs + ArtifactEntityEffectsSystem.cs + Atmos/ + CreateGasEntityEffect.cs + ExtinguishEntityEffectSystem.cs + FlammableEntityEffect.cs + IgniteEntityEffect.cs + Body/ + CleanBloodstreamEntityEffectSystem.cs + EyeDamageEntityEffectSystem.cs + ModifyBleedEntityEffectSystem.cs + ModifyBloodLevelEntityEffectSystem.cs + ModifyLungGasEntityEffectSystem.cs + OxygenateEntityEffect.cs + ReduceRottingEntityEffectSystem.cs + SatiateEntityEffectSystem.cs + VomitEntityEffectSystem.cs + Botany/PlantAttributes/ + BasePlantAdjustAttributeEntityEffect.cs + PlantAdjustHealth.cs + PlantAdjustMutationLevel.cs + PlantAdjustMutationMod.cs + PlantAdjustNutrition.cs + PlantAdjustPests.cs + PlantAdjustPotency.cs + PlantAdjustToxins.cs + PlantAdjustWater.cs + PlantAdjustWeeds.cs + PlantAffectGrowth.cs + PlantChangeStat.cs + PlantCryoxadone.cs + PlantDestroySeeds.cs + PlantDiethylamine.cs + PlantPhalanximine.cs + PlantRemoveKudzu.cs + PlantRestoreSeeds.cs + RobustHarvest.cs + Botany/ + PlantMutateChemicalsEntityEffect.cs + PlantMutateGasesEntityEffect.cs + PlantMutateHarvestEntityEffect.cs + PlantMutateSpeciesChangeEntityEffect.cs + Damage/ + DistributedHealthChangeEntityEffectSystem.cs + EvenHealthChangeEntityEffectSystem.cs + HealthChangeEntityEffectSystem.cs + EmoteEntityEffectSystem.cs + EntitySpawning/ + BaseSpawnEntityEntityEffect.cs + SpawnEntityEntityEffectSystem.cs + SpawnEntityInContainerEntityEffectSystem.cs + SpawnEntityInContainerOrDropEntityEffectSystem.cs + SpawnEntityInInventoryEntityEffectSystem.cs + ExplodeEntityEffect.cs + GlowEntityEffectSystem.cs + MakeSentientEntityEffect.cs + MetaData/DestructibleActEntityEffectSystem.cs + NestedEffect.cs + PolymorphEntityEffect.cs + ResetNarcolepsyEntityEffectSystem.cs + SlipifyEntityEffectSystem.cs + Solution/ + AddReagentToSolutionEntityEffectSystem.cs + AdjustReagentEntityEffectSystem.cs + AreaReactionEntityEffect.cs + SolutionTemperatureEntityEffectsSystem.cs + StatusEffects/ + BaseStatusEffectEntityEffect.cs + DrunkEntityEffectSystem.cs + ElectrocuteEntityEffectSystem.cs + GenericStatusEffectEntityEffectSystem.cs + JitterEntityEffectSystem.cs + ModifyKnockdownEntityEffectSystem.cs + ModifyParalysisEntityEffectSystem.cs + ModifyStatusEffectEntityEffectSystem.cs + MovementSpeedModifierEntityEffectSystem.cs + TemplateEntityEffectSystem.cs + Transform/ + EmpEntityEffectSystem.cs + ExplosionEntityEffect.cs + FlashEntityEffectSystem.cs + PopupMessageEntityEffectSystem.cs + WashCreamPieEntityEffectSystem.cs + ZombieEntityEffectsSystem.cs +EntityEffects/ + EntityEffect.cs + EntityEffectEvent.cs + EntityEffectPrototype.cs + SharedEntityEffectsSystem.cs + SharedEntityEffectsSystem.Nested.cs +EntityList/EntityListPrototype.cs +EntityTable/Conditions/ + EntityTableCondition.cs + HasBudgetCondition.cs + MaxRuleOccurenceCondition.cs + PlayerCountCondition.cs + ReoccurrenceDelayCondition.cs + RoundDurationCondition.cs +EntityTable/EntitySelectors/ + AllSelector.cs + EntityTableSelector.cs + EntityTableTypeSerializer.cs + EntSelector.cs + GroupSelector.cs + NestedSelector.cs + NoneSelector.cs +EntityTable/ + EntityTablePrototype.cs + EntityTableSystem.cs + ValueSelector/ + BinomialNumberSelector.cs + ConstantNumberSelector.cs + NumberSelector.cs + NumberSelectorTypeSerializer.cs + RangeNumberSelector.cs +Entry/EntryPoint.cs +Eui/ + EuiMessageBase.cs + EuiStateBase.cs + MsgEuiCtl.cs + MsgEuiMessage.cs + MsgEuiState.cs +Examine/ + ExaminerComponent.cs + ExamineSystemMessages.cs + ExamineSystemShared.cs + ExamineSystemShared.Group.cs + GroupExamineComponent.cs +Exchanger/ExchangerDoAfterEvent.cs +Execution/ + DoAfterEvent.cs + ExecutionComponent.cs + SharedExecutionSystem.cs +Explosion/Components/ + ExplosionResistanceComponent.cs + ExplosionVisualsComponent.cs + ExplosiveComponent.cs + ScatteringGrenadeComponent.cs + SharedClusterGrenadeComponent.cs +Explosion/EntitySystems/ + SharedExplosionSystem.cs + SharedScatteringGrenadeSystem.cs +Explosion/ + ExplosionEvents.cs + ExplosionPrototype.cs +Eye/Blinding/Components/ + ActivatableUIRequiresVisionComponent.cs + BlindableComponent.cs + BlindfoldComponent.cs + BlurryVisionComponent.cs + EyeClosingComponent.cs + EyeProtectionComponent.cs + RequiresEyeProtectionComponent.cs + TemporaryBlindnessComponent.cs + VisionCorrectionComponent.cs +Eye/Blinding/Systems/ + ActivatableUIRequiresVisionSystem.cs + BlindableSystem.cs + BlindfoldSystem.cs + BlurryVisionSystem.cs + EyeClosingSystem.cs + EyeProtectionSystem.cs + TemporaryBlindnessSystem.cs +Eye/VisibilityFlags.cs +Fax/ + AdminFaxEui.cs + Components/ + FaxableObjectComponent.cs + FaxecuteComponent.cs + FaxMachineComponent.cs + DamageOnFaxecuteEvent.cs + FaxVisuals.cs + SharedFax.cs + Systems/FaxecuteSystem.cs +FeedbackSystem/ + FeedbackPopupMessage.cs + FeedbackPopupPrototype.cs + SharedFeedbackManager.cs + SharedFeedbackManager.Events.cs +FingerprintReader/ + FingerprintReaderComponent.cs + FingerprintReaderSystem.cs +FixedPoint/ + FixedPoint2.cs + FixedPoint4.cs +Flash/Components/ + ActiveFlashComponent.cs + DamagedByFlashingComponent.cs + FlashComponent.cs + FlashedComponent.cs + FlashImmunityComponent.cs +Flash/ + DamagedByFlashingSystem.cs + FlashEvents.cs + FlashVisuals.cs + SharedFlashSystem.cs +Fluids/ + AbsorbentComponent.cs + Components/ + DrainComponent.cs + EquipSprayComponent.cs + EvaporationComponent.cs + PuddleComponent.cs + SpillableComponent.cs + SpillWhenWornComponent.cs + SprayComponent.cs + SpraySafetyComponent.cs + EntitySystems/ + DrainSystem.cs + SharedSpraySystem.cs + SolutionDumpingSystem.cs + SpillWhenWornSystem.cs + Events.cs + PuddleVisuals.cs + SharedAbsorbentSystem.cs + SharedPuddleDebugOverlaySystem.cs + SharedPuddleSystem.cs + SharedPuddleSystem.Evaporation.cs + SharedPuddleSystem.Spillable.cs + SharedSprayComponent.cs + SpraySafetySystem.cs +Foldable/ + DeployFoldableComponent.cs + DeployFoldableSystem.cs + FoldableComponent.cs + FoldableSystem.cs +Follower/Components/ + FollowedComponent.cs + FollowerComponent.cs + OrbitVisualsComponent.cs +Follower/FollowerSystem.cs +Forensics/Components/ + DnaComponent.cs + FingerprintComponent.cs + FingerprintMaskComponent.cs +Forensics/ + Events.cs + ForensicScannerEvent.cs + Systems/SharedForensicsSystem.cs +Friction/ + TileFrictionController.cs + TileFrictionModifier.cs +Friends/ + Components/PettableFriendComponent.cs + Systems/PettableFriendSystem.cs +GameTicking/Components/ + ActiveGameRuleComponent.cs + DelayedStartRuleComponent.cs + EndedGameRuleComponent.cs + GameRuleComponent.cs +GameTicking/ + PlayerBeforeSpawnEvent.cs + PlayerSpawnCompleteEvent.cs + Prototypes/LobbyBackgroundPrototype.cs + RoundRestartCleanupEvent.cs + Rules/ + DynamicRuleComponent.cs + DynamicRuleCostComponent.cs + SharedGameTicker.cs +GameWindow/RequestWindowAttentionEvent.cs +Gateway/ + GatewayUi.cs + SharedGatewayGeneratorSystem.cs +Ghost/ + GhostComponent.cs + GhostHearingComponent.cs + GhostOnMoveComponent.cs + GhostRoleRadioEvents.cs + ReturnToBodyEuiMessage.cs + Roles/ + Components/GhostRoleMobSpawnerComponent.cs + GhostRolePrototype.cs + GhostRolesEuiMessages.cs + MakeGhostRoleEuiState.cs + Raffles/ + GhostRoleRaffleSettings.cs + GhostRoleRaffleSettingsPrototype.cs + SharedGhostRoleSystem.cs + SharedGhostSystem.cs + SpectralComponent.cs +GhostKick/MsgGhostKick.cs +GhostTypes/ + GhostSpriteStateComponent.cs + GhostSpriteStateSystem.cs + LastBodyDamageComponent.cs + StoreDamageTakenOnMindComponent.cs + StoreDamageTakenOnMindSystem.cs +Gibbing/ + GibbingSystem.cs + GibOnRoundEndComponent.cs +GlobalUsings.cs +Glue/ + GlueComponent.cs + GluedComponent.cs + GlueSystem.cs +GPS/ + Components/HandheldGPSComponent.cs + Systems/HandheldGpsSystem.cs +Gravity/ + FloatingVisualsComponent.cs + GravityAffectedComponent.cs + GravityChangedEvent.cs + GravityComponent.cs + GravityGeneratorComponent.cs + GravityShakeComponent.cs + SharedFloatingVisualizerSystem.cs + SharedGravityGeneratorSystem.cs + SharedGravitySystem.cs + SharedGravitySystem.Shake.cs +GridPreloader/ + Prototypes/PreloadedGridPrototype.cs + Systems/SharedGridPreloaderSystem.cs +Guardian/ + GuardianCreatorDoAfterEvent.cs + GuardianToggleActionEvent.cs +Guidebook/ + Events.cs + GuidebookData.cs + GuidebookDataAttribute.cs + GuideEntry.cs +Hands/Components/ + ExtraHandsEquipmentComponent.cs + HandsComponent.cs +Hands/EntitySystems/ + ExtraHandsEquipmentSystem.cs + SharedHandsSystem.AI.cs + SharedHandsSystem.cs + SharedHandsSystem.Drop.cs + SharedHandsSystem.EventListeners.cs + SharedHandsSystem.Interactions.cs + SharedHandsSystem.Pickup.cs + SharedHandsSystem.Relay.cs + SharedHandsSystem.Whitelist.cs +Hands/HandEvents.cs +HealthExaminable/ + HealthExaminableComponent.cs + HealthExaminableSystem.cs +HijackBeacon/ + ActiveHijackBeaconComponent.cs + HijackBeaconComponent.cs + HijackBeaconSystem.cs +Holiday/HolidayVisuals.cs +Holopad/ + HolographicAvatarComponent.cs + HolopadComponent.cs + HolopadHologramComponent.cs + HolopadUserComponent.cs + SharedHolopadSystem.cs +Holosign/ + HolosignProjectorComponent.cs + HolosignSystem.cs +HotPotato/ + ActiveHotPotatoComponent.cs + HotPotatoComponent.cs + SharedHotPotatoSystem.cs +HUD/HudThemePrototype.cs +Humanoid/ + HairStyles.cs + HideableHumanoidLayersComponent.cs + HumanoidCharacterAppearance.cs + HumanoidProfileComponent.cs + HumanoidProfileExportV1.cs + HumanoidProfileExportV2.cs + HumanoidProfileSystem.cs + HumanoidVisualLayers.cs + HumanoidVisualLayersExtension.cs + Markings/ColoringTypes/ + CategoryColoring.cs + EyeColoring.cs + SimpleColoring.cs + SkinColoring.cs + TattooColoring.cs + Markings/ + Marking.cs + MarkingColoring.cs + MarkingManager.cs + MarkingPrototype.cs + MarkingsGroupPrototype.cs + NamingSystem.cs + Prototypes/ + HumanoidProfilePrototype.cs + RandomHumanoidSettingsPrototype.cs + SpeciesPrototype.cs + Sex.cs + SharedHideableHumanoidLayersSystem.cs + SharedHumanoidMarkingModifierSystem.cs + SkinColorationPrototype.cs +IconSmoothing/ + RandomIconSmoothComponent.cs + SharedRandomIconSmoothSystem.cs + SmoothEdgeComponent.cs +IdentityManagement/Components/ + IdentityBlockerComponent.cs + IdentityComponent.cs +IdentityManagement/ + Identity.cs + IdentitySystem.cs + TryGetIdentityShortInfoEvent.cs +IgnitionSource/Components/ + MatchboxComponent.cs + MatchstickComponent.cs +IgnitionSource/EntitySystems/ + MatchboxSystem.cs + MatchstickSystem.cs +IgnitionSource/ + IgnitionEvent.cs + IgnitionSourceComponent.cs + SharedIgnitionSourceSystem.cs +Imperial/Atmos/AtmosphericRCD/AtmosphericRCDAmmo/ + AtmosphericRCDAmmoComponent.cs + AtmosphericRCDAmmoSystem.cs +Imperial/Atmos/AtmosphericRCD/ + AtmosphericRCDComponent.cs + AtmosphericRCDDeconstructableComponent.cs + AtmosphericRCDSystem.cs + Events/ + AtmosphericRCDConstructionGhostRotationEvent.cs + AtmosphericRCDSystemMessageEvent.cs + enum.cs + RCDDoAfterEvent.cs +Imperial/Atmos/Hydrogen/Pipes/Components/ + HydrogenGasPressurePumpComponent.cs + HydrogenGasVolumePumpComponent.cs + Shared/ + SharedHydrogenGasFilterComponent.cs + SharedHydrogenGasMixerComponent.cs + SharedHydrogenGasPressurePumpComponent.cs + SharedHydrogenGasVolumePumpComponent.cs +Imperial/Atmos/Hydrogen/Pipes/SharedSystems/ + SharedHydrogenGasPressurePumpSystem.cs + SharedHydrogenGasVolumePumpSystem.cs +Imperial/Atmos/Prototypes/ + AtmosphericRCDPrototype.cs + GasPhazeReactionPrototype.cs +Imperial/ + Atmos/Structures/PPGenerator/enum.cs + ChemistryRework/ + EffectsConditions/TotalDamageByGroups.cs + EntityEffects/ + ChangeMarkingColor.cs + Gib.cs + Luminescence/ + Luminescence.cs + LuminescenceTrackerComponent.cs + RemoveMark.cs + Resurrection.cs + Types/ChemicalMarkingCategory.enum.cs + Enums.cs + ReactionEffects/ + GravityReactionEffect.cs + ImperialFlashReactionEffect.cs + TeleportReactionEffect.cs + CrewSkills/CrewSkillsComponent.cs + Crook/Visuals/MetalDetectorVisuals.cs + Damage/DamageModifierStorageComponent.cs + DeimonFly/Storage/PunishOnStorageTakeComponent.cs + ElectroMouse/Components/ + ElectroMouseComponent.cs + ElectroMouseShieldComponent.cs + ElectroMouse/ElectroMouseEvents.cs + EmergencyButton/Components/EmergencyButtonComponent.cs + EnergyCore/Components/ + CoreAccessComputerComponent.cs + SharedCoreAccessComponent.cs + EnergyCore/ + Enums/EnergyCoreEnums.cs + Interface/ + Messages/CoreAccessInterfaceMessage.cs + States/CoreAccessInterfaceState.cs + Systems/SharedCoreAccessComputerSystem.cs + EntityLayer/ + EntityLayerComponent.cs + Enums.cs + Entry.cs + EverythingStamp/EverythingStampComponent.cs + Fredik21/MTFCallPresetPrototype.cs + HardsuitInjection/Ampula/ + AmpulaComponent.cs + AmpulaSystem.cs + HardsuitInjection/Events/Local/ + EjectionEvent.cs + InjectionEvent.cs + ToggleECEvent.cs + HardsuitInjection/Events/Network/ + ToggleSlotDoAfterEvent.cs + UpdateEvent.cs + HardsuitInjection/ + InjectComponent.cs + InjectNeedComponent.cs + InjectSystem.ActionEvents.cs + InjectSystem.BaseEvents.cs + InjectSystem.cs + InjectSystem.DoAfterEvents.cs + InjectSystem.Helpers.cs + Helpers/ + ColorHelper.cs + CoordsHelper.cs + FactionHelper.cs + MouseInputSystem/Events/ + MousePositionRefreshEvent.cs + Network/ + MousePositionRequestEvent.cs + MousePositionResponseEvent.cs + MouseInputSystem/SharedMouseInputSystem.cs + ICCVars/ICCVars.cs + ImperialBorgs/ + BorgEngElectronicsBoard.cs + BorgHypoComponent.cs + BorgHypoSolution.cs + Events/ + BorgHypoComponentState.cs + ChangeReagentAction.cs + ChangeReagentEvent.cs + OpenBorgHypoUIEvent.cs + ImperialBorgHandsComponent.cs + MimicryComponent.cs + ImperialLightning/ + Events/SpawnLightningEffectsMessage.cs + SharedImperialLightningSystem.cs + ImperialScatteringGrenade/ + ImperialScatteringGrenadeComponent.cs + SharedImperialScatteringGrenadeSystem.cs + ImperialStore/ + Enums.cs + Events/Messages/ + ImperialStoreBuyListingMessage.cs + ImperialStoreRequestRefundMessage.cs + ImperialStoreRequestUpdateInterfaceMessage.cs + ImperialStoreRequestWithdrawMessage.cs + Events/RefEvents/ + ImperialRefundEntityDeletedEvent.cs + ImperialStoreAddedEvent.cs + ImperialStoreRemovedEvent.cs + Events/States/ImperialStoreUpdateState.cs + Helpers/ + ImperialListingCondition.cs + ImperialListingLocalisationHelpers.cs + ImperialStoreComponent.cs + Prototypes/ + ImperialCurrencyPrototype.cs + ImperialListingPrototype.cs + ImperialStoreCategoryPrototype.cs + ImperialStorePresetPrototype.cs + SharedImperialStoreSystem.cs + ImperialVehicle/Components/ + ImperialVehicleComponent.cs + ImperialVehiclePilotComponent.cs + ImperialVehicle/ + Enums.cs + Events/HornActionEvent.cs + SharedImperialVehicleSystem.cs + SharedImperialVehicleSystem.Helpers.cs + JanitorsResponseTeam/Components/ + SolutionDynamicColorOfStampComponent.cs + TrashComponent.cs + JanitorsResponseTeam/SolutionDynamicColorSystem.cs + Lavaland/DashAbility/ + DashAction.cs + DashComponent.cs + DashSystem.cs + Localization/ + ContentLocalizationManager.cs + RussianPlura.cs + MANDARIN/MANDARINComponent.cs + Medical/CustomHypospray/ + Components/CustomHyposprayComponent.cs + Events/ + DoAfterCustomHyposprayEvent.cs + HyposprayUseDelaySystem.cs + Medieval/ + Admin/MessageNotif/MessageNotifPrototype.cs + Administration/Nrp/ + NrpCCVars.cs + NrpCurseComponent.cs + NrpPanelEuiMessages.cs + EntityEffects/Effects/ + AddMagicEssence.cs + AddReagentToSolution.cs + EntityEffects/EffectsConditions/ + MagicTargetMobStateCondition.cs + TargetContainsComponentsCondition.cs + Magic/DataDefinition/ + MedievalSpellDoAfterArgs.cs + MedievalSpellSpeech.cs + Spells/EntityAiming/Data/ + MedievalEntityTargetProjectileSpellData.cs + MedievalHomingProjectilesSpellData.cs + MedievalLightningSpellData.cs + MedievalSpawnAimingEntityData.cs + Spells/ + EntityAiming/MedievalEntityAimingSpellData.cs + Instant/Data/ + MedievalInstantSpawnData.cs + MedievalSpawnInHandSpellData.cs + Instant/MedievalInstantSpellData.cs + MedievalSpellData.cs + Target/Data/ + MedievalProjectileSpellData.cs + MedievalSpawnSpellData.cs + MedievalTeleportSpellData.cs + Target/MedievalTargetSpellData.cs + Magic/ + Enumcs.cs + Events/DoAfter/ + MedievalCastEntityTargetProjectileSpellDoAfterEvent.cs + MedievalCastHomingProjectilesSpellDoAfterEvent.cs + MedievalCastLightningSpellDoAfterEvent.cs + MedievalCastProjectileSpellDoAfterEvent.cs + MedievalCastSpawnSpellDoAfterEvent.cs + MedievalCastTeleportSpellDoAfterEvent.cs + MedievalInstantSpawnDoAfterEvent.cs + MedievalSpawnAimingEntityDoAfterEvent.cs + MedievalSpawnInHandSpellDoAfterEvent.cs + MedievalSpellDoAfterEvent.cs + Events/ + MedievalActionStartTargetingEvent.cs + MedievalActionStopTargetingEvent.cs + MedievalAfterAimingSpawnBySpellEvent.cs + MedievalAfterCastSpellEvent.cs + MedievalAfterSpawnEntityBySpellEvent.cs + MedievalFailCastSpellEvent.cs + RefEvents/ + MedievalBeforeAimingSpawnBySpellEvent.cs + MedievalBeforeCastSpellEvent.cs + MedievalBeforeSpawnEntityBySpellEvent.cs + MedievalSpeakSpellEvent.cs + Relay/MedievalSpellRelayEvent.cs + Spells/EntityAiming/ + MedievalEntityAimingSpellEvent.cs + Spells/ + MedievalEntityTargetProjectileSpellEvent.cs + MedievalHomingProjectilesSpellEvent.cs + MedievalLightningSpellEvent.cs + MedievalSpawnAimingEntityEvent.cs + Spells/Instant/ + MedievalInstantSpellEvent.cs + Spells/ + MedievalInstantSpawnEvent.cs + MedievalSpawnInHandSpellEvent.cs + Spells/Target/ + MedievalTargetSpellEvent.cs + Spells/ + MedievalProjectileSpellEvent.cs + MedievalSpawnSpellEvent.cs + MedievalTeleportSpellEvent.cs + Interfaces/ + IMedievalDoAfterSpell.cs + IMedievalSpeakSpell.cs + ITrainingResult.cs + MedievalSpellCasterComponent.cs + Other/Mana/ + ManaComponent.cs + ManaDrainSpellComponent.cs + ManaRegen/ + ManaRegenComponent.cs + ManaRegenSystem.cs + ManaSystem.cs + Modifiers/ + ManaJobModifierComponent.cs + ManaTraitModifierComponent.cs + Other/Minigames/MedievalArrowsMinigame/ + Enums.cs + Events/Network/ + MedievalArrowInvalidCombination.cs + MedievalArrowValidCombination.cs + MedievalArrowsMinigameComponent.cs + SharedMedievalArrowsMinigameSystem.cs + Other/OverlayHelpers/ + DataDefinition/PrespawnedEntitySprite.cs + Events/Network/SpawnedEntitySpriteRotateEvent.cs + SharedShowSpawnedEntitySystem.cs + ShowSpawnedEntityComponent.cs + Other/ProjectileCursorFollower/ + ProjectileCursorFollowerComponent.cs + ProjectileCursorFollowerSystem.cs + Other/Shaders/ + FireballDistortion/FireballDistortionComponent.cs + MedievalParticles/MedievalParticlesComponent.cs + Other/Spells/MedievalSpawnOnlyInFreeHands/ + MedievalSpawnOnlyInFreeHandsComponent.cs + MedievalSpawnOnlyInFreeHandsSystem.cs + Other/Spells/MedievalSpellTeleportEffect/ + MedievalSpellTeleportEffectComponent.cs + SharedMedievalSpellTeleportEffectSystem.cs + Other/Spells/ + MedievalTargetOverlayOnAiming/MedievalTargetOverlayOnAimingComponent.cs + TurnedToStone/Events/ + AfterBecomeFromStone.cs + AfterTurnetToStone.cs + BeforeTurnetToStone.cs + TurnedToStone/ + SharedTurnedToStoneSystem.cs + TurnedToStoneComponent.cs + SharedMedievalMagicSystem.cs + SharedMedievalMagicSystem.EntityAiming.cs + SharedMedievalMagicSystem.Instant.cs + SharedMedievalMagicSystem.Target.cs + SpellTypes/ + MedievalInstantSpellComponent.cs + MedievalTargetSpellComponent.cs + MedievalMeleeBadTargetComponent.cs + MedievalMeleeRepairComponent.cs + MedievalMeleeRepairManComponen.cs + MindPacified/Components/ + GiveMindPacifiedComponent.cs + MindPacifiedComponent.cs + MindPacified/MindPacifiedSystem.cs + Minigames/ + DataDefinition/MinigameData.cs + Events/ + AfterMinigameAddedEvent.cs + BeforeMinigameAddedEvent.cs + LoseInMinigameEvent.cs + Network/StartMinigameEvent.cs + WinInMinigamEvent.cs + InMinigameComponent.cs + MinigameComponent.cs + Prototypes/MinigamePrototype.cs + SharedMinigamesSystem.cs + MiningWeapons/EmpoweredAttacks/EarthshakerStrike/ + EarthshakerStrikeComponent.cs + Events/ + EarthshakerStrikeDoAfterEvent.cs + EarthshakerStrikeEvent.cs + SharedAttacksSystem.EarthshakerStrike.cs + UserEarthshakerStrikeComponent.cs + MiningWeapons/EmpoweredAttacks/EnhancedShot/ + EnhancedShotComponent.cs + Events/ + EnhancedShotDoAfterEvent.cs + EnhancedShotEvent.cs + SharedAttacksSystem.EnhancedShot.cs + UserEnhancedShotComponent.cs + MiningWeapons/EmpoweredAttacks/PiercingLunge/Events/ + PiercingLungeDoAfterEvent.cs + PiercingLungeEvent.cs + MiningWeapons/EmpoweredAttacks/PiercingLunge/ + PiercingLungeComponent.cs + SharedAttacksSystem.PiercingLunge.cs + UserPiercingLungeComponent.cs + MiningWeapons/EmpoweredAttacks/ + SharedAttacksSystem.cs + SharedAttacksSystem.Helpers.cs + MiningWeapons/ + MiningWeaponsHelpers.cs + Smasher/ + ChargeData.cs + Components/ + ShieldActiveComponent.cs + ShieldDecayComponent.cs + SmasherChargingComponent.cs + SmasherComponent.cs + Enums.cs + Events/ + HideShieldEffectEvent.cs + ShowShieldEffectEvent.cs + UpdateSmasherStateEvent.cs + SharedSmasherSystem.cs + Mobs/Phantomor/ + PhantomorMovementBlockComponent.cs + PhantomorSummonTentacleAction.cs + PhantomorTentacleSystem.cs + NewRad/ + EntitySystems/NewRad.cs + NewRad.cs + Ninja/NinjaHeadset/ + Events/NinjaHackDoAfterEvent.cs + NinjaHeadsetComponent.cs + Ninja/Objectives/LeaveNoTrace/DataDefinition/ + GlitchShaderParametersData.cs + RevealTextureData.cs + Ninja/Objectives/LeaveNoTrace/Events/ + NinjaHideAttemptEvent.cs + NinjaHideEvent.cs + NinjaRevealedAttemptEvent.cs + NinjaRevealedEvent.cs + Ninja/Objectives/LeaveNoTrace/ + LeaveNoTraceComponent.cs + LeaveNoTraceConditionComponent.cs + SharedLeaveNoTraceSystem.cs + Nrp/ImperialMedievalNrpIconPrototype.cs + PhaseSpace/Fade/ + PhaseSpaceFadeDistortionComponent.cs + PhaseSpaceFadeShadowComponent.cs + PhaseSpace/ + PhaseSpaceComponent.cs + PhaseSpaceDistortionComponent.cs + PhaseSpaceShadowComponent.cs + Prototypes/ + PhaseSpaceDistortion.cs + PhaseSpaceShadow.cs + SharedPhaseSpaceSystem.cs + PiratesNewHorizon/ + EntityEffects/ResistChange.cs + GPS/Events/GPSTrackerRemoveDoAfterEvent.cs + Pirate/SharedPirateSystem.cs + StatusIcons/ + PirateCaptainComponent.cs + PirateComponent.cs + PlantsAnalyzer/ + PlantsAnalyzerDoAfterEvent.cs + PlantsAnalyzerScannedUserMessage.cs + PlantsAnalyzerUiKey.cs + Playablecards/ + FoldableCardComponent.cs + FoldableCardSystem.cs + Power/Components/ + SupermatterGasComponent.cs + SupermatterIntegrityComponent.cs + Power/GasReactions/ + ISupermatterGasReaction.cs + Reactions/ + SupermatterAntiNobliumShutdownReaction.cs + SupermatterDisableTouchReaction.cs + SupermatterEventSpeedMultiplierReaction.cs + SupermatterIntegrityRegenReaction.cs + SupermatterLightningMultiplierReaction.cs + SupermatterRadiationMultiplierReaction.cs + Power/ + Prototypes/SupermatterGasReactionPrototype.cs + SupermatterShutdownDoAfterEvent.cs + PrinterDoc/PrinterDocCheckIdCardMessage.cs + Prying/events/PryingEvent.cs + Restriction/ + Components/RestrictionComponent.cs + RestrictionSystem.cs + Revolution/ + ConsentRequestedEuiMessage.cs + ConsentRequestedState.cs + RemoveConversionDeniedAlertEvent.cs + SCP/ + ChaseMusic/Events/ImperialSCPChaseMusicActionEvent.cs + NothingThere/Events/ImperialNothingThereGoodbyeEvent.cs + SCP106/Events/ + SCP106DestroyPuddleEvent.cs + SCP106DimensionSwitchEvent.cs + SCP106DoAfterGhostPuddleEvent.cs + SCP106DoAfterPuddleEvent.cs + SCP106PuddleSpawnEvent.cs + SCP106TransmissionPuddleEvent.cs + ShockWave/ + Events/ShockWaveEntityCollideEvent.cs + SharedShockWaveSystem.cs + ShockWaveDistortionComponent.cs + Sponsors/SponsorInfo.cs + StaminaResistance/Events/ImperialModifyStaminaEvent.cs + TargetOverlay/Events/ + AddTargetOverlayEvent.cs + RemoveTargetOverlayEvent.cs + TargetOverlayShootEvent.cs + TargetOverlay/SharedTargetOverlaySystem.cs + Traits/ + ImperialLegsParalyzedComponent.cs + SharedImperialLegsParalyzedSystem.cs + Vampire/Components/ + DamageOnContactComponent.cs + GhoulComponent.cs + VampireAbilityListPrototype.cs + VampireBuffComponent.cs + VampireComponent.cs + VampireConvertedGhoulsPurposesComponent.cs + VampireDrinkBloodPurposesComponent.cs + VampireJerkComponent.cs + VampireJerkOnContactComponent.cs + VampireTrapOnTriggerComponent.cs + Vampire/Event/ + VampireAbilityType.cs + VampireActionEvent.cs + VampireDoAfterEvent.cs + VampireRequestedEuiMessage.cs + VampireShadowTrapDoAfterEvent.cs + Vampire/ + SharedVampireSystem.cs + SharedVampireSystem.Gargantua.cs + SharedVampireSystem.Hemomancer.cs + SharedVampireSystem.Umbrae.cs + XenoGenetics/Components/ + GeneCombinerComponent.cs + GeneSplicerComponent.cs + GeneWithdrawnComponent.cs + XenoGeneComponent.cs + XenoGenetics/Events/ + GeneSplicerEvents.cs + XenoGeneticsDoAfterEvents.cs + XenoGenetics/Genes/ + AddArmorGeneSystem.cs + AddComponentsGeneSystem.cs + AddComponentsNoRandomGene.cs + AddDamageGeneSystem.cs + Components/ + AddComponentsGeneComponent.cs + AddComponentsNoRandomGeneComponent.cs + AddDamageResistanceGeneComponent.cs + AddSpriteGeneComponent.cs + DamageIncreasedByGene.cs + DamageIncreaserGeneComponent.cs + GrantActionsGeneComponent.cs + RemoveFactionGeneComponent.cs + RespiratorGeneComponent.cs + SpeedModifiedByGeneComponent.cs + SpeedModifierGeneComponent.cs + GrantActionsGeneSystem.cs + RemoveFactionGeneSystem.cs + SpeedModifierGeneSystem.cs + XenoGenetics/SharedXenoGeneticsSystem.cs + XxRaay/079/Events/ + NDA079AirlockAbilityEvents.cs + NDA079LightFlickerActionEvent.cs + NDA079SpawnCableActionEvent.cs + NDA079ToggleVisionModeEvent.cs + XxRaay/079/ + NDA079AirlockAbilityComponent.cs + NDA079AirlockAbilitySystem.cs + NDA079AirlockBoltTimerComponent.cs + NDA079CableAbilityComponent.cs + NDA079CableAbilitySystem.cs + NDA079CableComponent.cs + NDA079Component.cs + NDA079CpuHackingComponent.cs + NDA079GeneratorComponent.cs + NDA079LevelConfig.cs + NDA079LightFlickerAbilityComponent.cs + NDA079LightFlickerAbilitySystem.cs + NDA079LightRestoreTimerComponent.cs + NDA079Visuals.cs + XxRaay/Components/ + AlertEnergyComponent.cs + CameraRelayChatComponent.cs + EntityTileMovementComponent.cs + EntityTileMovementComponentState.cs + LLMNPCComponent.cs + NDA079CpuComponent.cs + OrbitalExplosionMode.cs + OrbitalStrikeComponent.cs + RechargeShoulderRocketLauncherComponent.cs + SandevistanComponent.cs + ShoulderRocketLauncherComponent.cs + SmasherInnateToolsComponent.cs + SpawnItemsOnDespawnComponent.cs + SupplyCaseComponent.cs + XxRaay/ + DataDefinitions/LLMMessageHistoryItem.cs + Events/SandevistanActionEvent.cs + FactionShop/FactionShopComponent.cs + FlagSystem/ + FlagCaptureComponent.cs + FlagCaptureDoAfterEvent.cs + SharedFlagCaptureSystem.cs + Systems/ + SharedEntityTileMovementSystem.cs + SupplyCaseSystem.cs + Types/EntityTileMovement.cs + Zero/KatanaDeflect/ + KatanaDeflectComponent.cs + KatanaDeflectSystem.cs + Zero/Overthrow/ + OverthrowComponent.cs + OverthrownItemComponent.cs + SharedOverthrowSystem.cs + Zombies/ZombieSystem.Restrictions.cs + _SystemsSupplementation/ + ItemToggle/ItemToggleSystem.cs + VendingMachine/ + VendingMachineSystem.cs + VendingMachineTargetEvent.cs + _TTS/TTSProviderComponentPublic.cs +Implants/ + ChameleonControllerImplantComponent.cs + ChameleonOutfitPrototype.cs + Components/ + ImplantedComponent.cs + ImplanterComponent.cs + RadioImplantComponent.cs + ReplacementImplantComponent.cs + StorageImplantComponent.cs + SubdermalImplantComponent.cs + ReplacementImplantSystem.cs + SharedChameleonControllerSystem.cs + SharedImplanterSystem.cs + SharedSubdermalImplantSystem.cs + SharedSubdermalImplantSystem.Relays.cs + StorageImplantSystem.cs +Info/RulesMessages.cs +Input/ContentKeyFunctions.cs +Instruments/ + SharedInstrumentComponent.cs + SharedInstrumentSystem.cs + UI/InstrumentBoundUserInterfaceMessages.cs +Intellicard/Components/IntellicardComponent.cs +Interaction/ + ActivateInWorldEvent.cs + AfterInteract.cs + BeforeInteract.cs + Components/ + BlockMovementComponent.cs + BypassInteractionChecksComponent.cs + ComplexInteractionComponent.cs + InteractionPopupComponent.cs + InteractionRelayComponent.cs + NoRotateOnInteractComponent.cs + UnremoveableComponent.cs + Events/ + AttackAttemptEvent.cs + ChangeDirectionAttemptEvent.cs + ContactInteractionEvent.cs + DroppedEvent.cs + GettingAttackedAttemptEvent.cs + GettingUsedAttemptEvent.cs + InteractionAttemptEvent.cs + InteractionFailureEvent.cs + InteractionSuccessEvent.cs + SuicideEvent.cs + UseAttemptEvent.cs + UseInHandEvent.cs + InteractHand.cs + InteractionPopupSystem.cs + InteractUsing.cs + ITargetedInteractEventArgs.cs + RangedInteract.cs + RotateToFaceSystem.cs + SharedInteractionSystem.Blocking.cs + SharedInteractionSystem.cs + SharedInteractionSystem.Relay.cs + SmartEquipSystem.cs +Internals/InternalsDoAfterEvent.cs +Inventory/Events/ + BeforeEquipEvents.cs + BeforeUnequipEvents.cs + EquipAttemptEvents.cs + EquippedEvents.cs + InventoryEquipActEvent.cs + OpenSlotStorageNetworkMessage.cs + RefreshEquipmentHudEvent.cs + UnequipAttemptEvent.cs + UnequippedEvents.cs + UseSlotNetworkMessage.cs +Inventory/ + InventoryComponent.cs + InventorySystem.cs + InventorySystem.Equip.cs + InventorySystem.Helpers.cs + InventorySystem.Relay.cs + InventorySystem.Slots.cs + InventoryTemplatePrototype.cs + RelaySubscriptionHelpers.cs + SelfEquipOnlyComponent.cs + SelfEquipOnlySystem.cs + SlotBlockComponent.cs + SlotBlockSystem.cs + SlotFlags.cs + VirtualItem/ + SharedVirtualItemSystem.cs + VirtualItemComponent.cs +IoC/SharedContentIoC.cs +Item/ + HeldSpeedModifierComponent.cs + HeldSpeedModifierSystem.cs + ItemComponent.cs + ItemSizeChangedEvent.cs + ItemSizePrototype.cs + ItemToggle/Components/ + ComponentTogglerComponent.cs + ItemToggleActiveSoundComponent.cs + ItemToggleComponent.cs + ItemToggleHotComponent.cs + ItemToggleMeleeWeaponComponent.cs + ItemTogglePrefixComponent.cs + ItemToggleSizeComponent.cs + ItemToggle/ + ComponentTogglerSystem.cs + ItemTogglePrefixSystem.cs + ItemToggleSystem.cs + MultiHandedItemComponent.cs + MultiHandedItemSystem.cs + PickupAttemptEvent.cs + SharedItemSystem.cs +ItemRecall/ + ItemRecallComponent.cs + ItemRecallEvents.cs + RecallMarkerComponent.cs + SharedItemRecallSystem.cs +Jaunt/ + JauntComponent.cs + JauntSystem.cs +Jittering/ + JitteringComponent.cs + SharedJitteringSystem.cs +Kitchen/ + BeingMicrowavedEvent.cs + Components/ + EntitySystems/HandheldGrinderSystem.cs + ExtractableComponent.cs + HandheldGrinderComponent.cs + KitchenSpikeComponent.cs + KitchenSpikeHookedComponent.cs + KitchenSpikeVictimComponent.cs + ReagentGrinderComponent.cs + RecipeProviderComponent.cs + SharedMicrowave.cs + SharpComponent.cs + EntitySystems/SharedReagentGrinderSystem.cs + GetSecretRecipesEvent.cs + MicrowaveMealRecipePrototype.cs + RecipeManager.cs + SharedKitchenSpikeSystem.cs + SharedReagentGrinder.cs + SharpDoAfterEvent.cs +Labels/Components/ + HandLabelerComponent.cs + LabelComponent.cs + PaperLabelComponent.cs + PaperLabelTypeComponent.cs +Labels/EntitySystems/ + LabelSystem.cs + SharedHandLabelerSystem.cs +Labels/LabelEvents.cs +LandMines/LandMineComponent.cs +Lathe/ + EmagLatheComponent.cs + LatheComponent.cs + LatheMessages.cs + LatheVisuals.cs + PrototypeIdLinkedListSerializer.cs + Prototypes/ + LatheCategoryPrototype.cs + LatheRecipePackPrototype.cs + SharedLatheSystem.cs +Light/Components/ + HandheldLightComponent.cs + ImplicitRoofComponent.cs + IsRoofComponent.cs + ItemTogglePointLightComponent.cs + LightBulbComponent.cs + LightCycleComponent.cs + LightReplacerComponent.cs + PoweredLightComponent.cs + RgbLightControllerComponent.cs + RoofComponent.cs + RotatingLightComponent.cs + SharedEmergencyLightComponent.cs + SharedExpendableLightComponent.cs + SharedLightBehaviourComponent.cs + SlimPoweredLightComponent.cs + SunShadowCastComponent.cs + SunShadowComponent.cs + SunShadowCycleComponent.cs + TileEmissionComponent.cs + UnpoweredFlashlightComponent.cs +Light/EntitySystems/ + ItemTogglePointLightSystem.cs + SharedLightBulbSystem.cs + SharedLightCycleSystem.cs + SharedLightReplacerSystem.cs + SharedPoweredLightSystem.cs + SharedRoofSystem.cs + SharedSunShadowSystem.cs + SlimPoweredLightSystem.cs + UnpoweredFlashlightSystem.cs +Light/ + LightToggleEvent.cs + PoweredLightDoAfterEvent.cs + SharedEmergencyLightSystem.cs + SharedHandheldLightSystem.cs + SharedPoweredLightVisuals.cs + SharedRgbLightControllerSystem.cs + SharedRotatingLightSystem.cs + SharedUnpoweredFlashlightVisuals.cs +Lightning/ + Components/SharedLightningComponent.cs + SharedLightningSystem.cs +Localizations/ + ContentLocalizationManager.cs + Units.cs + UserInputParser.cs +Lock/BypassLock/Components/ + BypassLockComponent.cs + BypassLockRequiresMobStateComponent.cs +Lock/BypassLock/Systems/ + BypassLockSystem.cs + BypassLockSystem.MobState.cs +Lock/ + ItemToggleRequiresLockComponent.cs + LockComponent.cs + LockedAnchorableComponent.cs + LockedStorageComponent.cs + LockedWiresPanelComponent.cs + LockingWhitelistComponent.cs + LockingWhitelistSystem.cs + LockSystem.cs + UIRequiresLockComponent.cs +Lube/ + LubeComponent.cs + LubedComponent.cs +MachineLinking/ + SharedSignalTimerComponent.cs + UIKeys.cs +Machines/Components/ + MultipartMachineComponent.cs + MultipartMachinePartComponent.cs +Machines/ + EntitySystems/SharedMultipartMachineSystem.cs + Events/MultipartMachineEvents.cs +Magic/Components/ + AnimateableComponent.cs + AnimateComponent.cs + MagicComponent.cs + SpellbookComponent.cs + WizardClothesComponent.cs +Magic/Events/ + BeforeCastSpellEvent.cs + ChangeComponentSpellEvent.cs + ChargeSpellEvent.cs + InstantSpawnSpellEvent.cs + KnockSpellEvent.cs + MindSwapSpellEvent.cs + ProjectileSpellEvent.cs + RandomGlobalSpawnSpellEvent.cs + SmiteSpellEvent.cs + TeleportSpellEvent.cs + VoidApplauseSpellEvent.cs + WorldSpawnSpellEvent.cs +Magic/ + MagicInstantSpawnData.cs + SharedMagicSystem.cs + SpellbookDoAfterEvent.cs + SpellbookSystem.cs + Systems/AnimateSpellSystem.cs +MagicMirror/ + MagicMirrorComponent.cs + MagicMirrorSystem.cs +Mapping/ + MappingMapDataMessage.cs + MappingSaveMapErrorMessage.cs + MappingSaveMapMessage.cs + StartPlacementActionEvent.cs +Maps/ + ContentTileDefinition.cs + GameMapCondition.cs + GameMapPrototype.cs + GameMapPrototype.MapSelection.cs + SharedGridDraggingSystem.cs + TileHistoryComponent.cs + TileSystem.cs + TurfSystem.cs +MapText/ + SharedMapTextComponent.cs + SharedMapTextSystem.cs +MassMedia/Components/ + NewsWriterBuiMessages.cs + StationNewsComponent.cs +MassMedia/Systems/SharedNewsSystem.cs +Materials/ + ActiveMaterialReclaimerComponent.cs + CollideMaterialReclaimerComponent.cs + InsertingMaterialStorageComponent.cs + MaterialComponent.cs + MaterialPrototype.cs + MaterialReclaimerComponent.cs + MaterialStorageComponent.cs + OreSilo/ + OreSiloClientComponent.cs + OreSiloComponent.cs + SharedOreSiloSystem.cs + PhysicalCompositionComponent.cs + SharedMaterialReclaimerSystem.cs + SharedMaterialStorageSystem.cs +Mech/Components/ + MechComponent.cs + MechPilotComponent.cs +Mech/EntitySystems/ + SharedMechSystem.cs + SharedMechSystem.Relay.cs +Mech/Equipment/Components/ + MechEquipmentComponent.cs + MechSoundboardComponent.cs +Mech/ + Equipment/Systems/MechSoundboardSystem.cs + MechUI.cs + SharedMech.cs +Medical/ + CrewMonitoring/CrewMonitoringShared.cs + Cryogenics/ + ActiveCryoPodComponent.cs + CryoPodComponent.cs + CryoPodWireStatus.cs + InsideCryoPodComponent.cs + SharedCryoPodSystem.cs + SharedInsideCryoPodSystem.cs + DefibrillatorComponent.cs + DefibrillatorEvents.cs + Healing/ + HealingComponent.cs + HealingSystem.cs + HealingDoAfterEvent.cs + ReclaimerDoAfterEvent.cs + SharedDefibrillatorSystem.cs + Stethoscope/ + Components/StethoscopeComponent.cs + StethoscopeSystem.cs + StethoscopeDoAfterEvent.cs + SuitSensors/ + SharedSuitSensor.cs + SharedSuitSensorSystem.cs + SuitSensorComponent.cs + VomitSystem.cs +MedicalScanner/ + HealthAnalyzerDoAfterEvent.cs + HealthAnalyzerScannedUserMessage.cs + HealthAnalyzerUiKey.cs + SharedMedicalScannerComponent.cs +Metabolism/ + MetabolismStagePrototype.cs + MetabolizerComponent.cs + MetabolizerSystem.cs + MetabolizerTypePrototype.cs +Mind/Components/ + IsDeadICComponent.cs + MindContainerComponent.cs + MindExaminableComponent.cs + TransferMindOnGibComponent.cs + VisitingMindComponent.cs +Mind/Filters/ + AliveAiPool.cs + AliveHumansPool.cs + AntagonistMindFilter.cs + BodyMindFilter.cs + HasRoleMindFilter.cs + IMindPool.cs + JobMindFilter.cs + MindFilter.cs + MindPool.cs + ObjectiveMindFilter.cs +Mind/ + IsDeadICSystem.cs + MindComponent.cs + MindExamineSystem.cs + MindVisuals.cs + RoleTypePrototype.cs + SharedMindSystem.cs + SharedMindSystem.Relay.cs +Mindshield/Components/ + FakeMindshieldComponent.cs + FakeMindShieldImplantComponent.cs + MindShieldComponent.cs + MindShieldImplantComponent.cs +Mindshield/FakeMindShield/FakeMindshieldSystem.cs +Mining/Components/ + MiningScannerComponent.cs + MiningScannerViewableComponent.cs + MiningScannerViewerComponent.cs + OreVeinComponent.cs +Mining/ + MiningScannerSystem.cs + OrePrototype.cs +Mobs/Components/ + MobStateActionsComponent.cs + MobStateComponent.cs + MobThresholdsComponent.cs +Mobs/ + CritMobActions.cs + Events/BeforeAlertSeverityCheckEvent.cs + MobState.cs + Systems/ + MobStateActionsSystem.cs + MobStateSystem.cs + MobStateSystem.StateMachine.cs + MobStateSystem.Subscribers.cs + MobThresholdSystem.cs +Module/SharedModuleTestingCallbacks.cs +Morgue/Components/ + ActiveCrematoriumComponent.cs + CrematoriumComponent.cs + EntityStorageLayingDownOverrideComponent.cs + MorgueComponent.cs +Morgue/ + EntityStorageLayingDownOverrideSystem.cs + SharedCrematoriumSystem.cs + SharedMorgue.cs + SharedMorgueSystem.cs +MouseRotator/ + MouseRotatorComponent.cs + SharedMouseRotatorSystem.cs +Mousetrap/ + MousetrapComponent.cs + MousetrapSystem.cs +Movement/Components/ + ActiveInputMoverComponent.cs + ActiveJetpackComponent.cs + ActiveLeaperComponent.cs + AutoOrientComponent.cs + BoundaryComponent.cs + CanMoveInAirComponent.cs + ContentEyeComponent.cs + CursorOffsetRequiresWieldComponent.cs + EyeCursorOffsetComponent.cs + FloorOccluderComponent.cs + FloorOcclusionComponent.cs + FootstepModifierComponent.cs + FrictionContactsComponent.cs + FrictionStatusEffectComponent.cs + IgnoreKudzuComponent.cs + InputMoverComponent.cs + JetpackComponent.cs + JetpackUserComponent.cs + JumpAbilityComponent.cs + MobCollisionComponent.cs + MobMoverComponent.cs + MovementAlwaysTouchingComponent.cs + MovementIgnoreGravityComponent.cs + MovementModifiedByContactComponent.cs + MovementModStatusEffectComponent.cs + MovementRelayTargetComponent.cs + MovementSoundComponent.cs + MovementSpeedModifierComponent.cs + NoRotateOnMoveComponent.cs + RelayInputMoverComponent.cs + SpeedModifiedOnWieldComponent.cs + SpeedModifierContactsComponent.cs + SpriteMovementComponent.cs + WormComponent.cs +Movement/Events/ + ContainerRelayMovementEntityEvent.cs + GetFootstepSoundEvent.cs + GetSpeedModifierContactCapEvent.cs + MoveInputEvent.cs + SpriteMoveEvent.cs + TileFrictionEvent.cs + ToggleJetpackEvent.cs + UpdateCanMoveEvent.cs + WeightlessMoveEvent.cs +Movement/Pulling/Components/ + ActivePullerComponent.cs + PullableComponent.cs + PullerComponent.cs +Movement/Pulling/Events/ + AttemptPullEvent.cs + AttemptStopPullingEvent.cs + BeingPulledAttemptEvent.cs + PullMessage.cs + PullStartedMessage.cs + PullStoppedMessage.cs + StartPullAttemptEvent.cs +Movement/ + Pulling/Systems/PullingSystem.cs + Systems/ + AutoOrientSystem.cs + FrictionContactsSystem.cs + MovementIgnoreGravitySystem.cs + MovementModStatusSystem.cs + MovementSoundSystem.cs + MovementSpeedModifierSystem.cs + SharedContentEyeSystem.cs + SharedFloorOcclusionSystem.cs + SharedJetpackSystem.cs + SharedJumpAbilitySystem.cs + SharedMobCollisionSystem.cs + SharedMoverController.cs + SharedMoverController.Input.cs + SharedMoverController.Relay.cs + SharedSpriteMovementSystem.cs + SpeedModifierContactsSystem.cs + WormSystem.cs +NameIdentifier/ + NameIdentifierComponent.cs + NameIdentifierGroupPrototype.cs + SharedNameIdentifierSystem.cs +NameModifier/Components/ + ModifyWearerNameComponent.cs + NameModifierComponent.cs +NameModifier/EntitySystems/ + ModifyWearerNameSystem.cs + NameModifierSystem.cs +Ninja/Components/ + BatteryDrainerComponent.cs + DashAbilityComponent.cs + EmagProviderComponent.cs + EnergyKatanaComponent.cs + ItemCreatorComponent.cs + NinjaGlovesComponent.cs + NinjaSuitComponent.cs + SpaceNinjaComponent.cs + SpiderChargeComponent.cs + StunProviderComponent.cs +Ninja/Systems/ + DashAbilitySystem.cs + EmagProviderSystem.cs + EnergyKatanaSystem.cs + ItemCreatorSystem.cs + SharedBatteryDrainerSystem.cs + SharedNinjaGlovesSystem.cs + SharedNinjaSuitSystem.cs + SharedSpaceNinjaSystem.cs + SharedSpiderChargeSystem.cs + SharedStunProviderSystem.cs +NodeContainer/ + Node.cs + NodeContainerComponent.cs + NodeGroups/ + INodeGroup.cs + IPipeNet.cs + NodeGroupID.cs + NodeVis.cs + SharedNodeContainerSystem.cs +NPC/ + ActiveNPCComponent.cs + Components/ + FactionExceptionComponent.cs + FactionExceptionTrackerComponent.cs + NpcFactionMemberComponent.cs + NPCRecentlyInjectedComponent.cs + Events/ + HTNMessage.cs + NPCSteeringDebugEvent.cs + PathBreadcrumbsMessage.cs + PathPolysRefreshMessage.cs + PathRouteMessage.cs + RequestNPCSteeringDebugEvent.cs + RequestPathfindingDebugMessage.cs + PathfindingBoundary.cs + PathfindingBreadcrumb.cs + PathfindingDebugMode.cs + PathPoly.cs + Prototypes/NpcFactionPrototype.cs + RequestHTNMessage.cs + SharedNPCComponent.cs + SharedNPCSteeringSystem.cs + SharedPathfindingSystem.cs + SharedPathfindingSystem.Line.cs + Systems/ + NpcFactionSystem.cs + NpcFactionSystem.Exception.cs + SharedNPCImprintingOnSpawnBehaviourSystem.cs + SharedNPCSystem.cs +Nuke/ + NukeDiskComponent.cs + NukeUiMessages.cs + SharedNuke.cs +NukeOps/ + NukeOperativeComponent.cs + WarDeclaratorEvents.cs +Nutrition/AnimalHusbandry/ + InfantComponent.cs + ReproductiveComponent.cs + ReproductivePartnerComponent.cs +Nutrition/Components/ + ButcherableComponent.cs + CreamPieComponent.cs + CreamPiedComponent.cs + EdibleComponent.cs + ExaminableHungerComponent.cs + FlavorProfileComponent.cs + FoodMetamorphableByAddingComponent.cs + FoodSequenceElementComponent.cs + FoodSequenceStartPointComponent.cs + HungerComponent.cs + IngestionBlockerComponent.cs + MessyDrinkerComponent.cs + OpenableComponent.cs + PressurizedSolutionComponent.cs + SealableComponent.cs + ShakeableComponent.cs + SharedFatExtractor.cs + SharedFoodComponent.cs + SmokableComponent.cs + ThirstComponent.cs + UtensilComponent.cs +Nutrition/EntitySystems/ + ExaminableHungerSystem.cs + FlavorProfileSystem.cs + FoodSequenceSystem.cs + HungerSystem.cs + IngestionSystem.API.cs + IngestionSystem.Blockers.cs + IngestionSystem.cs + IngestionSystem.Utensils.cs + MessyDrinkerSystem.cs + OpenableSystem.cs + PressurizedSolutionSystem.cs + SealableSystem.cs + ShakeableSystem.cs + SharedCreamPieSystem.cs + SharedFoodSequenceSystem.cs + ThirstSystem.cs + UtensilSystem.cs +Nutrition/ + Events.cs + Flavor.cs + FoodMetamorphRules/FoodMetamorphRule.cs + IngestionEvents.cs + Prototypes/ + EdiblePrototype.cs + FoodSequenceElementPrototype.cs + MetamorphRecipePrototype.cs +obj/Debug/ + .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + Content.Shared.AssemblyInfo.cs +Objectives/Components/ + ObjectiveComponent.cs + StealAreaComponent.cs + StealTargetComponent.cs +Objectives/ + ObjectiveInfo.cs + Prototypes/StealTargetGroupPrototype.cs + Systems/ + SharedObjectivesSystem.cs + TargetSystem.cs +Overlays/ + BlackAndWhiteOverlayComponent.cs + NoirOverlayComponent.cs + ShowCrewIconsComponent.cs + ShowCriminalRecordIconsComponent.cs + ShowHealthBarsComponent.cs + ShowHealthIconsComponent.cs + ShowHungerIconsComponent.cs + ShowJobIconsComponent.cs + ShowMindShieldIconsComponent.cs + ShowSyndicateIconsComponent.cs + ShowThirstIconsComponent.cs + ThermalSightComponent.cs +PAI/PAIComponent.cs +Paper/ + ActivateOnPaperOpenedComponent.cs + BlockWritingComponent.cs + BlockWritingSystem.cs + EnvelopeComponent.cs + EnvelopeSystem.cs + PaperComponent.cs + PaperSystem.cs + RandomPaperContentComponent.cs + StampComponent.cs +Parallax/Biomes/ + BiomeComponent.cs + BiomeTemplatePrototype.cs + Layers/ + BiomeDecalLayer.cs + BiomeDummyLayer.cs + BiomeEntityLayer.cs + BiomeMetaLayer.cs + BiomeTileLayer.cs + IBiomeLayer.cs + IBiomeWorldLayer.cs + Markers/ + BiomeMarkerLayerPrototype.cs + IBiomeMarkerLayer.cs + SharedBiomeSystem.cs +Parallax/ + ParallaxComponent.cs + SharedParallaxSystem.cs +ParcelWrap/Components/ + ParcelWrapComponent.cs + ParcelWrapOverrideComponent.cs + WrappedParcelComponent.cs + WrappedParcelVisuals.cs +ParcelWrap/Systems/ + ParcelWrapEvents.cs + ParcelWrappingSystem.cs + ParcelWrappingSystem.ParcelWrap.cs + ParcelWrappingSystem.WrappedParcel.cs +ParticleAccelerator/ + AcceleratorParts.cs + Components/ + ParticleAcceleratorEmitterComponent.cs + ParticleAcceleratorEndCapComponent.cs + ParticleAcceleratorFuelChamberComponent.cs + ParticleAcceleratorPowerBoxComponent.cs +Payload/Components/ + ChemicalPayloadComponent.cs + PayloadCaseComponent.cs + PayloadTriggerComponent.cs +Payload/EntitySystems/ChemicalPayloadSystem.cs +PDA/ + PdaComponent.cs + PdaMessagesUi.cs + PdaUpdateState.cs + PdaVisuals.cs + Ringer/ + RingerComponent.cs + RingerMessagesUI.cs + RingerUplinkComponent.cs + RingerVisuals.cs + SharedPdaSystem.cs + SharedRingerSystem.cs +Photography/ + PhotographComponent.cs + PhotographySystem.cs + PictureTakerComponent.cs +Physics/ + CollisionGroup.cs + Controllers/SharedConveyorController.cs + JointVisualsComponent.cs + PreventCollideComponent.cs + SharedPreventCollideSystem.cs +Pinpointer/ + ConfigurableNavMapBeaconComponent.cs + NavMapBeaconComponent.cs + NavMapComponent.cs + NavMapDoorComponent.cs + NukeopsStationMapComponent.cs + PinpointerComponent.cs + PinpointerVisuals.cs + SharedNavMapSystem.cs + SharedPinpointerSystem.cs + SharedStationMapSystem.cs + StationMapComponent.cs +Placeable/ + ItemPlacerComponent.cs + ItemPlacerSystem.cs + PlaceableSurfaceComponent.cs + PlaceableSurfaceSystem.cs +Players/ + ContentPlayerData.cs + JobWhitelist/MsgJobWhitelist.cs + MsgRoleBans.cs + PlayerDataExt.cs + PlayTimeTracking/ + ISharedPlaytimeManager.cs + MsgPlayTime.cs + PlayTimeTrackerPrototype.cs + PlayTimeTrackingShared.cs + RateLimiting/ + RateLimitRegistration.cs + SharedPlayerRateLimitManager.cs + SharedPlayerSystem.cs +Plunger/Components/ + PlungerComponent.cs + PlungerUseComponent.cs +Plunger/ + PlungerDoAfterEvent.cs + Systems/PlungerSystem.cs +PneumaticCannon/ + PneumaticCannonComponent.cs + SharedPneumaticCannonSystem.cs +Pointing/Components/ + SharedPointingArrowComponent.cs + SharedRoguePointingArrowComponent.cs +Pointing/ + PointingEvents.cs + SharedPointingSystem.cs +Points/ + PointManagerComponent.cs + SharedPointSystem.cs +Polymorph/Components/ + ChameleonDisguiseComponent.cs + ChameleonDisguisedComponent.cs + ChameleonProjectorComponent.cs +Polymorph/ + PolymorphActions.cs + PolymorphEvents.cs + PolymorphPrototype.cs + Systems/SharedChameleonProjectorSystem.cs +Popups/SharedPopupSystem.cs +Power/ + ApcToolFinishedEvent.cs + ChargeEvents.cs + Components/ + ActivatableUIRequiresPowerComponent.cs + ApcPowerReceiverBatteryComponent.cs + ApcPowerReceiverComponentState.cs + BatteryComponent.cs + BatterySelfRechargerComponent.cs + BatteryVisualsComponent.cs + ChargerComponent.cs + ExaminableBatteryComponent.cs + InsideChargerComponent.cs + ItemSlotRequiresPowerComponent.cs + PowerStateComponent.cs + SharedApcPowerReceiverComponent.cs + UIPowerStateComponent.cs + EntitySystems/ + ChargerSystem.cs + ItemSlotRequiresPowerSystem.cs + PowerStateSystem.cs + SharedActivatableUIRequiresPowerSystem.cs + SharedBatterySystem.API.cs + SharedBatterySystem.cs + SharedPowerNetSystem.cs + SharedPowerReceiverSystem.cs + UIPowerStateSystem.cs + Generation/Teg/SharedTeg.cs + Generator/ + ActiveGeneratorRevvingComponent.cs + ActiveGeneratorRevvingSystem.cs + FuelGeneratorComponent.cs + GeneratorExhaustGasComponent.cs + PowerSwitchableComponent.cs + SharedGeneratorSystem.cs + SharedPortableGeneratorComponent.cs + SharedPortableGeneratorSystem.cs + SharedPowerSwitchableSystem.cs + PowerChangedEvent.cs + PowerMonitoringCableNetworksComponent.cs + SharedBattery.cs + SharedPower.cs + SharedPowerCharge.cs + SharedPowerChargeComponent.cs + SharedPowerDevice.cs + SharedPowerMonitoringConsoleComponent.cs + SharedPowerMonitoringConsoleSystem.cs + TeslaCoilVisuals.cs +PowerCell/Components/ + PowerCellComponent.cs + PowerCellDrawComponent.cs + PowerCellSlotComponent.cs + ToggleCellDrawComponent.cs +PowerCell/ + PowerCellEvents.cs + PowerCellSystem.API.cs + PowerCellSystem.cs + PowerCellSystem.Draw.cs + PowerCellSystem.Relay.cs + ToggleCellDrawSystem.cs +Prayer/PrayableComponent.cs +Preferences/ + GameSettings.cs + HumanoidCharacterProfile.cs + JobPriority.cs + Loadouts/Effects/ + GroupLoadoutEffect.cs + JobRequirementLoadoutEffect.cs + LoadoutEffect.cs + LoadoutEffectGroupPrototype.cs + PointsCostLoadoutEffect.cs + SpeciesLoadoutEffect.cs + Loadouts/ + Loadout.cs + LoadoutGroupPrototype.cs + LoadoutPrototype.cs + RoleLoadout.cs + RoleLoadoutPrototype.cs + MsgDeleteCharacter.cs + MsgPreferencesAndSettings.cs + MsgSelectCharacter.cs + MsgUpdateCharacter.cs + MsgUpdateConstructionFavorites.cs + PlayerPreferences.cs + PreferenceUnavailableMode.cs + SpawnPriorityPreference.cs +Procedural/ + Components/EntityRemapComponent.cs + Distance/ + DunGenEuclideanSquaredDistance.cs + DunGenSquareBump.cs + IDunGenDistance.cs + Dungeon.cs + DungeonConfig.cs + DungeonGenerators/ + ExteriorDunGen.cs + NoiseDistanceDunGen.cs + NoiseDunGen.cs + PrefabDunGen.cs + PrototypeDunGen.cs + ReplaceTileDunGen.cs + DungeonLayers/ + EntityTableDunGen.cs + FillGridDunGen.cs + MobsDunGen.cs + OreDunGen.cs + OreDunGenPrototype.cs + DungeonPath.cs + DungeonPresetPrototype.cs + DungeonRoom.cs + DungeonRoomPackPrototype.cs + DungeonRoomPrototype.cs + IDunGenLayer.cs + Loot/ + BiomeMarkerLoot.cs + BiomeTemplateLoot.cs + IDungeonLoot.cs + RandomSpawnsLoot.cs + SalvageLootPrototype.cs + PostGeneration/ + AutoCablingDunGen.cs + BiomeDunGen.cs + BiomeMarkerLayerDunGen.cs + BoundaryWallDunGen.cs + CornerClutterDunGen.cs + CorridorClutterDunGen.cs + CorridorDecalSkirtingDunGen.cs + CorridorDunGen.cs + DungeonEntranceDunGen.cs + EntranceFlankDunGen.cs + ExternalWindowDunGen.cs + InternalWindowDunGen.cs + JunctionDunGen.cs + MiddleConnectionDunGen.cs + RoomEntranceDunGen.cs + SplineDungeonConnectorDunGen.cs + WallMountDunGen.cs + WormCorridorDunGen.cs + SalvageDifficultyPrototype.cs + SharedDungeonSystem.cs +Projectiles/ + EmbeddableProjectileComponent.cs + EmbeddedContainerComponent.cs + EmbedEvents.cs + ProjectileComponent.cs + ProjectileEmbedEvent.cs + ProjectileSpreadComponent.cs + SharedProjectileSystem.cs +Prototypes/ + EntityPrototypeHelpers.cs + NavMapBlipPrototype.cs +ProximityDetection/ + Components/ProximityDetectorComponent.cs + ProximityDetectionEvents.cs + Systems/ProximityDetectionSystem.cs +Prying/Components/ + PryingComponent.cs + PryUnpoweredComponent.cs +Prying/Systems/PryingSystem.cs +Puppet/ + SharedVentriloquistPuppetSystem.cs + VentriloquistPuppetComponent.cs +Radiation/Components/ + GeigerComponent.cs + RadiationPulseComponent.cs + RadiationSourceComponent.cs +Radiation/Events/ + OnIrradiatedEvent.cs + OnRadiationOverlayUpdateEvent.cs +Radiation/ + RadiationRay.cs + Systems/ + RadiationPulseSystem.cs + SharedGeigerSystem.cs + SharedRadiationSystem.cs +Radio/Components/ + ActiveRadioComponent.cs + ActiveRadioJammerComponent.cs + EncryptionKeyComponent.cs + EncryptionKeyHolderComponent.cs + HeadsetComponent.cs + IntercomComponent.cs + IntrinsicRadioReceiverComponent.cs + IntrinsicRadioTransmitterComponent.cs + RadioJammerComponent.cs + RadioMicrophoneComponent.cs + RadioSpeakerComponent.cs + TelecomExemptComponent.cs + TelecomServerComponent.cs + WearingHeadsetComponent.cs +Radio/ + EncryptionChannelsChangedEvent.cs + EntitySystems/ + EncryptionKeySystem.cs + SharedHeadsetSystem.cs + SharedJammerSystem.cs + SharedRadioDeviceSystem.cs + GetDefaultRadioChannelEvent.cs + RadioChannelPrototype.cs + RadioVisuals.cs + SharedIntercom.cs +Random/ + Helpers/SharedRandomExtensions.cs + IBudgetEntry.cs + IWeightedRandom.cs + RandomFillSolution.cs + RandomHelperSystem.cs + RandomPlantMutation.cs + RandomPlantMutationListPrototype.cs + RandomSystem.cs + Rules/ + AlwaysTrue.cs + GridInRange.cs + InSpace.cs + NearbyAccess.cs + NearbyComponents.cs + NearbyEntities.cs + NearbyTilesPercent.cs + OnMapGrid.cs + RulesSystem.cs + WeightedRandomEntityPrototype.cs + WeightedRandomFillSolutionPrototype.cs + WeightedRandomOrePrototype.cs + WeightedRandomPrototype.cs + WeightedRandomSpeciesPrototype.cs +RatKing/Components/ + RummageableComponent.cs + RummagerComponent.cs +RatKing/ + RatKingActions.cs + RatKingComponent.cs + RatKingServantComponent.cs + SharedRatKingSystem.cs + Systems/RummagerSystem.cs +RCD/Components/ + RCDAmmoComponent.cs + RCDComponent.cs + RCDDeconstructableComponent.cs +RCD/ + RCDEvents.cs + RCDPrototype.cs + Systems/ + RCDAmmoSystem.cs + RCDSystem.cs +ReagentSpeed/ + ReagentSpeedComponent.cs + ReagentSpeedSystem.cs +Rejuvenate/RejuvenateEvent.cs +Remotes/ + Components/DoorRemoteComponent.cs + EntitySystems/SharedDoorRemoteSystem.cs +Repairable/ + RepairableComponent.cs + RepairableSystem.cs +RepulseAttract/ + Events/RepulseAttractActionEvent.cs + RepulseAttractComponent.cs + RepulseAttractSystem.cs +Research/Components/ + BlueprintComponent.cs + BlueprintReceiverComponent.cs + ResearchClientComponent.cs + ResearchServerComponent.cs + ResearchStealerComponent.cs + SharedResearchConsoleComponent.cs + TechnologyDatabaseComponent.cs +Research/Prototypes/ + LatheRecipePrototype.cs + TechDisciplinePrototype.cs + TechnologyPrototype.cs +Research/ + SharedDiskConsole.cs + Systems/ + BlueprintSystem.cs + SharedResearchStealerSystem.cs + SharedResearchSystem.cs + TechnologyDisk/ + Components/TechnologyDiskComponent.cs + Systems/TechnologyDiskSystem.cs +Resist/ + EscapeInventoryEvent.cs + ResistLockerDoAfterEvent.cs +Respawn/ + SharedSpecialRespawnSystem.cs + SpecialRespawnComponent.cs +RetractableItemAction/ + ActionRetractableItemComponent.cs + ItemActionEvents.cs + RetractableItemActionComponent.cs + RetractableItemActionSystem.cs +Revenant/Components/ + CorporealComponent.cs + RevenantComponent.cs + RevenantOverloadedLightsComponent.cs +Revenant/EntitySystems/ + SharedCorporealSystem.cs + SharedRevenantOverloadedLightsSystem.cs +Revenant/SharedRevenant.cs +Revolutionary/Components/ + AlwaysRevolutionaryConvertibleComponent.cs + HeadRevolutionaryComponent.cs + RevolutionaryComponent.cs +Revolutionary/SharedRevolutionarySystem.cs +Robotics/ + Components/RoboticsConsoleComponent.cs + RoboticsConsoleUi.cs + Systems/SharedRoboticsConsoleSystem.cs +Roles/ + AntagPrototype.cs + Components/ + ChangelingRoleComponent.cs + DragonRoleComponent.cs + GhostRoleMarkerRoleComponent.cs + GhoulRoleComponent.cs + InitialInfectedRoleComponent.cs + JobRoleComponent.cs + MindRoleComponent.cs + NinjaRoleComponent.cs + NukeopsRoleComponent.cs + ObserverRoleComponent.cs + ParadoxCloneRoleComponent.cs + RevolutionaryRoleComponent.cs + RoleBriefingComponent.cs + SiliconBrainRoleComponent.cs + StartingMindRoleComponent.cs + SubvertedSiliconRoleComponent.cs + SurvivorRoleComponent.cs + ThiefRoleComponent.cs + TraitorRoleComponent.cs + VampireRoleComponent.cs + WizardRoleComponent.cs + XenoborgRoleComponent.cs + ZombieRoleComponent.cs + DepartmentPrototype.cs + JobPrototype.cs + JobRequirement/ + AgeRequirement.cs + DepartmentTimeRequirement.cs + OverallPlaytimeRequirement.cs + RoleTimeRequirement.cs + SpeciesRequirement.cs + TraitsRequirement.cs + JobRequirementOverridePrototype.cs + JobRequirements.cs + Jobs/SharedJobSystem.cs + JobSpecial.cs + MindGetAllRoleInfoEvent.cs + MindIsAntagonistEvent.cs + RoleAddedEvent.cs + RoleCodeword/ + RoleCodewordComponent.cs + SharedRoleCodewordSystem.cs + RoleEvent.cs + RoleRemovedEvent.cs + SharedRoleSystem.cs + StartingGearEquippedEvent.cs + StartingGearPrototype.cs +Rootable/ + RootableComponent.cs + RootableSystem.cs + SharedRootableSystem.cs +Rotatable/ + FlippableComponent.cs + RotatableComponent.cs + RotatableSystem.cs +Rotation/ + RotationVisualsComponent.cs + SharedRotationVisualsSystem.cs +Rounding/ContentHelpers.cs +Salvage/Expeditions/ + ExpeditionStage.cs + Modifiers/ + IBiomeSpecificMod.cs + ISalvageMod.cs + SalvageAirMod.cs + SalvageBiomeModPrototype.cs + SalvageDungeonModPrototype.cs + SalvageLightMod.cs + SalvageMod.cs + SalvageTemperatureMod.cs + SalvageWeatherMod.cs + SalvageExpeditions.cs + SalvageFactionPrototype.cs + SalvageMobEntry.cs + SharedSalvageExpeditionComponent.cs +Salvage/Fulton/ + FultonBeaconComponent.cs + FultonComponent.cs + FultonedComponent.cs + SharedFultonSystem.cs +Salvage/ + JobBoard/SalvageJobBoardConsoleComponent.cs + Magnet/ + AsteroidOffering.cs + DebrisOffering.cs + ISalvageMagnetOffering.cs + MagnetClaimOfferEvent.cs + SalvageMagnetBoundUserInterfaceState.cs + SalvageMagnetUiKey.cs + SalvageOffering.cs + RestrictedRangeComponent.cs + SalvageMapPrototype.cs + SharedRestrictedRangeSystem.cs + SharedSalvageSystem.cs + SharedSalvageSystem.Magnet.cs +Sandbox/SharedSandboxSystem.cs +SecretLocks/ + SharedVoiceTriggerLockSystem.cs + VoiceTriggerLockComponent.cs +Security/Components/ + CriminalRecordComponent.cs + DeployableBarrierComponent.cs + GenpopIdCardComponent.cs + GenpopLockerComponent.cs +Security/ + SecurityStatus.cs + Systems/ + DeployableBarrierSystem.cs + SharedGenpopSystem.cs +SelectableComponentAdder/ + SelectableComponentAdderComponent.cs + SelectableComponentAdderSystem.cs +SensorMonitoring/SharedSensorMonitoringConsoleComponent.cs +Sericulture/ + SericultureComponent.cs + SericultureSystem.cs +SharedArrayExtension.cs +Shuttles/BUIStates/ + DockingInterfaceState.cs + DockingPortState.cs + EmergencyConsoleBoundUserInterfaceState.cs + IFFConsoleBoundUserInterfaceState.cs + NavBoundUserInterfaceState.cs + NavInterfaceState.cs + ShuttleBoundUserInterfaceState.cs + ShuttleMapInterfaceState.cs +Shuttles/Components/ + EmergencyShuttleConsoleComponent.cs + FTLComponent.cs + FTLDestinationComponent.cs + FTLMapComponent.cs + FTLSmashImmuneComponent.cs + FtlVisualizerComponent.cs + IFFComponent.cs + NoFTLComponent.cs + PilotComponent.cs + PreventFTLComponent.cs + PreventPilotComponent.cs + RadarConsoleComponent.cs + SharedDockingComponent.cs + SharedShuttleConsoleComponent.cs + SharedThrusterComponent.cs + ShuttleDestinationCoordinatesComponent.cs + ShuttleMapParallaxComponent.cs + SpaceGarbageComponent.cs +Shuttles/Events/ + DockRequestMessage.cs + EmergencyShuttleAuthorizeMessage.cs + EmergencyShuttlePositionMessage.cs + EmergencyShuttleRepealAllMessage.cs + EmergencyShuttleRepealMessage.cs + EmergencyShuttleRequestPositionMessage.cs + IFFShowIFFMessage.cs + ShuttleConsoleFTLBeaconMessage.cs + ShuttleConsoleFTLPositionMessage.cs + UndockRequestMessage.cs +Shuttles/Systems/ + SharedDockingSystem.cs + SharedEmergencyShuttleSystem.cs + SharedRadarConsoleSystem.cs + SharedShuttleConsoleSystem.cs + SharedShuttleSystem.cs + SharedShuttleSystem.EmergencyConsole.cs + SharedShuttleSystem.IFF.cs +Shuttles/UI/MapObjects/ + GridMapObject.cs + IMapObject.cs + ShuttleBeaconObject.cs + ShuttleExclusionObject.cs +Silicons/Borgs/ + BorgTypePrototype.cs + BorgUI.cs + Components/ + BorgBrainComponent.cs + BorgChassisComponent.cs + BorgModuleComponent.cs + BorgModuleIconComponent.cs + BorgSwitchableTypeComponent.cs + BorgTransponderComponent.cs + ComponentBorgModuleComponent.cs + ItemBorgModuleComponent.cs + MMIComponent.cs + SelectableBorgModuleComponent.cs + SharedBorgSwitchableTypeSystem.cs + SharedBorgSystem.API.cs + SharedBorgSystem.cs + SharedBorgSystem.MMI.cs + SharedBorgSystem.Module.cs + SharedBorgSystem.Relay.cs + SharedBorgSystem.Ui.cs +Silicons/Bots/ + EmaggableMedibotComponent.cs + HugBotComponent.cs + MedibotComponent.cs + MedibotSystem.cs + SharedHugBotSystem.cs +Silicons/Laws/Components/ + EmagSiliconLawComponent.cs + IonStormTargetComponent.cs + SiliconLawBoundComponent.cs + SiliconLawProviderComponent.cs + SiliconLawUpdaterComponent.cs + StartIonStormedComponent.cs +Silicons/Laws/ + IonLawPrototype.cs + SharedSiliconLawSystem.cs + SharedSiliconLawSystem.Updater.cs + SiliconLawEditEuiState.cs + SiliconLawPrototype.cs + SiliconLawsetPrototype.cs +Silicons/StationAi/ + SharedStationAiFixerConsoleSystem.cs + SharedStationAiSystem.Airlock.cs + SharedStationAiSystem.cs + SharedStationAiSystem.Customization.cs + SharedStationAiSystem.Held.cs + SharedStationAiSystem.Light.cs + StationAiCoreComponent.cs + StationAiCustomizationComponent.cs + StationAiCustomizationGroupPrototype.cs + StationAiCustomizationPrototype.cs + StationAiFixerConsoleComponent.cs + StationAiHeldComponent.cs + StationAiHolderComponent.cs + StationAiOverlayComponent.cs + StationAiVisionComponent.cs + StationAiVisionSystem.cs + StationAiWhitelistComponent.cs +Singularity/Components/ + ContainmentFieldComponent.cs + ContainmentFieldGeneratorComponent.cs + EventHorizonComponent.cs + SharedEmitterComponent.cs + SharedParticleAcceleratorComponent.cs + SharedRadiationCollectorComponent.cs + SingularityComponent.cs + SingularityDistortionComponent.cs + SingularityGeneratorComponent.cs +Singularity/EntitySystems/ + SharedEmitterSystem.cs + SharedEventHorizonSystem.cs + SharedGravityWellSystem.cs + SharedSingularityGeneratorSystem.cs + SharedSingularitySystem.cs +Singularity/ + Events/SingularityLevelChangedEvent.cs + SingularityVisualKeys.cs +Slippery/ + GetSlowedOverSlipperyModifierEvent.cs + NoSlipComponent.cs + SlidingComponent.cs + SlidingSystem.cs + SlipperyComponent.cs + SlipperySystem.cs + SlowedOverSlipperyComponent.cs +SmartFridge/ + SharedSmartFridgeSystem.cs + SmartFridgeComponent.cs +SMES/SharedSmesComponent.cs +Smoking/ + BurningComponent.cs + SmokableState.cs + SmokeVisuals.cs + SmokingVisuals.cs +Solar/SharedSolarControlConsoleComponent.cs +Sound/Components/ + BaseEmitSoundComponent.cs + EmitSoundOnActivateComponent.cs + EmitSoundOnCollideComponent.cs + EmitSoundOnDropComponent.cs + EmitSoundOnInteractUsingComponent.cs + EmitSoundOnLandComponent.cs + EmitSoundOnPickupComponent.cs + EmitSoundOnSpawnComponent.cs + EmitSoundOnThrowComponent.cs + EmitSoundOnUIOpenComponent.cs + EmitSoundOnUseComponent.cs + SpamEmitSoundComponent.cs + SpamEmitSoundRequirePowerComponent.cs +Sound/ + SharedEmitSoundSystem.cs + SharedSpamEmitSoundRequirePowerSystem.cs +Spawning/EntitySystemExtensions.cs +Species/Components/ + GibActionComponent.cs + NymphComponent.cs + ReformComponent.cs +Species/Systems/ + GibActionSystem.cs + ReformSystem.cs +Speech/ + AccentEvent.cs + Components/ + ActiveListenerComponent.cs + DamagedSiliconAccentComponent.cs + EmoteBlockerComponent.cs + MeleeSpeechComponent.cs + RatvarianLanguageComponent.cs + SpeakOnActionComponent.cs + VocalComponent.cs + EntitySystems/ + EmoteBlockerSystem.cs + SharedListenWireAction.cs + SharedMeleeSpeechSystem.cs + SharedRatvarianLanguageSystem.cs + SharedSlurredSystem.cs + SharedSpeakOnActionSystem.cs + SharedSpeechWireAction.cs + SharedStutteringSystem.cs + ListenEvent.cs + Muting/MutedComponent.cs + ScreamActionEvent.cs + SpeakAttemptEvent.cs + SpeechComponent.cs + SpeechSoundsPrototype.cs + SpeechSystem.cs + SpeechVerbPrototype.cs +Spider/ + IgnoreSpiderWebComponent.cs + SharedSpiderSystem.cs + SpiderComponent.cs + SpiderWebObjectComponent.cs +Spillable/SpillDoAfterEvent.cs +SprayPainter/Components/ + PaintableComponent.cs + PaintedComponent.cs + SprayPainterAmmo.cs + SprayPainterComponent.cs +SprayPainter/Prototypes/ + PaintableGroupCategoryPrototype.cs + PaintableGroupPrototype.cs +SprayPainter/ + SharedSprayPainterSystem.cs + SprayPainterAmmoSystem.cs + SprayPainterEvents.cs +Spreader/ + EdgeSpreaderPrototype.cs + KudzuVisuals.cs +Sprite/ + RandomSpriteComponent.cs + ScaleVisualsComponent.cs + SharedRandomSpriteSystem.cs + SharedScaleVisualsSystem.cs + SpriteFadeComponent.cs +SSDIndicator/ + SSDIndicatorComponent.cs + SSDIndicatorSystem.cs +Stacks/ + SharedStackSystem.API.cs + SharedStackSystem.cs + StackComponent.cs + StackPrototype.cs + StackSplitEvent.cs + StackThresholdComponent.cs + StackVisuals.cs +Standing/ + StandingStateComponent.cs + StandingStateSystem.cs +Station/Components/ + StationDataComponent.cs + StationMemberComponent.cs + StationTrackerComponent.cs +Station/ + SharedStationSpawningSystem.cs + SharedStationSystem.cs + SharedStationSystem.Tracker.cs + StationConfig.cs + StationsUpdatedEvent.cs +StationRecords/ + GeneralRecordsUi.cs + GeneralStationRecord.cs + SharedStationRecordsSystem.cs + StationRecordKey.cs + StationRecordKeyStorageComponent.cs + StationRecordKeyStorageSystem.cs + StationRecordsComponent.cs + StationRecordSet.cs + StationRecordsFilter.cs +StatusEffect/ + StatusEffectPrototype.cs + StatusEffectsComponent.cs + StatusEffectsSystem.cs +StatusEffectNew/Components/ + CloneableStatusEffectComponent.cs + RejuvenateRemovedStatusEffectComponent.cs + StatusEffectAlertComponent.cs + StatusEffectComponent.cs + StatusEffectContainerComponent.cs +StatusEffectNew/ + StatusEffectAlertSystem.cs + StatusEffectsSystem.cs + StatusEffectSystem.API.cs + StatusEffectSystem.Relay.cs +StatusIcon/Components/ + JobStatusComponent.cs + StatusIconComponent.cs +StatusIcon/ + SharedStatusIconSystem.cs + StatusIconPrototype.cs +Stealth/Components/ + StealthComponent.cs + StealthOnMoveComponent.cs +Stealth/SharedStealthSystem.cs +StepTrigger/Components/ + PreventableStepTriggerComponent.cs + ProtectedFromStepTriggersComponent.cs + StepTriggerComponent.cs +StepTrigger/Systems/ + StepTriggerImmuneSystem.cs + StepTriggerSystem.cs +Sticky/Components/ + StickyComponent.cs + StickyVisualizerComponent.cs +Sticky/ + EntityStuckEvent.cs + StickyDoAfterEvent.cs + Systems/StickySystem.cs +Storage/Components/ + AnchoredStorageFilterComponent.cs + BinComponent.cs + DumpableComponent.cs + EntityStorageComponent.cs + InsideEntityStorageComponent.cs + ItemCounterComponent.cs + ItemMapperComponent.cs + MagnetPickupComponent.cs + PickRandomComponent.cs + SecretStashComponent.cs + SharedBagOpenVisuals.cs + SharedMapLayerData.cs + StorageFillComponent.cs + StorageFillVisualizerComponent.cs + StoreAfterFailedInteractComponent.cs + StoreOnCollideComponent.cs +Storage/ + EntitySpawnEntry.cs + EntitySystems/ + AnchoredStorageFilterSystem.cs + BinSystem.cs + BluespaceLockerDoAfterEvent.cs + DumpableSystem.cs + MagnetPickupSystem.cs + PickRandomSystem.cs + SecretStashSystem.cs + SharedEntityStorageSystem.cs + SharedItemCounterSystem.cs + SharedItemMapperSystem.cs + SharedStorageSystem.cs + StoreAfterFailedInteractSystem.cs + StoreOnCollideSystem.cs + Events.cs + Events/StorageInsertFailedEvent.cs + ItemStorageLocation.cs + StorageComponent.cs + StorageHelpers.cs +Store/Components/ + CurrencyComponent.cs + RemoteStoreComponent.cs + StoreComponent.cs +Store/ + CurrencyPrototype.cs + Events/IntrinsicStoreActionEvent.cs + ListingCondition.cs + ListingLocalisationHelpers.cs + ListingPrototype.cs + SharedStoreSystem.cs + SharedStoreSystem.Listings.cs + SharedStoreSystem.UI.cs + StoreCategoryPrototype.cs + StorePresetPrototype.cs + StoreUi.cs +StoreDiscount/Components/StoreDiscountComponent.cs +StoryGen/ + EntitySystems/StoryGeneratorSystem.cs + Prototypes/StoryTemplatePrototype.cs +Strip/Components/ + StrippableComponent.cs + StrippingComponent.cs + ThievingComponent.cs +Strip/ + SharedStrippableSystem.cs + ThievingSystem.cs +Stunnable/ + CrawlerComponent.cs + KnockdownStatusEffectComponent.cs + KnockedDownComponent.cs + SharedStunbatonSystem.cs + SharedStunSystem.cs + SharedStunSystem.Knockdown.cs + SharedStunSystem.Visualizer.cs + StunbatonComponent.cs + StunnableEvents.cs + StunnedComponent.cs + StunnedStatusEffectComponent.cs + StunOnContactComponent.cs + StunVisualsComponent.cs +SubFloor/ + SharedSubFloorHideSystem.cs + SharedTrayScannerSystem.cs + SubFloorHideComponent.cs + TrayScannerComponent.cs + TrayScannerUserComponent.cs + TrayScanRevealComponent.cs +SurveillanceCamera/Components/ + CameraActiveOnCollideColliderComponent.cs + CameraActiveOnCollideComponent.cs + SurveillanceCameraComponent.cs + SurveillanceCameraMapComponent.cs +SurveillanceCamera/ + SharedSurveillanceCameraMonitorSystem.cs + SharedSurveillanceCameraSystem.cs +Survivor/Components/SurvivorComponent.cs +Swab/SwabEvents.cs +Tabletop/ + Components/TabletopDraggableComponent.cs + Events/ + TabletopDraggingPlayerChangedEvent.cs + TabletopMoveEvent.cs + TabletopPlayEvent.cs + TabletopStopPlayingEvent.cs + SharedTabletopSystem.cs + TabletopItemVisuals.cs +Tag/ + TagComponent.cs + TagPrototype.cs + TagSystem.cs +Telephone/ + SharedTelephoneSystem.cs + TelephoneComponent.cs +Teleportation/Components/ + HandTeleporterComponent.cs + LinkedEntityComponent.cs + PortalComponent.cs + PortalTimeoutComponent.cs + SwapTeleporterComponent.cs + TeleportLocationsComponent.cs +Teleportation/Systems/ + LinkedEntitySystem.cs + SharedPortalSystem.cs + SharedTeleportLocationsSystem.cs + SwapTeleporterSystem.cs +Teleportation/TeleportLocationsUi.cs +Temperature/Components/ + AlwaysHotComponent.cs + EntityHeaterComponent.cs + TemperatureComponent.cs + TemperatureDamageComponent.cs + TemperatureSpeedComponent.cs +Temperature/HeatContainer/ + HeatContainer.cs + HeatContainerHelpers.Conduct.cs + HeatContainerHelpers.cs + HeatContainerHelpers.Divide.cs + HeatContainerHelpers.Exchange.cs + HeatContainerHelpers.Merge.cs + IHeatContainer.cs +Temperature/ + IsHotEvent.cs + SharedEntityHeater.cs + Systems/ + AlwaysHotSystem.cs + SharedEntityHeaterSystem.cs + SharedTemperatureSystem.cs + TemperatureEvents.cs + TemperatureHelpers.cs +Tests/TestMarkerComponent.cs +TextScreen/TextScreenVisuals.cs +Thief/Components/ + ThiefBackpackUI.cs + ThiefBeaconComponent.cs +Thief/ + Prototypes/ThiefBackpackSetPrototype.cs + Systems/ThiefBeaconSystem.cs +Throwing/ + BeforeThrowEvent.cs + CatchableComponent.cs + CatchableSystem.cs + CatchAttemptEvent.cs + LandAtCursorComponent.cs + LandEvent.cs + ThrowAttemptEvent.cs + ThrowEvents.cs + ThrowingAngleComponent.cs + ThrowingSystem.cs + ThrownItemComponent.cs + ThrownItemSystem.cs +Tiles/ + FloorTileAttemptEvent.cs + FloorTileComponent.cs + FloorTileSystem.cs + ProtectedGridComponent.cs + ProtectedGridSystem.cs + ReplaceFloorOnSpawnComponent.cs + ReplaceFloorOnSpawnSystem.cs + RequiresTileComponent.cs +Timing/ + StartEndTime.cs + UseDelayComponent.cs + UseDelaySystem.cs +Tips/ + SharedTipsSystem.cs + TippyEvent.cs +Toggleable/ToggleActionEvent.cs +Toilet/ + Components/ToiletComponent.cs + Systems/ToiletSystem.cs +Tools/Components/ + LayerChangeOnWeldComponent.cs + MultipleToolComponent.cs + SimpleToolUsageComponent.cs + ToolComponent.cs + ToolOpenableComponent.cs + ToolRefinableComponent.cs + ToolTileCompatibleComponent.cs + WeldableComponent.cs + WelderComponent.cs +Tools/Systems/ + SharedToolSystem.cs + SharedToolSystem.MultipleTool.cs + SharedToolSystem.Tile.cs + SharedToolSystem.Welder.cs + SimpleToolUsageSystem.cs + ToolOpenableSystem.cs + ToolRefinableSystem.cs + WeldableAttemptEvent.cs + WeldableChangedEvent.cs + WeldableSystem.cs + WeldFinishedEvent.cs +Tools/ToolQualityPrototype.cs +Toolshed/TypeParsers/FixedPoint2TypeParser.cs +Traits/Assorted/ + AccentlessComponent.cs + AccentlessSystem.cs + HemophiliaComponent.cs + HemophiliaStatusEffectComponent.cs + HemophiliaSystem.cs + ImpairedMobilityComponent.cs + ImpairedMobilitySystem.cs + LegsParalyzedComponent.cs + LegsParalyzedSystem.cs + LightweightDrunkComponent.cs + MobilityAidComponent.cs + MobilityAidSystem.cs + NarcolepsyComponent.cs + NarcolepsySystem.cs + PainNumbnessStatusEffectComponent.cs + PainNumbnessSystem.cs + ParacusiaComponent.cs + PermanentBlindnessComponent.cs + PermanentBlindnessSystem.cs + SharedParacusiaSystem.cs + UnrevivableComponent.cs +Traits/ + TraitCategoryPrototype.cs + TraitPrototype.cs +Trigger/Components/ + ActiveTimerTriggerComponent.cs + ActiveTwoStageTriggerComponent.cs + Conditions/ + BaseTriggerConditionComponent.cs + MindRoleTriggerConditionComponent.cs + RandomChanceTriggerConditionComponent.cs + ToggleTriggerConditionComponent.cs + UseDelayTriggerConditionComponent.cs + WhitelistTriggerConditionComponent.cs + Effects/ + AddComponentsOnTriggerComponent.cs + AddGameRuleOnTriggerComponent.cs + AddSolutionOnTriggerComponent.cs + AddTagsOnTriggerComponent.cs + AdminLogOnTriggerComponent.cs + AlertLevelChangeOnTriggerComponent.cs + AnchorOnTriggerComponent.cs + BaseXOnTriggerComponent.cs + CleanContainersOnTriggerComponent.cs + DamageOnTriggerComponent.cs + DeleteOnTriggerComponent.cs + DnaScrambleOnTriggerComponent.cs + EmitSoundOnTriggerComponent.cs + EmpOnTriggerComponent.cs + EmptyContainersOnTriggerComponent.cs + EntityEffectOnTriggerComponent.cs + ExplodeOnTriggerComponent.cs + ExplosionOnTriggerComponent.cs + ExtinguishOnTriggerComponent.cs + FireStackOnTriggerComponent.cs + FlashOnTriggerComponent.cs + GhostKickOnTriggerComponent.cs + GibOnTriggerComponent.cs + IgniteOnTriggerComponent.cs + ItemToggleOnTriggerComponent.cs + JitterOnTriggerComponent.cs + KnockdownOnTriggerComponent.cs + LaunchOnTriggerComponent.cs + LightBehaviorOnTriggerComponent.cs + LockOnTriggerComponent.cs + PolymorphOnTriggerComponent.cs + PopupOnTriggerComponent.cs + RandomTriggerOnTriggerComponent.cs + RattleOnTriggerComponent.cs + RejuventateOnTriggerComponent.cs + ReleaseGasOnTriggerComponent.cs + RemoveComponentsOnTriggerComponent.cs + RemoveTagsOnTriggerComponent.cs + RepulseAttractOnTriggerComponent.cs + ScramOnTriggerComponent.cs + ShockOnTriggerComponent.cs + SignalOnTriggerComponent.cs + SmokeOnTriggerComponent.cs + SpawnEntityTableOnTriggerComponent.cs + SpawnOnTriggerComponent.cs + SpeakOnTriggerComponent.cs + StaminaDamageOnTriggerComponent.cs + StunOnTriggerComponent.cs + SwapLocationOnTriggerComponent.cs + TippyOnTriggerComponent.cs + ToggleComponentsOnTriggerComponent.cs + UncuffOnTriggerComponent.cs + UseDelayOnTriggerComponent.cs + WeatherOnTriggerComponent.cs + RandomTimerTriggerComponent.cs + TimerTriggerComponent.cs + Triggers/ + ActiveTriggerOnTimedCollideComponent.cs + BaseTriggerOnXComponent.cs + RepeatingTriggerComponent.cs + TriggerOnActivateComponent.cs + TriggerOnActivateImplantComponent.cs + TriggerOnBuckledComponent.cs + TriggerOnCollideComponent.cs + TriggerOnDidEquipComponent.cs + TriggerOnDidEquipHandComponent.cs + TriggerOnDidUnequipComponent.cs + TriggerOnDidUnequipHandComponent.cs + TriggerOnDroppedComponent.cs + TriggerOnEmbedComponent.cs + TriggerOnEmptyGunshotComponent.cs + TriggerOnExaminedComponent.cs + TriggerOnGotEquippedComponent.cs + TriggerOnGotEquippedHandComponent.cs + TriggerOnGotInsertedIntoContainerComponent.cs + TriggerOnGotRemovedFromContainerComponent.cs + TriggerOnGotUnequippedComponent.cs + TriggerOnGotUnequippedHandComponent.cs + TriggerOnHitscanFiredComponent.cs + TriggerOnHitscanHitComponent.cs + TriggerOnIngestedComponent.cs + TriggerOnInsertedIntoContainerComponent.cs + TriggerOnInteractHandComponent.cs + TriggerOnInteractUsingComponent.cs + TriggerOnLandComponent.cs + TriggerOnMeleeHitComponent.cs + TriggerOnMeleeMissComponent.cs + TriggerOnMeleeSwingComponent.cs + TriggerOnMobstateChangeComponent.cs + TriggerOnPlayerSpawnCompleteComponent.cs + TriggerOnProximityComponent.cs + TriggerOnRemovedFromContainerComponent.cs + TriggerOnRotComponent.cs + TriggerOnRoundEndComponent.cs + TriggerOnSignalComponent.cs + TriggerOnSimpleToolUsage.cs + TriggerOnSlipComponent.cs + TriggerOnSpawnComponent.cs + TriggerOnStepTriggerComponent.cs + TriggerOnStrappedComponent.cs + TriggerOnStuckComponent.cs + TriggerOnThrowComponent.cs + TriggerOnThrowDoHitComponent.cs + TriggerOnThrownComponent.cs + TriggerOnTimedCollideComponent.cs + TriggerOnUiCloseComponent.cs + TriggerOnUiOpenComponent.cs + TriggerOnUnbuckledComponent.cs + TriggerOnUnembedComponent.cs + TriggerOnUnstrappedComponent.cs + TriggerOnUseComponent.cs + TriggerOnUserInteractHandComponent.cs + TriggerOnUserInteractUsingComponent.cs + TriggerOnVerb.cs + TriggerOnVoiceComponent.cs + TwoStageTriggerComponent.cs +Trigger/ + SharedTriggerSystems.cs + Systems/ + AdminLogOnTriggerSystem.cs + ComponentsOnTriggerSystem.cs + DamageOnTriggerSystem.cs + DnaScrambleOnTriggerSystem.cs + EmitSoundOnTriggerSystem.cs + EmpOnTriggerSystem.cs + EmptyContainersOnTriggerSystem.cs + EntityEffectOnTriggerSystem.cs + ExplodeOnTriggerSystem.cs + FlashOnTriggerSystem.cs + GibOnTriggerSystem.cs + JitterOnTriggerSystem.cs + KnockdownOnTriggerSystem.cs + LaunchOnTriggerSystem.cs + LockOnTriggerSystem.cs + PopupOnTriggerSystem.cs + RandomTriggerOnTriggerSystem.cs + RejuvenateOnTriggerSystem.cs + RepulseAttractOnTriggerSystem.cs + ScramOnTriggerSystem.cs + SharedReleaseGasOnTriggerSystem.cs + ShockOnTriggerSystem.cs + SolutionTriggerSystem.cs + StaminaDamageOnTriggerSystem.cs + StunOnTriggerSystem.cs + SwapLocationOnTriggerSystem.cs + TagOnTriggerSystem.cs + TippyOnTriggerSystem.cs + TriggerOnActivateImplantSystem.cs + TriggerOnContainerInteractionSystem.cs + TriggerOnEmbedSystem.cs + TriggerOnEmptyGunshotSystem.cs + TriggerOnEquipmentSystem.cs + TriggerOnHandSystem.cs + TriggerOnHitscanSystem.cs + TriggerOnIngestedSystem.cs + TriggerOnLandSystem.cs + TriggerOnMeleeSystem.cs + TriggerOnMobstateChangeSystem.cs + TriggerOnRotSystem.cs + TriggerOnRoundEndSystem.cs + TriggerOnSlipSystem.cs + TriggerOnStrappedOrBuckledSystem.cs + TriggerOnStuckSystem.cs + TriggerOnThrowDoHitSystem.cs + TriggerOnToolUseSystem.cs + TriggerOnVerbSystem.cs + TriggerSystem.Collide.cs + TriggerSystem.Condition.cs + TriggerSystem.cs + TriggerSystem.Interaction.cs + TriggerSystem.Proximity.cs + TriggerSystem.Signal.cs + TriggerSystem.Spawn.cs + TriggerSystem.Timer.cs + TriggerSystem.Voice.cs + TwoStageTriggerSystem.cs + UncuffOnTriggerSystem.cs + WeatherTriggerSystem.cs + TriggerEvent.cs + TriggerVisuals.cs + VoiceTriggeredEvent.cs +TurretController/ + DeployableTurretControllerComponent.cs + SharedDeployableTurretControllerSystem.cs +Turrets/ + DeployableTurretComponent.cs + SharedDeployableTurretSystem.cs + StationAiTurretComponent.cs + TurretTargetSettingsComponent.cs + TurretTargetSettingsSystem.cs +Udder/MilkingDoAfterEvent.cs +UserInterface/ + ActivatableUIComponent.cs + ActivatableUIEvents.cs + ActivatableUIRequiresAnchorComponent.cs + ActivatableUIRequiresAnchorSystem.cs + ActivatableUIRequiresPowerCellComponent.cs + ActivatableUISystem.cs + ActivatableUISystem.Power.cs + IntrinsicUIComponent.cs + IntrinsicUISystem.cs + OpenUiActionEvent.cs + StatValuesEuiMessage.cs + ToggleIntrinsicUIEvent.cs +Vapor/SharedVaporComponent.cs +VendingMachines/ + SharedVendingMachineSystem.cs + SharedVendingMachineSystem.Restock.cs + VendingMachineComponent.cs + VendingMachineInterfaceState.cs + VendingMachineInventoryPrototype.cs + VendingMachineRestockComponent.cs +Verbs/ + SharedVerbSystem.cs + Verb.cs + VerbCategory.cs + VerbEvents.cs +VoiceMask/ + SharedVoiceMaskSystem.cs + VoiceMaskComponent.cs + VoiceMaskSetNameEvent.cs +Voting/ + MsgVoteCanCall.cs + MsgVoteData.cs + MsgVoteMenu.cs + StandardVoteType.cs + VotingEvents.cs +Wagging/WaggingComponent.cs +Wall/ + ReinforcedWallVisuals.cs + ShuttleWallVisuals.cs + WallMountComponent.cs +Warps/ + WarpPointComponent.cs + WarpPointSystem.cs +Weapons/Hitscan/Components/ + HitscanAmmoComponent.cs + HitscanBasicDamageComponent.cs + HitscanBasicEffectsComponent.cs + HitscanBasicRaycastComponent.cs + HitscanBasicVisualsComponent.cs + HitscanReflectComponent.cs + HitscanStaminaDamageComponent.cs +Weapons/Hitscan/ + Events/HitscanEvents.cs + Systems/ + HitscanBasicDamageSystem.cs + HitscanBasicEffectsSystem.cs + HitscanBasicRaycastSystem.cs + HitscanReflectSystem.cs + HitscanStunSystem.cs +Weapons/Marker/ + DamageMarkerComponent.cs + DamageMarkerOnCollideComponent.cs + LeechOnMarkerComponent.cs + SharedDamageMarkerSystem.cs +Weapons/Melee/ + Balloon/BalloonPopperComponent.cs + Components/ + AltFireMeleeComponent.cs + BonusMeleeAttackRateComponent.cs + BonusMeleeDamageComponent.cs + MeleeRequiresWieldComponent.cs + MeleeSoundComponent.cs + MeleeThrowOnHitComponent.cs + UseDelayOnMeleeHitComponent.cs + EnergySword/ + EnergySwordComponent.cs + EnergySwordSystem.cs + Events/ + AttackEvent.cs + AttemptMeleeEvent.cs + DisarmAttackEvent.cs + HeavyAttackEvent.cs + LightAttackEvent.cs + MeleeAttackEvent.cs + MeleeHitEvent.cs + MeleeLungeEvent.cs + StopAttackEvent.cs + MeleeSoundSystem.cs + MeleeThrowOnHitSystem.cs + MeleeWeaponComponent.cs + SharedMeleeWeaponSystem.cs + UseDelayOnMeleeHitSystem.cs +Weapons/Misc/ + BaseForceGunComponent.cs + ForceGunComponent.cs + GrapplingProjectileComponent.cs + SharedGrapplingGunSystem.cs + SharedTetherGunSystem.cs + SharedTetherGunSystem.Force.cs + TetheredComponent.cs + TetherGunComponent.cs +Weapons/Ranged/Components/ + ActionGunComponent.cs + AmmoComponent.cs + AmmoProviderComponent.cs + AutoShootGunComponent.cs + BallisticAmmoInteractLoaderComponent.cs + BallisticAmmoProviderComponent.cs + BallisticAmmoSelfRefillerComponent.cs + BasicEntityAmmoProviderComponent.cs + BatteryAmmoProviderComponent.cs + BatteryWeaponFireModesComponent.cs + ChamberMagazineAmmoProviderComponent.cs + ClothingSlotAmmoProviderComponent.cs + ContainerAmmoProviderComponent.cs + FlyBySoundComponent.cs + GrapplingGunComponent.cs + GunComponent.cs + GunRequiresWieldComponent.cs + GunSpreadModifierComponent.cs + GunWieldBonusComponent.cs + MagazineAmmoProviderComponent.cs + RechargeBasicEntityAmmoComponent.cs + RechargeCycleAmmoComponent.cs + ReflectiveComponent.cs + RevolverAmmoProviderComponent.cs + SharedAmmoCounterComponent.cs + SolutionAmmoProviderComponent.cs + SpeedLoaderComponent.cs + TargetedProjectileComponent.cs + UseDelayOnShootComponent.cs +Weapons/Ranged/Events/ + AmmoShotEvent.cs + BeforeAmmoLoadedEvent.cs + BeforeGunShootEvent.cs + GetAmmoCountEvent.cs + GunCycledEvent.cs + GunGetAmmoSpreadEvent.cs + GunMuzzleFlashAttemptEvent.cs + GunRefreshModifiersEvent.cs + HitScanReflectAttempt.cs + MuzzleFlashEvent.cs + OnEmptyGunShotEvent.cs + RequestShootEvent.cs + RequestStopShootEvent.cs + ShotAttemptedEvent.cs + TakeAmmoEvent.cs +Weapons/Ranged/ + IShootable.cs + Systems/ + ActionGunSystem.cs + BatteryWeaponFireModesSystem.cs + GunSpreadModifierSystem.cs + RechargeBasicEntityAmmoSystem.cs + RechargeCycleAmmoSystem.cs + SharedFlyBySoundSystem.cs + SharedGunSystem.AutoFire.cs + SharedGunSystem.Ballistic.cs + SharedGunSystem.BasicEntity.cs + SharedGunSystem.Battery.cs + SharedGunSystem.Cartridges.cs + SharedGunSystem.ChamberMagazine.cs + SharedGunSystem.Clothing.cs + SharedGunSystem.Container.cs + SharedGunSystem.cs + SharedGunSystem.Interactions.cs + SharedGunSystem.Magazine.cs + SharedGunSystem.Revolver.cs + SharedGunSystem.Solution.cs + UseDelayOnShootSystem.cs + Upgrades/Components/ + GunUpgradeComponent.cs + GunUpgradeDamageComponent.cs + GunUpgradeFireRateComponent.cs + GunUpgradeSpeedComponent.cs + UpgradeableGunComponent.cs + Upgrades/GunUpgradeSystem.cs +Weapons/Reflect/ + ReflectComponent.cs + ReflectSystem.cs +Weather/ + BlockWeatherComponent.cs + SharedWeatherSystem.cs + WeatherStatusEffectComponent.cs +Whistle/ + WhistleComponent.cs + WhistleSystem.cs +Whitelist/ + EntityWhitelist.cs + EntityWhitelistSystem.cs +Wieldable/Components/ + IncreaseDamageOnWieldComponent.cs + WieldableComponent.cs + WieldingBlockerComponent.cs +Wieldable/ + Events.cs + SharedWieldableSystem.cs +Wires/ + ActivatableUIRequiresPanelComponent.cs + Events.cs + SharedWiresComponent.cs + SharedWiresSystem.cs + SharedWireVisComponent.cs + WiresPanelComponent.cs + WiresPanelSecurityComponent.cs +Xenoarchaeology/Artifact/Components/ + XenoArtifactComponent.cs + XenoArtifactNodeComponent.cs + XenoArtifactUnlockingComponent.cs +Xenoarchaeology/Artifact/ + Prototypes/XenoArchTriggerPrototype.cs + SharedXenoArtifactSystem.cs + SharedXenoArtifactSystem.Graph.cs + SharedXenoArtifactSystem.Node.cs + SharedXenoArtifactSystem.Unlock.cs + SharedXenoArtifactSystem.XAE.cs + SharedXenoArtifactSystem.XAT.cs + XAE/ + BaseXAESystem.cs + Components/ + XAEApplyComponentsComponent.cs + XAEDamageInAreaComponent.cs + XAEKnockComponent.cs + XAERandomTeleportInvokerComponent.cs + XAERemoveCollisionComponent.cs + XAEShuffleComponent.cs + XAEApplyComponentsSystem.cs + XAEDamageInAreaSystem.cs + XAEKnockSystem.cs + XAERandomTeleportInvokerSystem.cs + XAERemoveCollisionSystem.cs + XAEShuffleSystem.cs + XAT/ + BaseQueryUpdateXATSystem.cs + BaseXATSystem.cs + Components/ + XATCompNearbyComponent.cs + XATDamageThresholdReachedComponent.cs + XATDeathComponent.cs + XATExaminableTextComponent.cs + XATExamineComponent.cs + XATInteractionComponent.cs + XATItemLandComponent.cs + XATReactiveComponent.cs + XATTimerComponent.cs + XATToolUseComponent.cs + XATCompNearbySystem.cs + XATDamageThresholdReachedSystem.cs + XATDeathSystem.cs + XATExaminableTextSystem.cs + XATExamineSystem.cs + XATInteractionSystem.cs + XATItemLandSystem.cs + XATReactiveSystem.cs + XATTimerSystem.cs + XATToolUseSystem.cs +Xenoarchaeology/Equipment/Components/ + AnalysisConsoleComponent.cs + ArtifactAnalyzerComponent.cs + ArtifactCrusherComponent.cs + NodeScannerComponent.cs + SuppressArtifactContainerComponent.cs +Xenoarchaeology/Equipment/ + NodeScannerSystem.cs + SharedArtifactAnalyzerSystem.cs + SharedArtifactCrusherSystem.cs + SuppressArtifactContainerSystem.cs +Xenoarchaeology/XenoArtifacts/ + RandomArtifactSpriteComponent.cs + SharedArtifact.cs +Xenoborgs/Components/ + MothershipCoreComponent.cs + XenoborgComponent.cs +Zombies/ + IncurableZombieComponent.cs + InitialInfectedComponent.cs + PendingZombieComponent.cs + SharedZombieSystem.cs + ZombieComponent.cs + ZombieEvents.cs + ZombieImmuneComponent.cs + ZombificationResistanceComponent.cs + ZombifyOnDeathComponent.cs + +[metadataReferences] +../ + Content.Shared.Database/obj/Debug/net10.0/ref/Content.Shared.Database.dll + RobustToolbox/ + Lidgren.Network/obj/Debug/net10.0/ref/SpaceWizards.Lidgren.Network.dll + NetSerializer/NetSerializer/obj/Debug/net10.0/ref/Robust.NetSerializer.dll + Robust.Shared.Maths/obj/Debug/ref/Robust.Shared.Maths.dll + Robust.Shared/obj/Debug/ref/Robust.Shared.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/ref/net10.0/ + Microsoft.CSharp.dll + Microsoft.VisualBasic.Core.dll + Microsoft.VisualBasic.dll + Microsoft.Win32.Primitives.dll + Microsoft.Win32.Registry.dll + mscorlib.dll + netstandard.dll + System.AppContext.dll + System.Buffers.dll + System.Collections.Concurrent.dll + System.Collections.dll + System.Collections.Immutable.dll + System.Collections.NonGeneric.dll + System.Collections.Specialized.dll + System.ComponentModel.Annotations.dll + System.ComponentModel.DataAnnotations.dll + System.ComponentModel.dll + System.ComponentModel.EventBasedAsync.dll + System.ComponentModel.Primitives.dll + System.ComponentModel.TypeConverter.dll + System.Configuration.dll + System.Console.dll + System.Core.dll + System.Data.Common.dll + System.Data.DataSetExtensions.dll + System.Data.dll + System.Diagnostics.Contracts.dll + System.Diagnostics.Debug.dll + System.Diagnostics.DiagnosticSource.dll + System.Diagnostics.FileVersionInfo.dll + System.Diagnostics.Process.dll + System.Diagnostics.StackTrace.dll + System.Diagnostics.TextWriterTraceListener.dll + System.Diagnostics.Tools.dll + System.Diagnostics.TraceSource.dll + System.Diagnostics.Tracing.dll + System.dll + System.Drawing.dll + System.Drawing.Primitives.dll + System.Dynamic.Runtime.dll + System.Formats.Asn1.dll + System.Formats.Tar.dll + System.Globalization.Calendars.dll + System.Globalization.dll + System.Globalization.Extensions.dll + System.IO.Compression.Brotli.dll + System.IO.Compression.dll + System.IO.Compression.FileSystem.dll + System.IO.Compression.ZipFile.dll + System.IO.dll + System.IO.FileSystem.AccessControl.dll + System.IO.FileSystem.dll + System.IO.FileSystem.DriveInfo.dll + System.IO.FileSystem.Primitives.dll + System.IO.FileSystem.Watcher.dll + System.IO.IsolatedStorage.dll + System.IO.MemoryMappedFiles.dll + System.IO.Pipelines.dll + System.IO.Pipes.AccessControl.dll + System.IO.Pipes.dll + System.IO.UnmanagedMemoryStream.dll + System.Linq.AsyncEnumerable.dll + System.Linq.dll + System.Linq.Expressions.dll + System.Linq.Parallel.dll + System.Linq.Queryable.dll + System.Memory.dll + System.Net.dll + System.Net.Http.dll + System.Net.Http.Json.dll + System.Net.HttpListener.dll + System.Net.Mail.dll + System.Net.NameResolution.dll + System.Net.NetworkInformation.dll + System.Net.Ping.dll + System.Net.Primitives.dll + System.Net.Quic.dll + System.Net.Requests.dll + System.Net.Security.dll + System.Net.ServerSentEvents.dll + System.Net.ServicePoint.dll + System.Net.Sockets.dll + System.Net.WebClient.dll + System.Net.WebHeaderCollection.dll + System.Net.WebProxy.dll + System.Net.WebSockets.Client.dll + System.Net.WebSockets.dll + System.Numerics.dll + System.Numerics.Vectors.dll + System.ObjectModel.dll + System.Reflection.DispatchProxy.dll + System.Reflection.dll + System.Reflection.Emit.dll + System.Reflection.Emit.ILGeneration.dll + System.Reflection.Emit.Lightweight.dll + System.Reflection.Extensions.dll + System.Reflection.Metadata.dll + System.Reflection.Primitives.dll + System.Reflection.TypeExtensions.dll + System.Resources.Reader.dll + System.Resources.ResourceManager.dll + System.Resources.Writer.dll + System.Runtime.CompilerServices.Unsafe.dll + System.Runtime.CompilerServices.VisualC.dll + System.Runtime.dll + System.Runtime.Extensions.dll + System.Runtime.Handles.dll + System.Runtime.InteropServices.dll + System.Runtime.InteropServices.JavaScript.dll + System.Runtime.InteropServices.RuntimeInformation.dll + System.Runtime.Intrinsics.dll + System.Runtime.Loader.dll + System.Runtime.Numerics.dll + System.Runtime.Serialization.dll + System.Runtime.Serialization.Formatters.dll + System.Runtime.Serialization.Json.dll + System.Runtime.Serialization.Primitives.dll + System.Runtime.Serialization.Xml.dll + System.Security.AccessControl.dll + System.Security.Claims.dll + System.Security.Cryptography.Algorithms.dll + System.Security.Cryptography.Cng.dll + System.Security.Cryptography.Csp.dll + System.Security.Cryptography.dll + System.Security.Cryptography.Encoding.dll + System.Security.Cryptography.OpenSsl.dll + System.Security.Cryptography.Primitives.dll + System.Security.Cryptography.X509Certificates.dll + System.Security.dll + System.Security.Principal.dll + System.Security.Principal.Windows.dll + System.Security.SecureString.dll + System.ServiceModel.Web.dll + System.ServiceProcess.dll + System.Text.Encoding.CodePages.dll + System.Text.Encoding.dll + System.Text.Encoding.Extensions.dll + System.Text.Encodings.Web.dll + System.Text.Json.dll + System.Text.RegularExpressions.dll + System.Threading.AccessControl.dll + System.Threading.Channels.dll + System.Threading.dll + System.Threading.Overlapped.dll + System.Threading.Tasks.Dataflow.dll + System.Threading.Tasks.dll + System.Threading.Tasks.Extensions.dll + System.Threading.Tasks.Parallel.dll + System.Threading.Thread.dll + System.Threading.ThreadPool.dll + System.Threading.Timer.dll + System.Transactions.dll + System.Transactions.Local.dll + System.ValueTuple.dll + System.Web.dll + System.Web.HttpUtility.dll + System.Windows.dll + System.Xml.dll + System.Xml.Linq.dll + System.Xml.ReaderWriter.dll + System.Xml.Serialization.dll + System.Xml.XDocument.dll + System.Xml.XmlDocument.dll + System.Xml.XmlSerializer.dll + System.Xml.XPath.dll + System.Xml.XPath.XDocument.dll + WindowsBase.dll +/ + jetbrains.annotations/2025.2.4/lib/netstandard2.0/JetBrains.Annotations.dll + robust.shared.authlib/0.1.2/lib/netstandard2.1/Robust.Shared.AuthLib.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll + +[analyzerReferences] +../RobustToolbox/ + Robust.Analyzers/bin/Debug/netstandard2.0/Robust.Analyzers.dll + Robust.Serialization.Generator/bin/Debug/netstandard2.0/Robust.Serialization.Generator.dll + Robust.Shared.CompNetworkGenerator/bin/Debug/netstandard2.0/Robust.Shared.CompNetworkGenerator.dll +/packs/Microsoft.NETCore.App.Ref/10.0.1/analyzers/dotnet/cs/ + Microsoft.Interop.LibraryImportGenerator.dll + Microsoft.Interop.SourceGeneration.dll + System.Text.Json.SourceGeneration.dll + System.Text.RegularExpressions.Generator.dll +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/ + Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll + Microsoft.CodeAnalysis.NetAnalyzers.dll + +[analyzerConfigFiles] +../.editorconfig +/sdk/10.0.101/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig +obj/Debug/Content.Shared.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Content.Shared/Crayon/CrayonComponent.cs b/Content.Shared/Crayon/CrayonComponent.cs new file mode 100644 index 00000000000..c772b43d000 --- /dev/null +++ b/Content.Shared/Crayon/CrayonComponent.cs @@ -0,0 +1,119 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Crayon; + +/// +/// Component holding the state of a crayon-like component +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedCrayonSystem))] +public sealed partial class CrayonComponent : Component +{ + /// + /// The ID of currently selected decal prototype that will be placed when the crayon is used. + /// + [DataField, AutoNetworkedField] + public string SelectedState; + + /// + /// Color with which the crayon will draw. + /// + [DataField, AutoNetworkedField] + public Color Color; + + /// + /// Play a sound when drawing if specified. + /// + [DataField] + public SoundSpecifier? UseSound; + + /// + /// Can the color can be changed? + /// + [DataField, AutoNetworkedField] + public bool SelectableColor; + + /// + /// Should the crayon be deleted when all charges are consumed? + /// + [DataField, AutoNetworkedField] + public bool DeleteEmpty = true; +} + +/// +/// Opens the crayon window for decal and color selection. +/// +[Serializable, NetSerializable] +public enum CrayonUiKey : byte +{ + Key, +} + +/// +/// Used by the client to notify the server about the selected decal ID +/// +[Serializable, NetSerializable] +public sealed class CrayonSelectMessage : BoundUserInterfaceMessage +{ + public readonly string State; + public CrayonSelectMessage(string selected) + { + State = selected; + } +} + +/// +/// Sets the color of the crayon, used by Rainbow Crayon +/// +[Serializable, NetSerializable] +public sealed class CrayonColorMessage : BoundUserInterfaceMessage +{ + public readonly Color Color; + public CrayonColorMessage(Color color) + { + Color = color; + } +} + +/// +/// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn. +/// Allows the client UI to advance forward in the client-only ephemeral queue, +/// preventing the crayon from becoming a magic text storage device. +/// +[Serializable, NetSerializable] +public sealed class CrayonUsedMessage : BoundUserInterfaceMessage +{ + public readonly string DrawnDecal; + + public CrayonUsedMessage(string drawn) + { + DrawnDecal = drawn; + } +} + +/// +/// The state of the crayon UI as sent by the server +/// +/// +/// TODO: Delete this and use component states and predict the UI. +/// This info is already networked on its own. +/// +[Serializable, NetSerializable] +public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState +{ + public string Selected; + /// + /// Can the color can be changed + /// + public bool SelectableColor; + public Color Color; + + public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color) + { + Selected = selected; + SelectableColor = selectableColor; + Color = color; + } +} diff --git a/Content.Shared/Crayon/SharedCrayonComponent.cs b/Content.Shared/Crayon/SharedCrayonComponent.cs deleted file mode 100644 index a9c21988ea6..00000000000 --- a/Content.Shared/Crayon/SharedCrayonComponent.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; - -namespace Content.Shared.Crayon -{ - - /// - /// Component holding the state of a crayon-like component - /// - [NetworkedComponent, ComponentProtoName("Crayon"), Access(typeof(SharedCrayonSystem))] - public abstract partial class SharedCrayonComponent : Component - { - /// - /// The ID of currently selected decal prototype that will be placed when the crayon is used - /// - public string SelectedState { get; set; } = string.Empty; - - /// - /// Color with which the crayon will draw - /// - [DataField("color")] - public Color Color; - - [Serializable, NetSerializable] - public enum CrayonUiKey : byte - { - Key, - } - } - - /// - /// Used by the client to notify the server about the selected decal ID - /// - [Serializable, NetSerializable] - public sealed class CrayonSelectMessage : BoundUserInterfaceMessage - { - public readonly string State; - public CrayonSelectMessage(string selected) - { - State = selected; - } - } - - /// - /// Sets the color of the crayon, used by Rainbow Crayon - /// - [Serializable, NetSerializable] - public sealed class CrayonColorMessage : BoundUserInterfaceMessage - { - public readonly Color Color; - public CrayonColorMessage(Color color) - { - Color = color; - } - } - - /// - /// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn. - /// Allows the client UI to advance forward in the client-only ephemeral queue, - /// preventing the crayon from becoming a magic text storage device. - /// - [Serializable, NetSerializable] - public sealed class CrayonUsedMessage : BoundUserInterfaceMessage - { - public readonly string DrawnDecal; - - public CrayonUsedMessage(string drawn) - { - DrawnDecal = drawn; - } - } - - /// - /// Component state, describes how many charges are left in the crayon in the near-hand UI - /// - [Serializable, NetSerializable] - public sealed class CrayonComponentState : ComponentState - { - public readonly Color Color; - public readonly string State; - public readonly int Charges; - public readonly int Capacity; - - public CrayonComponentState(Color color, string state, int charges, int capacity) - { - Color = color; - State = state; - Charges = charges; - Capacity = capacity; - } - } - - /// - /// The state of the crayon UI as sent by the server - /// - [Serializable, NetSerializable] - public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState - { - public string Selected; - /// - /// Whether or not the color can be selected - /// - public bool SelectableColor; - public Color Color; - - public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color) - { - Selected = selected; - SelectableColor = selectableColor; - Color = color; - } - } -} diff --git a/Content.Shared/Crayon/SharedCrayonSystem.cs b/Content.Shared/Crayon/SharedCrayonSystem.cs index 03113f12404..42da01904aa 100644 --- a/Content.Shared/Crayon/SharedCrayonSystem.cs +++ b/Content.Shared/Crayon/SharedCrayonSystem.cs @@ -1,4 +1,3 @@ namespace Content.Shared.Crayon; -[Virtual] public abstract class SharedCrayonSystem : EntitySystem { } diff --git a/Content.Shared/CrewManifest/SharedCrewManifestSystem.cs b/Content.Shared/CrewManifest/SharedCrewManifestSystem.cs index a9279cc7f1f..72c6a7bde84 100644 --- a/Content.Shared/CrewManifest/SharedCrewManifestSystem.cs +++ b/Content.Shared/CrewManifest/SharedCrewManifestSystem.cs @@ -1,5 +1,4 @@ using Content.Shared.Eui; -using NetSerializer; using Robust.Shared.Serialization; namespace Content.Shared.CrewManifest; diff --git a/Content.Shared/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs b/Content.Shared/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs index 3a35b807c01..7af7322b71b 100644 --- a/Content.Shared/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs +++ b/Content.Shared/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs @@ -5,6 +5,7 @@ using Content.Shared.StationRecords; using Robust.Shared.Prototypes; using Content.Shared.Security; +using Robust.Shared.GameStates; namespace Content.Shared.CriminalRecords.Components; @@ -12,7 +13,7 @@ namespace Content.Shared.CriminalRecords.Components; /// A component for Criminal Record Console storing an active station record key and a currently applied filter /// [RegisterComponent] -[Access(typeof(SharedCriminalRecordsConsoleSystem))] +[Access(typeof(SharedCriminalRecordsConsoleSystem)), NetworkedComponent] // imperial space: add NetworkedComponent public sealed partial class CriminalRecordsConsoleComponent : Component { /// diff --git a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs index d665d32f1ed..3e2e1035015 100644 --- a/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs +++ b/Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs @@ -45,6 +45,8 @@ public void SetCriminalIcon(string name, SecurityStatus status, EntityUid charac SecurityStatus.Detained => "SecurityIconIncarcerated", SecurityStatus.Discharged => "SecurityIconDischarged", SecurityStatus.Suspected => "SecurityIconSuspected", + SecurityStatus.Hostile => "SecurityIconHostile", + SecurityStatus.Eliminated => "SecurityIconEliminated", _ => record.StatusIcon }; diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs index a7eba34d8ce..046dd504c07 100644 --- a/Content.Shared/Cuffs/Components/CuffableComponent.cs +++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs @@ -24,12 +24,6 @@ public sealed partial class CuffableComponent : Component [ViewVariables] public int CuffedHandCount => Container.ContainedEntities.Count * 2; - /// - /// The last pair of cuffs that was added to this entity. - /// - [ViewVariables] - public EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; - /// /// Container of various handcuffs currently applied to the entity. /// diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index ff4201acaf0..fc36142c9de 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Components; @@ -34,6 +35,7 @@ using Robust.Shared.Player; using Robust.Shared.Serialization; using Robust.Shared.Utility; +using Content.Shared.Zombies; // Imperial Space edit using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent; namespace Content.Shared.Cuffs @@ -260,7 +262,7 @@ private void OnRemoveCuffsAlert(Entity ent, ref RemoveCuffsAl { if (args.Handled) return; - TryUncuff(ent, ent, cuffable: ent.Comp); + TryUncuff((ent, ent.Comp), ent); args.Handled = true; } @@ -278,7 +280,7 @@ private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsE Verb verb = new() { - Act = () => TryUncuff(uid, args.User, cuffable: component), + Act = () => TryUncuff((uid, component), args.User), DoContactInteraction = true, Text = Loc.GetString("uncuff-verb-get-data-text") }; @@ -317,6 +319,14 @@ private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, Aft return; } + // Imperial Space edit start + if (HasComp(args.User)) + { + _popup.PopupClient(Loc.GetString("handcuff-component-zombie-entity-error"), args.User, args.User); + return; + } + // Imperial Space edit end + var result = TryCuffing(args.User, target, uid, component); args.Handled = result; } @@ -326,6 +336,14 @@ private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHit if (!args.HitEntities.Any()) return; + // Imperial Space edit start + if (HasComp(args.User)) + { + _popup.PopupClient(Loc.GetString("handcuff-component-zombie-entity-error"), args.User, args.User); + return; + } + // Imperial Space edit end + TryCuffing(args.User, args.HitEntities.First(), uid, component); args.Handled = true; } @@ -585,41 +603,31 @@ public bool IsCuffed(Entity target, bool requireFullyCuffed = return true; } + /// + public void TryUncuff(Entity target, EntityUid user) + { + if (!TryGetLastCuff(target, out var cuff)) + return; + + TryUncuff(target, user, cuff.Value); + } + /// /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them. /// If the uncuffing succeeds, the cuffs will drop on the floor. /// - /// - /// The cuffed entity - /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity. - /// - /// - public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null) + /// The entity we're trying to remove cuffs from. + /// The entity doing the cuffing. + /// The handcuff entity we're attempting to remove. + public void TryUncuff(Entity target, EntityUid user, Entity cuff) { - if (!Resolve(target, ref cuffable)) + if (!Resolve(target, ref target.Comp) || !Resolve(cuff, ref cuff.Comp)) return; - var isOwner = user == target; - - if (cuffsToRemove == null) - { - if (cuffable.Container.ContainedEntities.Count == 0) - { - return; - } - - cuffsToRemove = cuffable.LastAddedCuffs; - } - else - { - if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value)) - { - Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!"); - } - } + var isOwner = user == target.Owner; - if (!Resolve(cuffsToRemove.Value, ref cuff)) - return; + if (!target.Comp.Container.ContainedEntities.Contains(cuff)) + Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!"); var attempt = new UncuffAttemptEvent(user, target); RaiseLocalEvent(user, ref attempt, true); @@ -629,29 +637,28 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove return; } - if (!isOwner && !_interaction.InRangeUnobstructed(user, target)) + if (!isOwner && !_interaction.InRangeUnobstructed(user, target.Owner)) { _popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user); return; } - - var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime); + var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.Comp.BreakoutTime : cuff.Comp.UncuffTime); RaiseLocalEvent(user, ref ev); var uncuffTime = ev.Duration; if (isOwner) { - if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay)) + if (!TryComp(cuff, out UseDelayComponent? useDelay)) return; - if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true)) + if (!_delay.TryResetDelay((cuff, useDelay), true)) { return; } } - var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove) + var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuff) { BreakOnMove = true, BreakOnWeightlessMove = false, @@ -666,7 +673,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove _adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}"); - var popupText = user == target + var popupText = user == target.Owner ? "cuffable-component-start-uncuffing-self-observer" : "cuffable-component-start-uncuffing-observer"; _popup.PopupEntity( @@ -678,7 +685,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true); - if (target == user) + if (isOwner) { _popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user); } @@ -694,7 +701,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove target); } - _audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user); + _audio.PlayPredicted(isOwner ? cuff.Comp.StartBreakoutSound : cuff.Comp.StartUncuffSound, target, user); } public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null) @@ -805,22 +812,69 @@ private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEnt private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args) { // is this a self-equip, or are they being stripped? - if (args.Equipee == uid) + if (args.User == uid) CheckAct(uid, component, args); } private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args) { // is this a self-equip, or are they being stripped? - if (args.Unequipee == uid) + if (args.User == uid) CheckAct(uid, component, args); } #endregion - public IReadOnlyList GetAllCuffs(CuffableComponent component) + /// + /// Tries to get a list of all the handcuffs stored in an entity's . + /// + /// The cuffable entity in question. + /// A list of cuffs if it exists. + /// True if a list of cuffs with cuffs exists. False if no list exists or if it is empty. + public bool TryGetAllCuffs(Entity entity, out IReadOnlyList cuffs) { - return component.Container.ContainedEntities; + cuffs = GetAllCuffs(entity); + + return cuffs.Count > 0; + } + + /// + /// Tries to get a list of all the handcuffs stored in a entity's . + /// + /// The cuffable entity in question. + /// A list of cuffs if it exists, or null if there are no cuffs. + public IReadOnlyList GetAllCuffs(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return []; + + return entity.Comp.Container.ContainedEntities; + } + + /// + /// Tries to get the most recently added pair of handcuffs added to an entity with . + /// + /// The cuffable entity in question. + /// The most recently added cuff. + /// Returns true if a cuff exists and false if one doesn't. + public bool TryGetLastCuff(Entity entity, [NotNullWhen(true)] out EntityUid? cuff) + { + cuff = GetLastCuffOrNull(entity); + + return cuff != null; + } + + /// + /// Tries to get the most recently added pair of handcuffs added to an entity with . + /// + /// The cuffable entity in question. + /// The most recently added cuff or null if none exists. + public EntityUid? GetLastCuffOrNull(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return null; + + return entity.Comp.Container.ContainedEntities.Count == 0 ? null : entity.Comp.Container.ContainedEntities.Last(); } } diff --git a/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs b/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs index 3d4bdd597c3..faf8a1bb579 100644 --- a/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs +++ b/Content.Shared/Damage/Components/ClothingSlowOnDamageModifierComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Damage.Systems; using Robust.Shared.GameStates; namespace Content.Shared.Damage.Components; diff --git a/Content.Shared/Damage/Components/DamageExaminableComponent.cs b/Content.Shared/Damage/Components/DamageExaminableComponent.cs index a9fa833070b..0ac9435bb67 100644 --- a/Content.Shared/Damage/Components/DamageExaminableComponent.cs +++ b/Content.Shared/Damage/Components/DamageExaminableComponent.cs @@ -2,7 +2,8 @@ namespace Content.Shared.Damage.Components; +/// +/// Shows a detailed examine window with this entity's damage stats when examined. +/// [RegisterComponent, NetworkedComponent] -public sealed partial class DamageExaminableComponent : Component -{ -} +public sealed partial class DamageExaminableComponent : Component; diff --git a/Content.Shared/Damage/Components/DamageModifierStatusEffectComponent.cs b/Content.Shared/Damage/Components/DamageModifierStatusEffectComponent.cs new file mode 100644 index 00000000000..51a80a12fea --- /dev/null +++ b/Content.Shared/Damage/Components/DamageModifierStatusEffectComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Damage.Components; + +/// +/// Component used on a status effect entity to modify damage taken. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class DamageModifierStatusEffectComponent : Component +{ + /// + /// The modifier prototype to apply. + /// + [DataField(required: true)] + public DamageModifierSet Modifiers; +} diff --git a/Content.Shared/Damage/Components/DamageOtherOnHitComponent.cs b/Content.Shared/Damage/Components/DamageOtherOnHitComponent.cs new file mode 100644 index 00000000000..7728bcf7eec --- /dev/null +++ b/Content.Shared/Damage/Components/DamageOtherOnHitComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Damage.Systems; + +namespace Content.Shared.Damage.Components; + +/// +/// Makes this entity deal damage when thrown at something. +/// +[RegisterComponent] +[Access(typeof(SharedDamageOtherOnHitSystem))] +public sealed partial class DamageOtherOnHitComponent : Component +{ + /// + /// Whether to ignore damage modifiers. + /// + [DataField] + public bool IgnoreResistances = false; + + /// + /// The damage amount to deal on hit. + /// + [DataField(required: true)] + public DamageSpecifier Damage = default!; + +} diff --git a/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs b/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs deleted file mode 100644 index 99b055a6456..00000000000 --- a/Content.Shared/Damage/Components/DamageProtectionBuffComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Damage.Prototypes; -using Robust.Shared.GameStates; - -namespace Content.Shared.Damage.Components; - -/// -/// Applies the specified DamageModifierSets when the entity takes damage. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class DamageProtectionBuffComponent : Component -{ - /// - /// The damage modifiers for entities with this component. - /// - [DataField] - public Dictionary Modifiers = new(); -} diff --git a/Content.Shared/Damage/Components/DamageableComponent.cs b/Content.Shared/Damage/Components/DamageableComponent.cs index 9e4e26e4226..a78cacf16a3 100644 --- a/Content.Shared/Damage/Components/DamageableComponent.cs +++ b/Content.Shared/Damage/Components/DamageableComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Mobs; using Content.Shared.StatusIcon; @@ -6,105 +7,104 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -namespace Content.Shared.Damage +namespace Content.Shared.Damage.Components; + +/// +/// Component that allows entities to take damage. +/// +/// +/// The supported damage types are specified using a s. DamageContainers +/// may also have resistances to certain damage types, defined via a . +/// +[RegisterComponent] +[NetworkedComponent] +[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)] +public sealed partial class DamageableComponent : Component { /// - /// Component that allows entities to take damage. + /// This specifies what damage types are supported by this component. + /// If null, all damage types will be supported. + /// + [DataField("damageContainer")] + // ReSharper disable once InconsistentNaming - This is wrong but fixing it is potentially annoying for downstreams. + public ProtoId? DamageContainerID; + + /// + /// This will be applied to any damage that is dealt to this container, + /// unless the damage explicitly ignores resistances. /// /// - /// The supported damage types are specified using a s. DamageContainers - /// may also have resistances to certain damage types, defined via a . + /// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here + /// to reduce duplication. /// - [RegisterComponent] - [NetworkedComponent] - [Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)] - public sealed partial class DamageableComponent : Component - { - /// - /// This specifies what damage types are supported by this component. - /// If null, all damage types will be supported. - /// - [DataField("damageContainer")] - public ProtoId? DamageContainerID; - - /// - /// This will be applied to any damage that is dealt to this container, - /// unless the damage explicitly ignores resistances. - /// - /// - /// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here - /// to reduce duplication. - /// - [DataField("damageModifierSet")] - public ProtoId? DamageModifierSetId; + [DataField("damageModifierSet")] + public ProtoId? DamageModifierSetId; - /// - /// All the damage information is stored in this . - /// - /// - /// If this data-field is specified, this allows damageable components to be initialized with non-zero damage. - /// - [DataField(readOnly: true)] //todo remove this readonly when implementing writing to damagespecifier - public DamageSpecifier Damage = new(); - - /// - /// Damage, indexed by ID keys. - /// - /// - /// Groups which have no members that are supported by this component will not be present in this - /// dictionary. - /// - [ViewVariables] public Dictionary DamagePerGroup = new(); + /// + /// All the damage information is stored in this . + /// + /// + /// If this data-field is specified, this allows damageable components to be initialized with non-zero damage. + /// + [DataField] + [Access(typeof(DamageableSystem), Other = AccessPermissions.None)] + public DamageSpecifier Damage = new(); - /// - /// The sum of all damages in the DamageableComponent. - /// - [ViewVariables] - public FixedPoint2 TotalDamage; + /// + /// Damage, indexed by ID keys. + /// + /// + /// Groups which have no members that are supported by this component will not be present in this + /// dictionary. + /// + [ViewVariables] + [Access(typeof(DamageableSystem), Other = AccessPermissions.None)] + public Dictionary, FixedPoint2> DamagePerGroup = new(); - [DataField("radiationDamageTypes")] - public List> RadiationDamageTypeIDs = new() { "Radiation" }; + /// + /// The sum of all damages in the DamageableComponent. + /// + [ViewVariables] + [Access(typeof(DamageableSystem), Other = AccessPermissions.None)] + public FixedPoint2 TotalDamage; - /// - /// Group types that affect the pain overlay. - /// - /// TODO: Add support for adding damage types specifically rather than whole damage groups - [DataField] - public List> PainDamageGroups = new() { "Brute", "Burn" }; + [DataField("radiationDamageTypes")] + // ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox violation. + public List> RadiationDamageTypeIDs = new() { "Radiation" }; - [DataField] - public Dictionary> HealthIcons = new() - { - { MobState.Alive, "HealthIconFine" }, - { MobState.Critical, "HealthIconCritical" }, - { MobState.Dead, "HealthIconDead" }, - }; + /// + /// Group types that affect the pain overlay. + /// + /// TODO: Add support for adding damage types specifically rather than whole damage groups + [DataField] + // ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox volation. + public List> PainDamageGroups = new() { "Brute", "Burn" }; - [DataField] - public ProtoId RottingIcon = "HealthIconRotting"; + [DataField] + public Dictionary> HealthIcons = new() + { + { MobState.Alive, "HealthIconFine" }, + { MobState.Critical, "HealthIconCritical" }, + { MobState.Dead, "HealthIconDead" }, + }; - [DataField] - public FixedPoint2? HealthBarThreshold; - } + [DataField] + public ProtoId RottingIcon = "HealthIconRotting"; - [Serializable, NetSerializable] - public sealed class DamageableComponentState : ComponentState - { - public readonly Dictionary DamageDict; - public readonly string? DamageContainerId; - public readonly string? ModifierSetId; - public readonly FixedPoint2? HealthBarThreshold; + [DataField] + public FixedPoint2? HealthBarThreshold; +} - public DamageableComponentState( - Dictionary damageDict, - string? damageContainerId, - string? modifierSetId, - FixedPoint2? healthBarThreshold) - { - DamageDict = damageDict; - DamageContainerId = damageContainerId; - ModifierSetId = modifierSetId; - HealthBarThreshold = healthBarThreshold; - } - } +[Serializable, NetSerializable] +public sealed class DamageableComponentState( + DamageSpecifier damage, + ProtoId? damageContainerId, + ProtoId? modifierSetId, + FixedPoint2? healthBarThreshold) + : ComponentState +{ + public readonly DamageSpecifier Damage = damage; + public readonly ProtoId? DamageContainerId = damageContainerId; + public readonly ProtoId? ModifierSetId = modifierSetId; + public readonly FixedPoint2? HealthBarThreshold = healthBarThreshold; } diff --git a/Content.Shared/Damage/Components/GodmodeComponent.cs b/Content.Shared/Damage/Components/GodmodeComponent.cs index 66f0daff39b..f49fd9bb935 100644 --- a/Content.Shared/Damage/Components/GodmodeComponent.cs +++ b/Content.Shared/Damage/Components/GodmodeComponent.cs @@ -8,7 +8,4 @@ public sealed partial class GodmodeComponent : Component { [DataField("wasMovedByPressure")] public bool WasMovedByPressure; - - [DataField("oldDamage")] - public DamageSpecifier? OldDamage = null; } diff --git a/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs b/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs index e933eb1a790..101a494bc93 100644 --- a/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs +++ b/Content.Shared/Damage/Components/IgnoreSlowOnDamageComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Damage.Systems; using Robust.Shared.GameStates; namespace Content.Shared.Damage.Components; diff --git a/Content.Shared/Damage/Components/PassiveDamageComponent.cs b/Content.Shared/Damage/Components/PassiveDamageComponent.cs index 269960adac4..679ce246620 100644 --- a/Content.Shared/Damage/Components/PassiveDamageComponent.cs +++ b/Content.Shared/Damage/Components/PassiveDamageComponent.cs @@ -29,12 +29,6 @@ public sealed partial class PassiveDamageComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public float Interval = 1f; - /// - /// The maximum HP the damage will be given to. If 0, disabled. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 DamageCap = 0; - [DataField("nextDamage", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] public TimeSpan NextDamage = TimeSpan.Zero; } diff --git a/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs b/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs index 5bd8292daae..3c94dcbc38e 100644 --- a/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs +++ b/Content.Shared/Damage/Components/RequireProjectileTargetComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Damage.Systems; using Robust.Shared.GameStates; namespace Content.Shared.Damage.Components; diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 6a91234e074..c1bf53012b6 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -39,9 +39,15 @@ public sealed partial class StaminaComponent : Component public float StaminaDamage; /// - /// How much stamina damage is required to enter stam crit. + /// The base stamina the entity requires to enter stam crit. Should rarely if ever be modified outside of yaml. /// - [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] + [DataField, AutoNetworkedField] + public float BaseCritThreshold = 100f; + + /// + /// Modified crit threshold for when an entity should enter stamcrit. + /// + [ViewVariables, AutoNetworkedField] public float CritThreshold = 100f; /// @@ -85,7 +91,7 @@ public sealed partial class StaminaComponent : Component public SoundSpecifier ForceStandSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); /// - /// Thresholds that determine an entity's slowdown as a function of stamina damage. + /// Thresholds that determine an entity's slowdown as a function of stamina damage, in percentages. /// [DataField] public Dictionary StunModifierThresholds = new() { {0, 1f }, { 60, 0.95f }, { 80, 0.85f } }; // imperial medieval balance changes diff --git a/Content.Shared/Damage/Components/StaminaModifierComponent.cs b/Content.Shared/Damage/Components/StaminaModifierComponent.cs deleted file mode 100644 index e492ea04ffe..00000000000 --- a/Content.Shared/Damage/Components/StaminaModifierComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Damage.Systems; -using Robust.Shared.GameStates; - -namespace Content.Shared.Damage.Components; - -/// -/// Multiplies the entity's by the . -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStaminaSystem))] -public sealed partial class StaminaModifierComponent : Component -{ - /// - /// What to multiply max stamina by. - /// When added this scales max stamina, but not stamina damags to give you an extra boost of survability. - /// If you have too much damage when the modifier is removed, you suffer "withdrawl" and instantly stamcrit. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("modifier"), AutoNetworkedField] - public float Modifier = 2f; -} diff --git a/Content.Shared/Damage/Components/StaminaModifierStatusEffectComponent.cs b/Content.Shared/Damage/Components/StaminaModifierStatusEffectComponent.cs new file mode 100644 index 00000000000..4ca888ffbe5 --- /dev/null +++ b/Content.Shared/Damage/Components/StaminaModifierStatusEffectComponent.cs @@ -0,0 +1,19 @@ +using Content.Shared.Damage.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Damage.Components; + +/// +/// Multiplies the entity's by the . +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStaminaSystem))] +public sealed partial class StaminaModifierStatusEffectComponent : Component +{ + /// + /// What to multiply max stamina by. + /// When added this scales max stamina, but not stamina damags to give you an extra boost of survability. + /// If you have too much damage when the modifier is removed, you suffer "withdrawl" and instantly stamcrit. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("modifier"), AutoNetworkedField] + public float Modifier = 2f; +} diff --git a/Content.Shared/Damage/DamageModifierSet.cs b/Content.Shared/Damage/DamageModifierSet.cs index eaa6e93da4c..d8b00ad1f2e 100644 --- a/Content.Shared/Damage/DamageModifierSet.cs +++ b/Content.Shared/Damage/DamageModifierSet.cs @@ -1,3 +1,4 @@ +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs index 00bd416e1f7..b0dc63e2e31 100644 --- a/Content.Shared/Damage/DamageSpecifier.cs +++ b/Content.Shared/Damage/DamageSpecifier.cs @@ -1,12 +1,12 @@ using System.Linq; using System.Text.Json.Serialization; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; -using JetBrains.Annotations; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Utility; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; namespace Content.Shared.Damage { @@ -18,26 +18,13 @@ namespace Content.Shared.Damage /// functions to apply resistance sets and supports basic math operations to modify this dictionary. /// [DataDefinition, Serializable, NetSerializable] - public sealed partial class DamageSpecifier : IEquatable + public sealed partial class DamageSpecifier : IEquatable, IRobustCloneable { - // These exist solely so the wiki works. Please do not touch them or use them. - [JsonPropertyName("types")] - [DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] - [UsedImplicitly] - private Dictionary? _damageTypeDictionary; - - [JsonPropertyName("groups")] - [DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] - [UsedImplicitly] - private Dictionary? _damageGroupDictionary; - /// /// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this. /// - [JsonIgnore] - [ViewVariables(VVAccess.ReadWrite)] - [IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)] - public Dictionary DamageDict { get; set; } = new(); + [DataField("types")] + public Dictionary, FixedPoint2> DamageDict { get; set; } = new(); /// /// Returns a sum of the damage values. @@ -78,6 +65,11 @@ public bool AnyPositive() [JsonIgnore] public bool Empty => DamageDict.Count == 0; + public DamageSpecifier Clone() + { + return new DamageSpecifier(this); + } + public override string ToString() { return "DamageSpecifier(" + string.Join("; ", DamageDict.Select(x => x.Key + ":" + x.Value)) + ")"; @@ -328,15 +320,15 @@ public bool TryGetDamageInGroup(DamageGroupPrototype group, out FixedPoint2 tota /// total of each group. If no members of a group are present in this , the /// group is not included in the resulting dictionary. /// - public Dictionary GetDamagePerGroup(IPrototypeManager protoManager) + public Dictionary, FixedPoint2> GetDamagePerGroup(IPrototypeManager protoManager) { - var dict = new Dictionary(); + var dict = new Dictionary, FixedPoint2>(); GetDamagePerGroup(protoManager, dict); return dict; } /// - public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary dict) + public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary, FixedPoint2> dict) { dict.Clear(); foreach (var group in protoManager.EnumeratePrototypes()) diff --git a/Content.Shared/Damage/DamageSpecifierDictionarySerializer.cs b/Content.Shared/Damage/DamageSpecifierDictionarySerializer.cs deleted file mode 100644 index 92f56571a62..00000000000 --- a/Content.Shared/Damage/DamageSpecifierDictionarySerializer.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Validation; -using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -using Robust.Shared.Serialization.TypeSerializers.Interfaces; - -namespace Content.Shared.Damage; - -//todo writing -public sealed class DamageSpecifierDictionarySerializer : ITypeReader, MappingDataNode> -{ - private ITypeValidator, MappingDataNode> _damageTypeSerializer = new PrototypeIdDictionarySerializer(); - private ITypeValidator, MappingDataNode> _damageGroupSerializer = new PrototypeIdDictionarySerializer(); - - public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, - IDependencyCollection dependencies, ISerializationContext? context = null) - { - var vals = new Dictionary(); - if (node.TryGet("types", out var typesNode)) - { - vals.Add(new ValidatedValueNode(new ValueDataNode("types")), _damageTypeSerializer.Validate(serializationManager, typesNode, dependencies, context)); - } - - if (node.TryGet("groups", out var groupsNode)) - { - vals.Add(new ValidatedValueNode(new ValueDataNode("groups")), _damageGroupSerializer.Validate(serializationManager, groupsNode, dependencies, context)); - } - - return new ValidatedMappingNode(vals); - } - - public Dictionary Read(ISerializationManager serializationManager, MappingDataNode node, IDependencyCollection dependencies, - SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate>? instanceProvider = null) - { - var dict = instanceProvider != null ? instanceProvider() : new(); - // Add all the damage types by just copying the type dictionary (if it is not null). - if (node.TryGet("types", out var typesNode)) - { - serializationManager.Read(typesNode, instanceProvider: () => dict, notNullableOverride: true); - } - - if (!node.TryGet("groups", out var groupsNode)) - return dict; - - // Then resolve damage groups and add them - var prototypeManager = dependencies.Resolve(); - foreach (var entry in serializationManager.Read>(groupsNode, notNullableOverride: true)) - { - if (!prototypeManager.TryIndex(entry.Key, out var group)) - { - // This can happen if deserialized before prototypes are loaded. - // i made this a warning bc it was failing tests -paul - dependencies.Resolve().RootSawmill.Error($"Unknown damage group given to DamageSpecifier: {entry.Key}"); - continue; - } - - // Simply distribute evenly (except for rounding). - // We do this by reducing remaining the # of types and damage every loop. - var remainingTypes = group.DamageTypes.Count; - var remainingDamage = entry.Value; - foreach (var damageType in group.DamageTypes) - { - var damage = remainingDamage / FixedPoint2.New(remainingTypes); - if (!dict.TryAdd(damageType, damage)) - { - // Key already exists, add values - dict[damageType] += damage; - } - remainingDamage -= damage; - remainingTypes -= 1; - } - } - - return dict; - } -} diff --git a/Content.Shared/Damage/DamageVisualizerKeys.cs b/Content.Shared/Damage/DamageVisualizerKeys.cs index ba70ab3b91b..9719b96fe7b 100644 --- a/Content.Shared/Damage/DamageVisualizerKeys.cs +++ b/Content.Shared/Damage/DamageVisualizerKeys.cs @@ -1,3 +1,5 @@ +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Damage @@ -14,16 +16,16 @@ public enum DamageVisualizerKeys [Serializable, NetSerializable] public sealed class DamageVisualizerGroupData : ICloneable { - public List GroupList; + public List> GroupList; - public DamageVisualizerGroupData(List groupList) + public DamageVisualizerGroupData(List> groupList) { GroupList = groupList; } public object Clone() { - return new DamageVisualizerGroupData(new List(GroupList)); + return new DamageVisualizerGroupData(new List>(GroupList)); } } } diff --git a/Content.Shared/Damage/Events/DamageExamineEvent.cs b/Content.Shared/Damage/Events/DamageExamineEvent.cs index d3e6175f496..33949cc0525 100644 --- a/Content.Shared/Damage/Events/DamageExamineEvent.cs +++ b/Content.Shared/Damage/Events/DamageExamineEvent.cs @@ -1,6 +1,10 @@ +using Content.Shared.Damage.Components; using Robust.Shared.Utility; namespace Content.Shared.Damage.Events; +/// +/// Raised on an entity with when examined to get the damage values displayed in the examine window. +/// [ByRefEvent] public readonly record struct DamageExamineEvent(FormattedMessage Message, EntityUid User); diff --git a/Content.Shared/Damage/Events/RefreshStaminaCritThresholdEvent.cs b/Content.Shared/Damage/Events/RefreshStaminaCritThresholdEvent.cs new file mode 100644 index 00000000000..4ed86445e18 --- /dev/null +++ b/Content.Shared/Damage/Events/RefreshStaminaCritThresholdEvent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Damage.Components; + +namespace Content.Shared.Damage.Events; + +/// +/// Raised whenever the needs to be refreshed. +/// +[ByRefEvent] +public record struct RefreshStaminaCritThresholdEvent +{ + public float ThresholdValue = 100f; + public float Modifier = 1f; + + public RefreshStaminaCritThresholdEvent(float thresholdValue) + { + ThresholdValue = thresholdValue; + } +} diff --git a/Content.Shared/Damage/Events/StaminaDamageOnHitAttemptEvent.cs b/Content.Shared/Damage/Events/StaminaDamageOnHitAttemptEvent.cs index c1bfff30d77..33f4c7333f0 100644 --- a/Content.Shared/Damage/Events/StaminaDamageOnHitAttemptEvent.cs +++ b/Content.Shared/Damage/Events/StaminaDamageOnHitAttemptEvent.cs @@ -4,4 +4,4 @@ namespace Content.Shared.Damage.Events; /// Attempting to apply stamina damage on entity. /// [ByRefEvent] -public record struct BeforeStaminaDamageOnTriggerEvent(bool Cancelled); +public record struct StaminaDamageOnHitAttemptEvent(bool Cancelled); diff --git a/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs b/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs index 00322f0884a..1643a5469db 100644 --- a/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs +++ b/Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs @@ -1,5 +1,5 @@ +using Content.Shared.Damage.Components; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; namespace Content.Shared.Damage.Prototypes { diff --git a/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs b/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs index a33064f934e..c19f14ea19a 100644 --- a/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs +++ b/Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs @@ -1,5 +1,5 @@ +using Content.Shared.Damage.Components; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; namespace Content.Shared.Damage.Prototypes { @@ -11,6 +11,7 @@ namespace Content.Shared.Damage.Prototypes /// to change/get/set damage in a . /// [Prototype(2)] + [Obsolete("Do not rely on DamageGroupPrototype for anything besides grouping logically similar damage in UIs")] public sealed partial class DamageGroupPrototype : IPrototype { [IdDataField] public string ID { get; private set; } = default!; diff --git a/Content.Shared/Damage/Systems/DamageContactsSystem.cs b/Content.Shared/Damage/Systems/DamageContactsSystem.cs index 9a53d83eb26..820fc3c90bb 100644 --- a/Content.Shared/Damage/Systems/DamageContactsSystem.cs +++ b/Content.Shared/Damage/Systems/DamageContactsSystem.cs @@ -14,6 +14,8 @@ public sealed class DamageContactsSystem : EntitySystem [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly EntityQuery _damageQuery = default!; + public override void Initialize() { base.Initialize(); @@ -45,13 +47,12 @@ private void OnEntityExit(EntityUid uid, DamageContactsComponent component, ref if (!TryComp(otherUid, out var body)) return; - var damageQuery = GetEntityQuery(); foreach (var ent in _physics.GetContactingEntities(otherUid, body)) { if (ent == uid) continue; - if (damageQuery.HasComponent(ent)) + if (_damageQuery.HasComponent(ent)) return; } diff --git a/Content.Shared/Damage/Systems/DamageModifierStatusEffect.cs b/Content.Shared/Damage/Systems/DamageModifierStatusEffect.cs new file mode 100644 index 00000000000..a47b239330a --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageModifierStatusEffect.cs @@ -0,0 +1,19 @@ +using Content.Shared.Damage.Components; +using Content.Shared.StatusEffectNew; + +namespace Content.Shared.Damage.Systems; + +public sealed class DamageModifierStatusEffectSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnDamageModifyStatus); + } + + private void OnDamageModifyStatus(Entity status, ref StatusEffectRelayedEvent args) + { + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, status.Comp.Modifiers); + } +} diff --git a/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs b/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs index 29de43fba4a..d628f40b764 100644 --- a/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs +++ b/Content.Shared/Damage/Systems/DamageOnAttackedSystem.cs @@ -73,9 +73,9 @@ private void OnAttacked(Entity entity, ref AttackedEv } } - totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity); + totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity); - if (totalDamage != null && totalDamage.AnyPositive()) + if (totalDamage.AnyPositive()) { _adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} injured themselves by attacking {ToPrettyString(entity):target} and received {totalDamage.GetTotal():damage} damage"); _audioSystem.PlayPredicted(entity.Comp.InteractSound, entity, args.User); diff --git a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs index bd3b6979f70..401c94f33ec 100644 --- a/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs +++ b/Content.Shared/Damage/Systems/DamageOnInteractSystem.cs @@ -65,7 +65,7 @@ private void OnHandInteract(Entity entity, ref Intera // or checking the entity for the comp itself if the inventory didn't work if (protectiveEntity.Comp == null && TryComp(args.User, out var protectiveComp)) protectiveEntity = (args.User, protectiveComp); - + // if protectiveComp isn't null after all that, it means the user has protection, // so let's calculate how much they resist @@ -75,9 +75,9 @@ private void OnHandInteract(Entity entity, ref Intera } } - totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target); + totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, origin: args.Target); - if (totalDamage != null && totalDamage.AnyPositive()) + if (totalDamage.AnyPositive()) { // Record this interaction and determine when a user is allowed to interact with this entity again entity.Comp.LastInteraction = _gameTiming.CurTime; diff --git a/Content.Shared/Damage/Systems/DamagePopupSystem.cs b/Content.Shared/Damage/Systems/DamagePopupSystem.cs index fa29b5a0187..d6a8e0beba9 100644 --- a/Content.Shared/Damage/Systems/DamagePopupSystem.cs +++ b/Content.Shared/Damage/Systems/DamagePopupSystem.cs @@ -7,6 +7,7 @@ namespace Content.Shared.Damage.Systems; public sealed class DamagePopupSystem : EntitySystem { [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; public override void Initialize() { @@ -19,7 +20,7 @@ private void OnDamageChange(Entity ent, ref DamageChangedE { if (args.DamageDelta != null) { - var damageTotal = args.Damageable.TotalDamage; + var damageTotal = _damageable.GetTotalDamage((ent, args.Damageable)); var damageDelta = args.DamageDelta.GetTotal(); var msg = ent.Comp.Type switch diff --git a/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs b/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs deleted file mode 100644 index bbb7bfda323..00000000000 --- a/Content.Shared/Damage/Systems/DamageProtectionBuffSystem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Damage.Components; - -namespace Content.Shared.Damage.Systems; - -public sealed class DamageProtectionBuffSystem : EntitySystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnDamageModify); - } - - private void OnDamageModify(EntityUid uid, DamageProtectionBuffComponent component, DamageModifyEvent args) - { - foreach (var modifier in component.Modifiers.Values) - args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifier); - } -} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.API.cs b/Content.Shared/Damage/Systems/DamageableSystem.API.cs new file mode 100644 index 00000000000..2975f227623 --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageableSystem.API.cs @@ -0,0 +1,479 @@ +using System.Linq; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Damage.Systems; + +public sealed partial class DamageableSystem +{ + /// If the damage container can take the given damage type + private bool SupportsType(ProtoId? container, ProtoId type) + { + if (container is null) + return true; + + return _supportedTypesByContainer[container.Value].Contains(type); + } + + /// + /// Directly sets the damage in a damageable component. + /// This method keeps the damage types supported by the DamageContainerPrototype in the component. + /// If a type is given in , but not supported then it will not be set. + /// If a type is supported but not given in then it will be set to 0. + /// + /// + /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed + /// event is raised. + /// + public void SetDamage(Entity ent, DamageSpecifier damage) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return; + + foreach (var type in ent.Comp.Damage.DamageDict.Keys) + { + if (!damage.DamageDict.ContainsKey(type)) + ent.Comp.Damage.DamageDict.Remove(type); + } + + foreach (var (type, amount) in damage.DamageDict) + { + if (SupportsType(ent.Comp.DamageContainerID, type)) + ent.Comp.Damage.DamageDict[type] = amount; + } + + OnEntityDamageChanged((ent, ent.Comp)); + } + + /// + /// Applies damage specified via a . + /// + /// + /// is effectively just a dictionary of damage types and damage values. This + /// function just applies the container's resistances (unless otherwise specified) and then changes the + /// stored damage data. Division of group damage into types is managed by . + /// + /// + /// If the attempt was successful or not. + /// + public bool TryChangeDamage( + Entity ent, + DamageSpecifier damage, + bool ignoreResistances = false, + bool interruptsDoAfters = true, + EntityUid? origin = null, + bool ignoreGlobalModifiers = false + ) + { + //! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty. + // If you deal 0.0 of some damage type, Empty will be false! + return TryChangeDamage(ent, damage, out _, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers); + } + + /// + /// Applies damage specified via a . + /// + /// + /// is effectively just a dictionary of damage types and damage values. This + /// function just applies the container's resistances (unless otherwise specified) and then changes the + /// stored damage data. Division of group damage into types is managed by . + /// + /// + /// If the attempt was successful or not. + /// + public bool TryChangeDamage( + Entity ent, + DamageSpecifier damage, + out DamageSpecifier newDamage, + bool ignoreResistances = false, + bool interruptsDoAfters = true, + EntityUid? origin = null, + bool ignoreGlobalModifiers = false + ) + { + //! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty. + // If you deal 0.0 of some damage type, Empty will be false! + newDamage = ChangeDamage(ent, damage, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers); + return !newDamage.Empty; + } + + /// + /// Applies damage specified via a . + /// + /// + /// is effectively just a dictionary of damage types and damage values. This + /// function just applies the container's resistances (unless otherwise specified) and then changes the + /// stored damage data. Division of group damage into types is managed by . + /// + /// + /// The actual amount of damage taken, as a DamageSpecifier. + /// + public DamageSpecifier ChangeDamage( + Entity ent, + DamageSpecifier damage, + bool ignoreResistances = false, + bool interruptsDoAfters = true, + EntityUid? origin = null, + bool ignoreGlobalModifiers = false + ) + { + var damageDone = new DamageSpecifier(); + + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return damageDone; + + if (damage.Empty) + return damageDone; + + var before = new BeforeDamageChangedEvent(damage, origin); + RaiseLocalEvent(ent, ref before); + + if (before.Cancelled) + return damageDone; + + // Apply resistances + if (!ignoreResistances) + { + if ( + ent.Comp.DamageModifierSetId != null && + _prototypeManager.Resolve(ent.Comp.DamageModifierSetId, out var modifierSet) + ) + damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); + + // TODO DAMAGE + // byref struct event. + var ev = new DamageModifyEvent(damage, origin); + RaiseLocalEvent(ent, ev); + damage = ev.Damage; + + if (damage.Empty) + return damageDone; + } + + if (!ignoreGlobalModifiers) + damage = ApplyUniversalAllModifiers(damage); + + + damageDone.DamageDict.EnsureCapacity(damage.DamageDict.Count); + + var dict = ent.Comp.Damage.DamageDict; + foreach (var (type, value) in damage.DamageDict) + { + if (!SupportsType(ent.Comp.DamageContainerID, type)) + continue; + + var oldValue = dict.GetValueOrDefault(type); + var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value); + if (newValue == oldValue) + continue; + + dict[type] = newValue; + damageDone.DamageDict[type] = newValue - oldValue; + } + + if (!damageDone.Empty) + OnEntityDamageChanged((ent, ent.Comp), damageDone, interruptsDoAfters, origin); + + return damageDone; + } + + /// + /// Will reduce the damage on the entity exactly by as close as equally distributed among all damage types the entity has. + /// If one of the damage types of the entity is too low. it will heal that completly and distribute the excess healing among the other damage types. + /// If the is larger than the total damage of the entity then it just clears all damage. + /// + /// entity to be healed + /// how much to heal. value has to be negative to heal + /// from which group to heal. if null, heal from all groups + /// who did the healing + public DamageSpecifier HealEvenly( + Entity ent, + FixedPoint2 amount, + ProtoId? group = null, + EntityUid? origin = null) + { + var damageChange = new DamageSpecifier(); + + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0) + return damageChange; + + // Get our total damage, or heal if we're below a certain amount. + if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group)) + return ChangeDamage(ent, -damage, true, false, origin); + + // make sure damageChange has the same damage types as damage + damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count); + foreach (var type in damage.DamageDict.Keys) + { + damageChange.DamageDict.Add(type, FixedPoint2.Zero); + } + + var remaining = -amount; + var keys = damage.DamageDict.Keys.ToList(); + + while (remaining > 0) + { + var count = keys.Count; + // We do this to ensure that we always round up when dividing to avoid excess loops. + // We already have logic to prevent healing more than we have. + var maxHeal = count == 1 ? remaining : (remaining + FixedPoint2.Epsilon * (count - 1)) / count; + + // Iterate backwards since we're removing items. + for (var i = count - 1; i >= 0; i--) + { + var type = keys[i]; + // This is the amount we're trying to heal, capped by maxHeal + var heal = damage.DamageDict[type] + damageChange.DamageDict[type]; + + // Don't go above max, if we don't go above max + if (heal > maxHeal) + heal = maxHeal; + // If we're not above max, we will heal it fully and don't need to enumerate anymore! + else + keys.RemoveAt(i); + + if (heal >= remaining) + { + // Don't remove more than we can remove. Prevents us from healing more than we'd expect... + damageChange.DamageDict[type] -= remaining; + remaining = FixedPoint2.Zero; + break; + } + + remaining -= heal; + damageChange.DamageDict[type] -= heal; + } + } + + return ChangeDamage(ent, damageChange, true, false, origin); + } + + /// + /// Will reduce the damage on the entity exactly by distributed by weight among all damage types the entity has. + /// (the weight is how much damage of the type there is) + /// If the is larger than the total damage of the entity then it just clears all damage. + /// + /// entity to be healed + /// how much to heal. value has to be negative to heal + /// from which group to heal. if null, heal from all groups + /// who did the healing + public DamageSpecifier HealDistributed( + Entity ent, + FixedPoint2 amount, + ProtoId? group = null, + EntityUid? origin = null) + { + var damageChange = new DamageSpecifier(); + + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0) + return damageChange; + + // Get our total damage, or heal if we're below a certain amount. + if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group)) + return ChangeDamage(ent, -damage, true, false, origin); + + // make sure damageChange has the same damage types as damageEntity + damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count); + var total = damage.GetTotal(); + + // heal weighted by the damage of that type + foreach (var (type, value) in damage.DamageDict) + { + damageChange.DamageDict.Add(type, value / total * amount); + } + + return ChangeDamage(ent, damageChange, true, false, origin); + } + + /// + /// Tries to get damage from an entity with an optional group specifier. + /// + /// Entity we're checking the damage on + /// Amount we want the damage to be greater than ideally + /// Damage specifier we're returning with + /// An optional group, note that if it fails to index it will just use all damage. + /// True if the total damage is greater than the specified amount + public bool TryGetDamageGreaterThan(Entity ent, + FixedPoint2 amount, + out DamageSpecifier damage, + ProtoId? group = null) + { + // get the damage should be healed (either all or only from one group) + damage = group == null ? GetPositiveDamage(ent) : GetPositiveDamage(ent, group.Value); + + // If trying to heal more than the total damage of damageEntity just heal everything + return damage.GetTotal() > amount; + } + + /// + /// Returns a with all positive damage of the entity from the group specified + /// + /// entity with damage + /// group of damage to get values from + /// + public DamageSpecifier GetPositiveDamage(Entity ent, ProtoId group) + { + // No damage if no group exists... + if (!_prototypeManager.Resolve(group, out var groupProto)) + return new DamageSpecifier(); + + var damage = new DamageSpecifier(); + damage.DamageDict.EnsureCapacity(groupProto.DamageTypes.Count); + + foreach (var damageId in groupProto.DamageTypes) + { + if (!ent.Comp.Damage.DamageDict.TryGetValue(damageId, out var value)) + continue; + if (value > FixedPoint2.Zero) + damage.DamageDict.Add(damageId, value); + } + + return damage; + } + + /// + /// Returns a with all positive damage of the entity + /// + /// entity with damage + /// + public DamageSpecifier GetPositiveDamage(Entity ent) + { + var damage = new DamageSpecifier(); + damage.DamageDict.EnsureCapacity(ent.Comp.Damage.DamageDict.Count); + + foreach (var (damageId, value) in ent.Comp.Damage.DamageDict) + { + if (value > FixedPoint2.Zero) + damage.DamageDict.Add(damageId, value); + } + + return damage; + } + + /// + /// Applies the two universal "All" modifiers, if set. + /// Individual damage source modifiers are set in their respective code. + /// + /// The damage to be changed. + public DamageSpecifier ApplyUniversalAllModifiers(DamageSpecifier damage) + { + // Checks for changes first since they're unlikely in normal play. + if ( + MathHelper.CloseToPercent(UniversalAllDamageModifier, 1f) && + MathHelper.CloseToPercent(UniversalAllHealModifier, 1f) + ) + return damage; + + foreach (var (key, value) in damage.DamageDict) + { + if (value == 0) + continue; + + if (value > 0) + { + damage.DamageDict[key] *= UniversalAllDamageModifier; + + continue; + } + + if (value < 0) + damage.DamageDict[key] *= UniversalAllHealModifier; + } + + return damage; + } + + public void ClearAllDamage(Entity ent) + { + SetAllDamage(ent, FixedPoint2.Zero); + } + + /// + /// Sets all damage types supported by a to the specified value. + /// + /// + /// Does nothing If the given damage value is negative. + /// + public void SetAllDamage(Entity ent, FixedPoint2 newValue) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return; + + if (newValue < 0) + return; + + foreach (var type in ent.Comp.Damage.DamageDict.Keys) + { + ent.Comp.Damage.DamageDict[type] = newValue; + } + + // Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an + // empty damage delta. + OnEntityDamageChanged((ent, ent.Comp), new DamageSpecifier()); + } + + /// + /// Set's the damage modifier set prototype for this entity. + /// + /// The entity we're setting the modifier set of. + /// The prototype we're setting. + public void SetDamageModifierSetId(Entity ent, ProtoId? damageModifierSetId) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.DamageModifierSetId = damageModifierSetId; + + Dirty(ent); + } + + /// + /// Gets the damages currently sustained by an entity. + /// + [Obsolete("Do not rely on the ability to determine a numerically quantifiable amount of damage")] + public DamageSpecifier GetAllDamage(Entity ent) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp)) + return new(); + + return ent.Comp.Damage.Clone(); + } + + /// + /// Gets the total amount of damage currently sustained by an entity. + /// + [Obsolete("Do not rely on the ability to determine a numerically quantifiable amount of damage")] + public FixedPoint2 GetTotalDamage(Entity ent) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return FixedPoint2.Zero; + + return ent.Comp.TotalDamage; + } + + /// + /// Gets the total amount of damage currently sustained by an entity, indexed by damage group. + /// + [Obsolete("Do not rely on the ability to determine a numerically quantifiable amount of damage")] + public IReadOnlyDictionary, FixedPoint2> GetDamagePerGroup(Entity ent) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp)) + return new Dictionary, FixedPoint2>(); + + return ent.Comp.DamagePerGroup; + } + + /// + /// Returns whether the entity can be damaged by the given type of damage + /// + [Obsolete("Do not rely on the ability to determine if an entity will be able to be damaged by something")] + public bool CanBeDamagedBy(Entity ent, ProtoId type) + { + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false)) + return false; + + return SupportsType(ent.Comp.DamageContainerID, type); + } +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs new file mode 100644 index 00000000000..0175b5fb5e9 --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs @@ -0,0 +1,17 @@ +using Content.Shared.Damage.Components; + +namespace Content.Shared.Damage.Systems; + +public sealed partial class DamageableSystem +{ + /// + /// Applies damage to all entities to see how expensive it is to deal damage. + /// + public void ApplyDamageToAllEntities(List> damageables, DamageSpecifier damage) + { + foreach (var (uid, damageable) in damageables) + { + ChangeDamage((uid, damageable), damage); + } + } +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.Events.cs b/Content.Shared/Damage/Systems/DamageableSystem.Events.cs new file mode 100644 index 00000000000..36f997c5a97 --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageableSystem.Events.cs @@ -0,0 +1,320 @@ +using Content.Shared.CCVar; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Inventory; +using Content.Shared.Radiation.Events; +using Content.Shared.Rejuvenate; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Damage.Systems; + +public sealed partial class DamageableSystem +{ + public override void Initialize() + { + RebuildContainerCache(); + + SubscribeLocalEvent(OnPrototypesReloaded); + SubscribeLocalEvent(DamageableInit); + SubscribeLocalEvent(OnIrradiated); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(DamageableHandleState); + SubscribeLocalEvent(DamageableGetState); + + // Damage modifier CVars are updated and stored here to be queried in other systems. + // Note that certain modifiers requires reloading the guidebook. + Subs.CVar( + _config, + CCVars.PlaytestAllDamageModifier, + value => + { + UniversalAllDamageModifier = value; + _chemistryGuideData.ReloadAllReagentPrototypes(); + _explosion.ReloadMap(); + }, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestAllHealModifier, + value => + { + UniversalAllHealModifier = value; + _chemistryGuideData.ReloadAllReagentPrototypes(); + }, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestProjectileDamageModifier, + value => UniversalProjectileDamageModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestMeleeDamageModifier, + value => UniversalMeleeDamageModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestProjectileDamageModifier, + value => UniversalProjectileDamageModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestHitscanDamageModifier, + value => UniversalHitscanDamageModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestReagentDamageModifier, + value => + { + UniversalReagentDamageModifier = value; + _chemistryGuideData.ReloadAllReagentPrototypes(); + }, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestReagentHealModifier, + value => + { + UniversalReagentHealModifier = value; + _chemistryGuideData.ReloadAllReagentPrototypes(); + }, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestExplosionDamageModifier, + value => + { + UniversalExplosionDamageModifier = value; + _explosion.ReloadMap(); + }, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestThrownDamageModifier, + value => UniversalThrownDamageModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestTopicalsHealModifier, + value => UniversalTopicalsHealModifier = value, + true + ); + Subs.CVar( + _config, + CCVars.PlaytestMobDamageModifier, + value => UniversalMobDamageModifier = value, + true + ); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev) + { + if (!ev.WasModified() && !ev.WasModified()) + return; + + RebuildContainerCache(); + } + + private void RebuildContainerCache() + { + _supportedTypesByContainer.Clear(); + + foreach (var proto in _prototypeManager.EnumeratePrototypes()) + { + var set = new HashSet>(); + _supportedTypesByContainer[proto.ID] = set; + + foreach (var type in proto.SupportedTypes) + { + set.Add(type); + } + + foreach (var groupId in proto.SupportedGroups) + { + var group = _prototypeManager.Index(groupId); + foreach (var type in group.DamageTypes) + { + set.Add(type); + } + } + } + } + + /// + /// Initialize a damageable component + /// + private void DamageableInit(Entity ent, ref ComponentInit _) + { + ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup); + ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal(); + } + + private void OnIrradiated(Entity ent, ref OnIrradiatedEvent args) + { + var damageValue = FixedPoint2.New(args.TotalRads); + + // Radiation should really just be a damage group instead of a list of types. + DamageSpecifier damage = new(); + foreach (var typeId in ent.Comp.RadiationDamageTypeIDs) + { + damage.DamageDict.Add(typeId, damageValue); + } + + ChangeDamage(ent.Owner, damage, interruptsDoAfters: false, origin: args.Origin); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + // Do this so that the state changes when we set the damage + _mobThreshold.SetAllowRevives(ent, true); + ClearAllDamage(ent.AsNullable()); + _mobThreshold.SetAllowRevives(ent, false); + } + + private void DamageableGetState(Entity ent, ref ComponentGetState args) + { + args.State = new DamageableComponentState( + _netMan.IsServer ? ent.Comp.Damage : ent.Comp.Damage.Clone(), + ent.Comp.DamageContainerID, + ent.Comp.DamageModifierSetId, + ent.Comp.HealthBarThreshold + ); + } + + private void DamageableHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not DamageableComponentState state) + return; + + ent.Comp.DamageContainerID = state.DamageContainerId; + ent.Comp.DamageModifierSetId = state.ModifierSetId; + ent.Comp.HealthBarThreshold = state.HealthBarThreshold; + + // Has the damage actually changed? + var newDamage = state.Damage.Clone(); + var delta = newDamage - ent.Comp.Damage; + delta.TrimZeros(); + + if (delta.Empty) + return; + + ent.Comp.Damage = newDamage; + + OnEntityDamageChanged(ent, delta); + } +} + +/// +/// Raised before damage is done, so stuff can cancel it if necessary. +/// +[ByRefEvent] +public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false); + +/// +/// Raised on an entity when damage is about to be dealt, +/// in case anything else needs to modify it other than the base +/// damageable component. +/// +/// For example, armor. +/// +public sealed class DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null) + : EntityEventArgs, IInventoryRelayEvent +{ + /// + /// + /// Whenever locational damage is a thing, this should just check only that bit of armor. + /// + public SlotFlags TargetSlots => ~SlotFlags.POCKET; + + /// + /// Contains the original damage, prior to any modifers. + /// + public readonly DamageSpecifier OriginalDamage = damage; + + /// + /// Contains the damage after modifiers have been applied. + /// This is the damage that will be inflicted. + /// + public DamageSpecifier Damage = damage; + + /// + /// Contains the entity which caused the damage, if any was responsible. + /// + public readonly EntityUid? Origin = origin; +} + +public sealed class DamageChangedEvent : EntityEventArgs +{ + /// + /// This is the component whose damage was changed. + /// + /// + /// Given that nearly every component that cares about a change in the damage, needs to know the + /// current damage values, directly passing this information prevents a lot of duplicate + /// Owner.TryGetComponent() calls. + /// + public readonly DamageableComponent Damageable; + + /// + /// The amount by which the damage has changed. If the damage was set directly to some number, this will be + /// null. + /// + public readonly DamageSpecifier? DamageDelta; + + /// + /// Was any of the damage change dealing damage, or was it all healing? + /// + public readonly bool DamageIncreased; + + /// + /// Does this event interrupt DoAfters? + /// Note: As provided in the constructor, this *does not* account for DamageIncreased. + /// As written into the event, this *does* account for DamageIncreased. + /// + public readonly bool InterruptsDoAfters; + + /// + /// Contains the entity which caused the change in damage, if any was responsible. + /// + public readonly EntityUid? Origin; + + public DamageChangedEvent( + DamageableComponent damageable, + DamageSpecifier? damageDelta, + bool interruptsDoAfters, + EntityUid? origin + ) + { + Damageable = damageable; + DamageDelta = damageDelta; + Origin = origin; + + if (DamageDelta is null) + return; + + foreach (var damageChange in DamageDelta.DamageDict.Values) + { + if (damageChange <= 0) + continue; + + DamageIncreased = true; + + break; + } + + InterruptsDoAfters = interruptsDoAfters && DamageIncreased; + } +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index f3a4f378301..fd8564cf3ca 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -1,452 +1,97 @@ using System.Linq; -using Content.Shared.CCVar; using Content.Shared.Chemistry; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; +using Content.Shared.Explosion.EntitySystems; using Content.Shared.FixedPoint; -using Content.Shared.Inventory; -using Content.Shared.Mind.Components; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.Radiation.Events; -using Content.Shared.Rejuvenate; using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Utility; -namespace Content.Shared.Damage -{ - public sealed class DamageableSystem : EntitySystem - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly INetManager _netMan = default!; - [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!; - - private EntityQuery _appearanceQuery; - private EntityQuery _damageableQuery; - private EntityQuery _mindContainerQuery; - - public float UniversalAllDamageModifier { get; private set; } = 1f; - public float UniversalAllHealModifier { get; private set; } = 1f; - public float UniversalMeleeDamageModifier { get; private set; } = 1f; - public float UniversalProjectileDamageModifier { get; private set; } = 1f; - public float UniversalHitscanDamageModifier { get; private set; } = 1f; - public float UniversalReagentDamageModifier { get; private set; } = 1f; - public float UniversalReagentHealModifier { get; private set; } = 1f; - public float UniversalExplosionDamageModifier { get; private set; } = 1f; - public float UniversalThrownDamageModifier { get; private set; } = 1f; - public float UniversalTopicalsHealModifier { get; private set; } = 1f; - public float UniversalMobDamageModifier { get; private set; } = 1f; - - public override void Initialize() - { - SubscribeLocalEvent(DamageableInit); - SubscribeLocalEvent(DamageableHandleState); - SubscribeLocalEvent(DamageableGetState); - SubscribeLocalEvent(OnIrradiated); - SubscribeLocalEvent(OnRejuvenate); - - _appearanceQuery = GetEntityQuery(); - _damageableQuery = GetEntityQuery(); - _mindContainerQuery = GetEntityQuery(); - - // Damage modifier CVars are updated and stored here to be queried in other systems. - // Note that certain modifiers requires reloading the guidebook. - Subs.CVar(_config, CCVars.PlaytestAllDamageModifier, value => - { - UniversalAllDamageModifier = value; - _chemistryGuideData.ReloadAllReagentPrototypes(); - }, true); - Subs.CVar(_config, CCVars.PlaytestAllHealModifier, value => - { - UniversalAllHealModifier = value; - _chemistryGuideData.ReloadAllReagentPrototypes(); - }, true); - Subs.CVar(_config, CCVars.PlaytestProjectileDamageModifier, value => UniversalProjectileDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestMeleeDamageModifier, value => UniversalMeleeDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestProjectileDamageModifier, value => UniversalProjectileDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestHitscanDamageModifier, value => UniversalHitscanDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestReagentDamageModifier, value => - { - UniversalReagentDamageModifier = value; - _chemistryGuideData.ReloadAllReagentPrototypes(); - }, true); - Subs.CVar(_config, CCVars.PlaytestReagentHealModifier, value => - { - UniversalReagentHealModifier = value; - _chemistryGuideData.ReloadAllReagentPrototypes(); - }, true); - Subs.CVar(_config, CCVars.PlaytestExplosionDamageModifier, value => UniversalExplosionDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestThrownDamageModifier, value => UniversalThrownDamageModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestTopicalsHealModifier, value => UniversalTopicalsHealModifier = value, true); - Subs.CVar(_config, CCVars.PlaytestMobDamageModifier, value => UniversalMobDamageModifier = value, true); - } - - /// - /// Initialize a damageable component - /// - private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _) - { - if (component.DamageContainerID != null && - _prototypeManager.Resolve(component.DamageContainerID, - out var damageContainerPrototype)) - { - // Initialize damage dictionary, using the types and groups from the damage - // container prototype - foreach (var type in damageContainerPrototype.SupportedTypes) - { - component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); - } - - foreach (var groupId in damageContainerPrototype.SupportedGroups) - { - var group = _prototypeManager.Index(groupId); - foreach (var type in group.DamageTypes) - { - component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero); - } - } - } - else - { - // No DamageContainerPrototype was given. So we will allow the container to support all damage types - foreach (var type in _prototypeManager.EnumeratePrototypes()) - { - component.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero); - } - } - - component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); - component.TotalDamage = component.Damage.GetTotal(); - } - - /// - /// Directly sets the damage specifier of a damageable component. - /// - /// - /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed - /// event is raised. - /// - public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpecifier damage) - { - damageable.Damage = damage; - DamageChanged(uid, damageable); - } - - /// - /// If the damage in a DamageableComponent was changed, this function should be called. - /// - /// - /// This updates cached damage information, flags the component as dirty, and raises a damage changed event. - /// The damage changed event is used by other systems, such as damage thresholds. - /// - public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null, - bool interruptsDoAfters = true, EntityUid? origin = null) - { - component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); - component.TotalDamage = component.Damage.GetTotal(); - Dirty(uid, component); - - if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null) - { - var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList()); - _appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance); - } - RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin)); - } - - /// - /// Applies damage specified via a . - /// - /// - /// is effectively just a dictionary of damage types and damage values. This - /// function just applies the container's resistances (unless otherwise specified) and then changes the - /// stored damage data. Division of group damage into types is managed by . - /// - /// - /// Returns a with information about the actual damage changes. This will be - /// null if the user had no applicable components that can take damage. - /// - public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false, - bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null) - { - if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false)) - { - // TODO BODY SYSTEM pass damage onto body system - return null; - } - - if (damage.Empty) - { - return damage; - } - - var before = new BeforeDamageChangedEvent(damage, origin); - RaiseLocalEvent(uid.Value, ref before); - - if (before.Cancelled) - return null; - - // Apply resistances - if (!ignoreResistances) - { - if (damageable.DamageModifierSetId != null && - _prototypeManager.Resolve(damageable.DamageModifierSetId, out var modifierSet)) - { - // TODO DAMAGE PERFORMANCE - // use a local private field instead of creating a new dictionary here.. - damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); - } - - var ev = new DamageModifyEvent(damage, origin); - RaiseLocalEvent(uid.Value, ev); - damage = ev.Damage; - - if (damage.Empty) - { - return damage; - } - } - - damage = ApplyUniversalAllModifiers(damage); - - // TODO DAMAGE PERFORMANCE - // Consider using a local private field instead of creating a new dictionary here. - // Would need to check that nothing ever tries to cache the delta. - var delta = new DamageSpecifier(); - delta.DamageDict.EnsureCapacity(damage.DamageDict.Count); - - var dict = damageable.Damage.DamageDict; - foreach (var (type, value) in damage.DamageDict) - { - // CollectionsMarshal my beloved. - if (!dict.TryGetValue(type, out var oldValue)) - continue; - - var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value); - if (newValue == oldValue) - continue; - - dict[type] = newValue; - delta.DamageDict[type] = newValue - oldValue; - } - - if (delta.DamageDict.Count > 0) - DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin); - - return delta; - } - - /// - /// Applies the two univeral "All" modifiers, if set. - /// Individual damage source modifiers are set in their respective code. - /// - /// The damage to be changed. - public DamageSpecifier ApplyUniversalAllModifiers(DamageSpecifier damage) - { - // Checks for changes first since they're unlikely in normal play. - if (UniversalAllDamageModifier == 1f && UniversalAllHealModifier == 1f) - return damage; - - foreach (var (key, value) in damage.DamageDict) - { - if (value == 0) - continue; +namespace Content.Shared.Damage.Systems; - if (value > 0) - { - damage.DamageDict[key] *= UniversalAllDamageModifier; - continue; - } - - if (value < 0) - { - damage.DamageDict[key] *= UniversalAllHealModifier; - } - } - - return damage; - } - - /// - /// Sets all damage types supported by a to the specified value. - /// - /// - /// Does nothing If the given damage value is negative. - /// - public void SetAllDamage(EntityUid uid, DamageableComponent component, FixedPoint2 newValue) - { - if (newValue < 0) - { - // invalid value - return; - } - - foreach (var type in component.Damage.DamageDict.Keys) - { - component.Damage.DamageDict[type] = newValue; - } - - // Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an - // empty damage delta. - DamageChanged(uid, component, new DamageSpecifier()); - } - - public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null) - { - if (!_damageableQuery.Resolve(uid, ref comp)) - return; - - comp.DamageModifierSetId = damageModifierSetId; - Dirty(uid, comp); - } - - private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args) - { - if (_netMan.IsServer) - { - args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold); - } - else - { - // avoid mispredicting damage on newly spawned entities. - args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold); - } - } - - private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradiatedEvent args) - { - var damageValue = FixedPoint2.New(args.TotalRads); - - // Radiation should really just be a damage group instead of a list of types. - DamageSpecifier damage = new(); - foreach (var typeId in component.RadiationDamageTypeIDs) - { - damage.DamageDict.Add(typeId, damageValue); - } - - TryChangeDamage(uid, damage, interruptsDoAfters: false, origin: args.Origin); - } - - private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args) - { - TryComp(uid, out var thresholds); - _mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage - SetAllDamage(uid, component, 0); - _mobThreshold.SetAllowRevives(uid, false, thresholds); - } - - private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args) - { - if (args.Current is not DamageableComponentState state) - { - return; - } - - component.DamageContainerID = state.DamageContainerId; - component.DamageModifierSetId = state.ModifierSetId; - component.HealthBarThreshold = state.HealthBarThreshold; - - // Has the damage actually changed? - DamageSpecifier newDamage = new() { DamageDict = new(state.DamageDict) }; - var delta = newDamage - component.Damage; - delta.TrimZeros(); - - if (!delta.Empty) - { - component.Damage = newDamage; - DamageChanged(uid, component, delta); - } - } - } - - /// - /// Raised before damage is done, so stuff can cancel it if necessary. - /// - [ByRefEvent] - public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false); +public sealed partial class DamageableSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!; + [Dependency] private readonly SharedExplosionSystem _explosion = default!; + + [Dependency] private readonly EntityQuery _appearanceQuery = default!; + [Dependency] private readonly EntityQuery _damageableQuery = default!; + + public float UniversalAllDamageModifier { get; private set; } = 1f; + public float UniversalAllHealModifier { get; private set; } = 1f; + public float UniversalMeleeDamageModifier { get; private set; } = 1f; + public float UniversalProjectileDamageModifier { get; private set; } = 1f; + public float UniversalHitscanDamageModifier { get; private set; } = 1f; + public float UniversalReagentDamageModifier { get; private set; } = 1f; + public float UniversalReagentHealModifier { get; private set; } = 1f; + public float UniversalExplosionDamageModifier { get; private set; } = 1f; + public float UniversalThrownDamageModifier { get; private set; } = 1f; + public float UniversalTopicalsHealModifier { get; private set; } = 1f; + public float UniversalMobDamageModifier { get; private set; } = 1f; + + private Dictionary, HashSet>> _supportedTypesByContainer = new(); /// - /// Raised on an entity when damage is about to be dealt, - /// in case anything else needs to modify it other than the base - /// damageable component. - /// - /// For example, armor. + /// If the damage in a DamageableComponent was changed this function should be called. /// - public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent + /// + /// This updates cached damage information, flags the component as dirty, and raises a damage changed event. + /// The damage changed event is used by other systems, such as damage thresholds. + /// + private void OnEntityDamageChanged( + Entity ent, + DamageSpecifier? damageDelta = null, + bool interruptsDoAfters = true, + EntityUid? origin = null + ) { - // Whenever locational damage is a thing, this should just check only that bit of armour. - public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET; - - public readonly DamageSpecifier OriginalDamage; - public DamageSpecifier Damage; - public EntityUid? Origin; + ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup); + ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal(); + Dirty(ent); - public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null) + if (damageDelta != null && _appearanceQuery.TryGetComponent(ent, out var appearance)) { - OriginalDamage = damage; - Damage = damage; - Origin = origin; + _appearance.SetData( + ent, + DamageVisualizerKeys.DamageUpdateGroups, + new DamageVisualizerGroupData(ent.Comp.DamagePerGroup.Keys.ToList()), + appearance + ); } + + // TODO DAMAGE + // byref struct event. + RaiseLocalEvent(ent, new DamageChangedEvent(ent.Comp, damageDelta, interruptsDoAfters, origin)); } - public sealed class DamageChangedEvent : EntityEventArgs + /// + /// Goes through an entity damage's and saves them inside a dictionary if the value is higher than 0 + /// The dictionary is structured with a string for the name of the damage type, and a FixedPoint2 for the numeric damage value + /// + public Dictionary, FixedPoint2> GetDamages(Dictionary, FixedPoint2> damagePerGroup, DamageSpecifier damage) { - /// - /// This is the component whose damage was changed. - /// - /// - /// Given that nearly every component that cares about a change in the damage, needs to know the - /// current damage values, directly passing this information prevents a lot of duplicate - /// Owner.TryGetComponent() calls. - /// - public readonly DamageableComponent Damageable; - - /// - /// The amount by which the damage has changed. If the damage was set directly to some number, this will be - /// null. - /// - public readonly DamageSpecifier? DamageDelta; + var damageTypes = new Dictionary, FixedPoint2>(); - /// - /// Was any of the damage change dealing damage, or was it all healing? - /// - public readonly bool DamageIncreased; - - /// - /// Does this event interrupt DoAfters? - /// Note: As provided in the constructor, this *does not* account for DamageIncreased. - /// As written into the event, this *does* account for DamageIncreased. - /// - public readonly bool InterruptsDoAfters; - - /// - /// Contains the entity which caused the change in damage, if any was responsible. - /// - public readonly EntityUid? Origin; - - public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin) + foreach (var (damageGroupId, _) in damagePerGroup) //go through each group { - Damageable = damageable; - DamageDelta = damageDelta; - Origin = origin; - - if (DamageDelta == null) - return; - - foreach (var damageChange in DamageDelta.DamageDict.Values) + var group = _prototypeManager.Index(damageGroupId); //get group + foreach (var type in group.DamageTypes) //go through each type inside that group { - if (damageChange > 0) - { - DamageIncreased = true; - break; - } + if (!damage.DamageDict.TryGetValue(type, out var damageValue) || damageValue == 0) //get value and make sure it isn't 0 + continue; + + damageTypes.Add(type, damageValue); } - InterruptsDoAfters = interruptsDoAfters && DamageIncreased; } + return damageTypes; } } diff --git a/Content.Shared/Damage/Systems/PassiveDamageSystem.cs b/Content.Shared/Damage/Systems/PassiveDamageSystem.cs index e750863e243..94fff7e97d8 100644 --- a/Content.Shared/Damage/Systems/PassiveDamageSystem.cs +++ b/Content.Shared/Damage/Systems/PassiveDamageSystem.cs @@ -1,10 +1,8 @@ using Content.Shared.Damage.Components; -using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Components; -using Content.Shared.FixedPoint; using Robust.Shared.Timing; -namespace Content.Shared.Damage; +namespace Content.Shared.Damage.Systems; public sealed class PassiveDamageSystem : EntitySystem { @@ -37,9 +35,6 @@ public override void Update(float frameTime) if (comp.NextDamage > curTime) continue; - if (comp.DamageCap != 0 && damage.TotalDamage >= comp.DamageCap) - continue; - // Set the next time they can take damage comp.NextDamage = curTime + TimeSpan.FromSeconds(1f); @@ -47,7 +42,7 @@ public override void Update(float frameTime) foreach (var allowedState in comp.AllowedStates) { if(allowedState == mobState.CurrentState) - _damageable.TryChangeDamage(uid, comp.Damage, true, false, damage); + _damageable.ChangeDamage((uid, damage), comp.Damage, true, false); } } } diff --git a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs index 66b1de65e8e..63b690a67c0 100644 --- a/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs +++ b/Content.Shared/Damage/Systems/RequireProjectileTargetSystem.cs @@ -1,10 +1,11 @@ +using Content.Shared.Damage.Components; using Content.Shared.Projectiles; -using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Standing; -using Robust.Shared.Physics.Events; +using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Containers; +using Robust.Shared.Physics.Events; -namespace Content.Shared.Damage.Components; +namespace Content.Shared.Damage.Systems; public sealed class RequireProjectileTargetSystem : EntitySystem { diff --git a/Content.Shared/Damage/Systems/SharedDamageOtherOnHitSystem.cs b/Content.Shared/Damage/Systems/SharedDamageOtherOnHitSystem.cs new file mode 100644 index 00000000000..3a76833b53a --- /dev/null +++ b/Content.Shared/Damage/Systems/SharedDamageOtherOnHitSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.CombatMode.Pacification; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Events; + +namespace Content.Shared.Damage.Systems; + +public abstract class SharedDamageOtherOnHitSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly DamageExamineSystem _damageExamine = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDamageExamine); + SubscribeLocalEvent(OnAttemptPacifiedThrow); + } + + private void OnDamageExamine(Entity ent, ref DamageExamineEvent args) + { + _damageExamine.AddDamageExamine(args.Message, _damageable.ApplyUniversalAllModifiers(ent.Comp.Damage * _damageable.UniversalThrownDamageModifier), Loc.GetString("damage-throw")); + } + + /// + /// Prevent players with the Pacified status effect from throwing things that deal damage. + /// + private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) + { + args.Cancel("pacified-cannot-throw"); + } + + /// + /// Imperial Medieval: scales throw damage (smithing quality modifiers). + /// + public void ScaleDamage(Entity ent, float factor) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.Damage *= factor; + Dirty(ent); + } +} diff --git a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs index 4bf762c479a..39ff4f10707 100644 --- a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs +++ b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs @@ -15,7 +15,6 @@ namespace Content.Shared.Damage.Systems; public abstract class SharedGodmodeSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _protoMan = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; public override void Initialize() { @@ -69,13 +68,6 @@ private void BeforeEdible(Entity ent, ref IngestibleEvent args public virtual void EnableGodmode(EntityUid uid, GodmodeComponent? godmode = null) { - godmode ??= EnsureComp(uid); - - if (TryComp(uid, out var damageable)) - { - godmode.OldDamage = new DamageSpecifier(damageable.Damage); - } - // Rejuv to cover other stuff RaiseLocalEvent(uid, new RejuvenateEvent()); } @@ -85,11 +77,6 @@ public virtual void DisableGodmode(EntityUid uid, GodmodeComponent? godmode = nu if (!Resolve(uid, ref godmode, false)) return; - if (TryComp(uid, out var damageable) && godmode.OldDamage != null) - { - _damageable.SetDamage(uid, damageable, godmode.OldDamage); - } - RemComp(uid); } diff --git a/Content.Shared/Damage/Systems/SharedStaminaSystem.Modifier.cs b/Content.Shared/Damage/Systems/SharedStaminaSystem.Modifier.cs index c723d8cb949..8fcb4b8d1d4 100644 --- a/Content.Shared/Damage/Systems/SharedStaminaSystem.Modifier.cs +++ b/Content.Shared/Damage/Systems/SharedStaminaSystem.Modifier.cs @@ -1,4 +1,6 @@ using Content.Shared.Damage.Components; +using Content.Shared.Damage.Events; +using Content.Shared.StatusEffectNew; namespace Content.Shared.Damage.Systems; @@ -6,47 +8,36 @@ public partial class SharedStaminaSystem { private void InitializeModifier() { - SubscribeLocalEvent(OnModifierStartup); - SubscribeLocalEvent(OnModifierShutdown); + SubscribeLocalEvent(OnEffectApplied); + SubscribeLocalEvent(OnEffectRemoved); + SubscribeLocalEvent>(OnRefreshCritThreshold); } - private void OnModifierStartup(EntityUid uid, StaminaModifierComponent comp, ComponentStartup args) + private void OnEffectApplied(Entity ent, ref StatusEffectAppliedEvent args) { - if (!TryComp(uid, out var stamina)) - return; - - stamina.CritThreshold *= comp.Modifier; + RefreshStaminaCritThreshold(args.Target); } - private void OnModifierShutdown(EntityUid uid, StaminaModifierComponent comp, ComponentShutdown args) + private void OnEffectRemoved(Entity ent, ref StatusEffectRemovedEvent args) { - if (!TryComp(uid, out var stamina)) - return; - - stamina.CritThreshold /= comp.Modifier; + RefreshStaminaCritThreshold(args.Target); } - /// - /// Change the stamina modifier for an entity. - /// If it has it will also be updated. - /// - public void SetModifier(EntityUid uid, float modifier, StaminaComponent? stamina = null, StaminaModifierComponent? comp = null) + private void OnRefreshCritThreshold(Entity ent, ref StatusEffectRelayedEvent args) { - if (!Resolve(uid, ref comp)) - return; - - var old = comp.Modifier; + var evArgs = args.Args; + evArgs.Modifier = Math.Max(ent.Comp.Modifier, evArgs.Modifier); // We only pick the highest value, to avoid stacking different status effects. + args.Args = evArgs; + } - if (old.Equals(modifier)) + public void RefreshStaminaCritThreshold(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) return; - comp.Modifier = modifier; - Dirty(uid, comp); + var ev = new RefreshStaminaCritThresholdEvent(entity.Comp.BaseCritThreshold); + RaiseLocalEvent(entity, ref ev); - if (Resolve(uid, ref stamina, false)) - { - // scale to the new threshold, act as if it was removed then added - stamina.CritThreshold *= modifier / old; - } + entity.Comp.CritThreshold = ev.ThresholdValue * ev.Modifier; } } diff --git a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs index ef62fa12440..2772018f16c 100644 --- a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs +++ b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs @@ -52,6 +52,8 @@ public abstract partial class SharedStaminaSystem : EntitySystem [Dependency] private readonly StatusEffectsSystem _status = default!; [Dependency] protected readonly SharedStunSystem StunSystem = default!; + [Dependency] private readonly EntityQuery _stamQuery = default!; + /// /// How much of a buffer is there between the stun duration and when stuns can be re-applied. /// @@ -108,6 +110,9 @@ protected virtual void OnShutdown(Entity entity, ref Component private void OnStartup(Entity entity, ref ComponentStartup args) { + // Set the base threshold here since ModifiedCritThreshold can't be modified via yaml. + entity.Comp.CritThreshold = entity.Comp.BaseCritThreshold; + UpdateStaminaVisuals(entity); } @@ -163,18 +168,17 @@ private void OnMeleeHit(EntityUid uid, StaminaDamageOnHitComponent component, Me return; } - var ev = new BeforeStaminaDamageOnTriggerEvent(); + var ev = new StaminaDamageOnHitAttemptEvent(); RaiseLocalEvent(uid, ref ev); if (ev.Cancelled) return; - var stamQuery = GetEntityQuery(); var toHit = new List<(EntityUid Entity, StaminaComponent Component)>(); // Split stamina damage between all eligible targets. foreach (var ent in args.HitEntities) { - if (!stamQuery.TryGetComponent(ent, out var stam)) + if (!_stamQuery.TryGetComponent(ent, out var stam)) continue; toHit.Add((ent, stam)); @@ -223,7 +227,7 @@ private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component, if (!HasComp(target)) return; - var ev = new BeforeStaminaDamageOnTriggerEvent(); + var ev = new StaminaDamageOnHitAttemptEvent(); RaiseLocalEvent(uid, ref ev); if (ev.Cancelled) return; @@ -364,14 +368,13 @@ public override void Update(float frameTime) { base.Update(frameTime); - var stamQuery = GetEntityQuery(); var query = EntityQueryEnumerator(); var curTime = Timing.CurTime; while (query.MoveNext(out var uid, out _)) { // Just in case we have active but not stamina we'll check and account for it. - if (!stamQuery.TryGetComponent(uid, out var comp) || + if (!_stamQuery.TryComp(uid, out var comp) || comp.StaminaDamage <= 0f && !comp.Critical) { RemComp(uid); @@ -468,7 +471,7 @@ private void AdjustStatus(Entity ent) { var key = thres.Key.Float(); - if (ent.Comp.StaminaDamage >= key && key > closest && closest < ent.Comp.CritThreshold) + if ((ent.Comp.StaminaDamage / ent.Comp.CritThreshold) >= key && key > closest && closest < 1f) closest = thres.Key; } diff --git a/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs b/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs index e339727200d..4c80aa89d0f 100644 --- a/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs +++ b/Content.Shared/Damage/Systems/SlowOnDamageSystem.cs @@ -5,109 +5,111 @@ using Content.Shared.Inventory; using Content.Shared.Movement.Systems; -namespace Content.Shared.Damage +namespace Content.Shared.Damage.Systems; + +public sealed class SlowOnDamageSystem : EntitySystem { - public sealed class SlowOnDamageSystem : EntitySystem + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!; + [Dependency] private readonly DamageableSystem _damage = default!; + + public override void Initialize() { - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!; + base.Initialize(); - public override void Initialize() - { - base.Initialize(); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnRefreshMovespeed); - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnRefreshMovespeed); + SubscribeLocalEvent>(OnModifySpeed); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent>(OnModifySpeed); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); + SubscribeLocalEvent(OnIgnoreStartup); + SubscribeLocalEvent(OnIgnoreShutdown); + SubscribeLocalEvent(OnIgnoreModifySpeed); + } - SubscribeLocalEvent(OnIgnoreStartup); - SubscribeLocalEvent(OnIgnoreShutdown); - SubscribeLocalEvent(OnIgnoreModifySpeed); - } + private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args) + { + if (!TryComp(uid, out var damage)) + return; + + var totalDamage = _damage.GetTotalDamage((uid, damage)); + + if (totalDamage == FixedPoint2.Zero) + return; - private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args) + // Get closest threshold + FixedPoint2 closest = FixedPoint2.Zero; + var total = totalDamage; + foreach (var thres in component.SpeedModifierThresholds) { - if (!TryComp(uid, out var damage)) - return; - - if (damage.TotalDamage == FixedPoint2.Zero) - return; - - // Get closest threshold - FixedPoint2 closest = FixedPoint2.Zero; - var total = damage.TotalDamage; - foreach (var thres in component.SpeedModifierThresholds) - { - if (total >= thres.Key && thres.Key > closest) - closest = thres.Key; - } - - if (closest != FixedPoint2.Zero) - { - var speed = component.SpeedModifierThresholds[closest]; - - var ev = new ModifySlowOnDamageSpeedEvent(speed); - RaiseLocalEvent(uid, ref ev); - args.ModifySpeed(ev.Speed, ev.Speed); - } + if (total >= thres.Key && thres.Key > closest) + closest = thres.Key; } - private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args) + if (closest != FixedPoint2.Zero) { - // We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated - // code and this isn't a super hot path anyway since basically only humans have this + var speed = component.SpeedModifierThresholds[closest]; - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid); + var ev = new ModifySlowOnDamageSpeedEvent(speed); + RaiseLocalEvent(uid, ref ev); + args.ModifySpeed(ev.Speed, ev.Speed); } + } - private void OnModifySpeed(Entity ent, ref InventoryRelayedEvent args) - { - var dif = 1 - args.Args.Speed; - if (dif <= 0) - return; + private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args) + { + // We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated + // code and this isn't a super hot path anyway since basically only humans have this - // reduces the slowness modifier by the given coefficient - args.Args.Speed += dif * ent.Comp.Modifier; - } + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid); + } - private void OnExamined(Entity ent, ref ExaminedEvent args) - { - var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100)); - args.PushMarkup(msg); - } + private void OnModifySpeed(Entity ent, ref InventoryRelayedEvent args) + { + var dif = 1 - args.Args.Speed; + if (dif <= 0) + return; - private void OnGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); - } + // reduces the slowness modifier by the given coefficient + args.Args.Speed += dif * ent.Comp.Modifier; + } - private void OnGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); - } + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100)); + args.PushMarkup(msg); + } - private void OnIgnoreStartup(Entity ent, ref ComponentStartup args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); - } + private void OnGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); + } - private void OnIgnoreShutdown(Entity ent, ref ComponentShutdown args) - { - _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); - } + private void OnGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer); + } - private void OnIgnoreModifySpeed(Entity ent, ref ModifySlowOnDamageSpeedEvent args) - { - args.Speed = 1f; - } + private void OnIgnoreStartup(Entity ent, ref ComponentStartup args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); + } + + private void OnIgnoreShutdown(Entity ent, ref ComponentShutdown args) + { + _movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent); } - [ByRefEvent] - public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent + private void OnIgnoreModifySpeed(Entity ent, ref ModifySlowOnDamageSpeedEvent args) { - public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; + args.Speed = 1f; } } + +[ByRefEvent] +public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent +{ + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} diff --git a/Content.Shared/Delivery/SharedDeliverySystem.cs b/Content.Shared/Delivery/SharedDeliverySystem.cs index d7fc40dcc66..71baa92ec67 100644 --- a/Content.Shared/Delivery/SharedDeliverySystem.cs +++ b/Content.Shared/Delivery/SharedDeliverySystem.cs @@ -162,7 +162,7 @@ private void OnGetSpawnerVerbs(Entity ent, ref GetVerb private bool TryUnlockDelivery(Entity ent, EntityUid user, bool rewardMoney = true, bool force = false) { // Check fingerprint access if there is a reader on the mail - if (!force && TryComp(ent, out var reader) && !_fingerprintReader.IsAllowed((ent, reader), user)) + if (!force && !_fingerprintReader.IsAllowed(ent.Owner, user, out _)) return false; var deliveryName = _nameModifier.GetBaseName(ent.Owner); diff --git a/Content.Shared/Destructible/SharedDestructibleSystem.cs b/Content.Shared/Destructible/SharedDestructibleSystem.cs index 7ea43890a52..e51d625cd95 100644 --- a/Content.Shared/Destructible/SharedDestructibleSystem.cs +++ b/Content.Shared/Destructible/SharedDestructibleSystem.cs @@ -1,9 +1,14 @@ -namespace Content.Shared.Destructible; +using Content.Shared.Damage.Systems; + +namespace Content.Shared.Destructible; public abstract class SharedDestructibleSystem : EntitySystem { + // TODO: I don't really like this but this is out of scope to re-do destructible triggers while refactoring damageable + [Dependency] public readonly DamageableSystem Damageable = default!; + /// - /// Force entity to be destroyed and deleted. + /// Force entity to be destroyed and deleted. /// public bool DestroyEntity(EntityUid owner, EntityUid? performer = null) { @@ -15,12 +20,12 @@ public bool DestroyEntity(EntityUid owner, EntityUid? performer = null) var eventArgs = new DestructionEventArgs(performer); RaiseLocalEvent(owner, eventArgs); - QueueDel(owner); + PredictedQueueDel(owner); return true; } /// - /// Force entity to break. + /// Force entity to break. /// public void BreakEntity(EntityUid owner, EntityUid? performer = null) { @@ -30,7 +35,7 @@ public void BreakEntity(EntityUid owner, EntityUid? performer = null) } /// -/// Raised before an entity is about to be destroyed and deleted +/// Raised before an entity is about to be destroyed and deleted /// public sealed class DestructionAttemptEvent : CancellableEntityEventArgs { @@ -38,7 +43,7 @@ public sealed class DestructionAttemptEvent : CancellableEntityEventArgs } /// -/// Raised when entity is destroyed and about to be deleted. +/// Raised when entity is destroyed and about to be deleted. /// public sealed class DestructionEventArgs : EntityEventArgs { @@ -51,7 +56,7 @@ public DestructionEventArgs(EntityUid? performer = null) } /// -/// Raised when entity was heavy damage and about to break. +/// Raised when entity was heavy damage and about to break. /// public sealed class BreakageEventArgs : EntityEventArgs { diff --git a/Content.Shared/Destructible/ThresholdActs.cs b/Content.Shared/Destructible/ThresholdActs.cs new file mode 100644 index 00000000000..281d58092fb --- /dev/null +++ b/Content.Shared/Destructible/ThresholdActs.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible; + +[Flags] +[Serializable, NetSerializable] +public enum ThresholdActs : byte +{ + None = 0, + Breakage = 1 << 0, + Destruction = 1 << 1, +} diff --git a/Content.Shared/Destructible/Triggers/AndTrigger.cs b/Content.Shared/Destructible/Triggers/AndTrigger.cs new file mode 100644 index 00000000000..938f125b01c --- /dev/null +++ b/Content.Shared/Destructible/Triggers/AndTrigger.cs @@ -0,0 +1,28 @@ +using Content.Shared.Damage.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when all of its triggers have activated. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class AndTrigger : IThresholdTrigger +{ + [DataField] + public List Triggers = new(); + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + foreach (var trigger in Triggers) + { + if (!trigger.Reached(damageable, system)) + { + return false; + } + } + + return true; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs b/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs new file mode 100644 index 00000000000..3acbf253758 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs @@ -0,0 +1,33 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the amount of damage received +/// of the specified class is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageGroupTrigger : IThresholdTrigger +{ + /// + /// The damage group to check for. + /// + [DataField(required: true)] + public ProtoId DamageGroup = default!; + + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return system.Damageable.GetDamagePerGroup(damageable.Owner).GetValueOrDefault(DamageGroup) >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTrigger.cs new file mode 100644 index 00000000000..481b55b30fd --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageTrigger.cs @@ -0,0 +1,25 @@ +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the total amount of damage received +/// is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageTrigger : IThresholdTrigger +{ + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return system.Damageable.GetTotalDamage(damageable.AsNullable()) >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs new file mode 100644 index 00000000000..5debc51864e --- /dev/null +++ b/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs @@ -0,0 +1,34 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when the amount of damage received +/// of the specified type is above the specified threshold. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class DamageTypeTrigger : IThresholdTrigger +{ + /// + /// The damage type to check for. + /// + [DataField(required: true)] + public ProtoId DamageType = default!; + + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField(required: true)] + public FixedPoint2 Damage = default!; + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + return system.Damageable.GetAllDamage(damageable.AsNullable()).DamageDict.TryGetValue(DamageType, out var damageReceived) && + damageReceived >= Damage; + } +} diff --git a/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs b/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs new file mode 100644 index 00000000000..d97394c4065 --- /dev/null +++ b/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs @@ -0,0 +1,27 @@ +using Content.Shared.Damage.Components; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A condition for triggering a . +/// +/// +/// I decided against converting these into EntityEffectConditions for performance reasons +/// (although I did not do any benchmarks, so it might be fine). +/// Entity effects will raise a separate event for each entity and each condition, which can become a huge number +/// for cases like nuke explosions or shuttle collisions where there are lots of DamageChangedEvents at once. +/// IThresholdTriggers on the other hand are directly checked in a foreach loop without raising events. +/// And there are only few of these conditions, so there is only a minor amount of code duplication. +/// +public interface IThresholdTrigger +{ + /// + /// Checks if this trigger has been reached. + /// + /// The damageable component to check with. + /// + /// An instance of to pull dependencies from, if any. + /// + /// true if this trigger has been reached, false otherwise. + bool Reached(Entity damageable, SharedDestructibleSystem system); +} diff --git a/Content.Shared/Destructible/Triggers/OrTrigger.cs b/Content.Shared/Destructible/Triggers/OrTrigger.cs new file mode 100644 index 00000000000..cdf72bed7fe --- /dev/null +++ b/Content.Shared/Destructible/Triggers/OrTrigger.cs @@ -0,0 +1,28 @@ +using Content.Shared.Damage.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Destructible.Thresholds.Triggers; + +/// +/// A trigger that will activate when any of its triggers have activated. +/// +[Serializable, NetSerializable] +[DataDefinition] +public sealed partial class OrTrigger : IThresholdTrigger +{ + [DataField] + public List Triggers = new(); + + public bool Reached(Entity damageable, SharedDestructibleSystem system) + { + foreach (var trigger in Triggers) + { + if (trigger.Reached(damageable, system)) + { + return true; + } + } + + return false; + } +} diff --git a/Content.Shared/DeviceLinking/Components/RandomGateComponent.cs b/Content.Shared/DeviceLinking/Components/RandomGateComponent.cs new file mode 100644 index 00000000000..0be9b6bff8d --- /dev/null +++ b/Content.Shared/DeviceLinking/Components/RandomGateComponent.cs @@ -0,0 +1,35 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.GameStates; + +namespace Content.Shared.DeviceLinking.Components; + +/// +/// A component for a random gate, which outputs a signal with a given probability. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RandomGateComponent : Component +{ + /// + /// The input port for receiving signals. + /// + [DataField] + public ProtoId InputPort = "RandomGateInput"; + + /// + /// The output port for sending signals. + /// + [DataField] + public ProtoId OutputPort = "Output"; + + /// + /// The last output state of the gate. + /// + [DataField, AutoNetworkedField] + public bool LastOutput; + + /// + /// The probability (0.0 to 1.0) that the gate will output a signal. + /// + [DataField, AutoNetworkedField] + public float SuccessProbability = 0.5f; +} diff --git a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs index 5d51bbc3e83..9e4e964f0aa 100644 --- a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs +++ b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs @@ -18,6 +18,8 @@ public abstract class SharedDeviceLinkSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly EntityQuery _deviceLinkSinkQuery = default!; + public const string InvokedPort = "link_port"; /// @@ -82,10 +84,9 @@ private void OnSourceStartup(Entity source, ref Compo /// private void OnSourceRemoved(Entity source, ref ComponentRemove args) { - var query = GetEntityQuery(); foreach (var sinkUid in source.Comp.LinkedPorts.Keys) { - if (query.TryGetComponent(sinkUid, out var sink)) + if (_deviceLinkSinkQuery.TryGetComponent(sinkUid, out var sink)) RemoveSinkFromSourceInternal(source, sinkUid, source, sink); else Log.Error($"Device source {ToPrettyString(source)} links to invalid entity: {ToPrettyString(sinkUid)}"); @@ -214,6 +215,17 @@ public string PortName(string port) where TPort : DevicePortPrototype, IP return links; } + /// + /// Gets the entities linked to a specific source port. + /// + public HashSet GetLinkedSinks(Entity source, ProtoId port) + { + if (!Resolve(source, ref source.Comp) || !source.Comp.Outputs.TryGetValue(port, out var linked)) + return new HashSet(); // not a source or not linked + + return new HashSet(linked); // clone to prevent modifying the original + } + /// /// Returns the default links for the given list of source port prototypes /// diff --git a/Content.Shared/DeviceLinking/SharedRandomGateEvents.cs b/Content.Shared/DeviceLinking/SharedRandomGateEvents.cs new file mode 100644 index 00000000000..7d26ff86236 --- /dev/null +++ b/Content.Shared/DeviceLinking/SharedRandomGateEvents.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.DeviceLinking; + +[Serializable, NetSerializable] +public sealed class RandomGateBoundUserInterfaceState(float successProbability) : BoundUserInterfaceState +{ + public float SuccessProbability = successProbability; +} + +[Serializable, NetSerializable] +public sealed class RandomGateProbabilityChangedMessage(float probability) : BoundUserInterfaceMessage +{ + public float Probability = probability; +} + +[Serializable, NetSerializable] +public enum RandomGateUiKey : byte +{ + Key +} diff --git a/Content.Shared/DeviceLinking/Systems/SharedRandomGateSystem.cs b/Content.Shared/DeviceLinking/Systems/SharedRandomGateSystem.cs new file mode 100644 index 00000000000..74d25cd8a6d --- /dev/null +++ b/Content.Shared/DeviceLinking/Systems/SharedRandomGateSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared.DeviceLinking.Components; +using Content.Shared.UserInterface; + +namespace Content.Shared.DeviceLinking.Systems; + +public abstract class SharedRandomGateSystem : EntitySystem +{ + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnAfterActivatableUIOpen); + SubscribeLocalEvent(OnProbabilityChanged); + } + + private void OnAfterActivatableUIOpen(Entity ent, ref AfterActivatableUIOpenEvent args) + { + UpdateUI(ent); + } + + private void OnProbabilityChanged(Entity ent, ref RandomGateProbabilityChangedMessage args) + { + ent.Comp.SuccessProbability = Math.Clamp(args.Probability, 0f, 100f) / 100f; + Dirty(ent); + UpdateUI(ent); + } + + private void UpdateUI(Entity ent) + { + if (!_ui.HasUi(ent.Owner, RandomGateUiKey.Key)) + return; + + _ui.SetUiState(ent.Owner, RandomGateUiKey.Key, new RandomGateBoundUserInterfaceState(ent.Comp.SuccessProbability)); + } +} diff --git a/Content.Shared/Devour/DevourSystem.cs b/Content.Shared/Devour/DevourSystem.cs index 834b4df84af..8dded21a80f 100644 --- a/Content.Shared/Devour/DevourSystem.cs +++ b/Content.Shared/Devour/DevourSystem.cs @@ -1,9 +1,9 @@ using Content.Shared.Actions; -using Content.Shared.Body.Events; using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Components; using Content.Shared.Devour.Components; using Content.Shared.DoAfter; +using Content.Shared.Gibbing; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Popups; @@ -34,7 +34,7 @@ public override void Initialize() SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnDevourAction); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnGibContents); + SubscribeLocalEvent(OnGibContents); } private void OnStartup(Entity ent, ref ComponentStartup args) @@ -110,7 +110,7 @@ private void OnDoAfter(Entity ent, ref DevourDoAfterEvent arg // Grant ichor if the devoured thing meets the dragon's food preference if (args.Args.Target != null && _whitelistSystem.IsWhitelistPassOrNull(ent.Comp.FoodPreferenceWhitelist, (EntityUid)args.Args.Target)) { - _bloodstreamSystem.TryAddToChemicals(ent.Owner, ichorInjection); + _bloodstreamSystem.TryAddToBloodstream(ent.Owner, ichorInjection); } // If the devoured thing meets the stomach whitelist criteria, add it to the stomach @@ -129,13 +129,11 @@ private void OnDoAfter(Entity ent, ref DevourDoAfterEvent arg _audioSystem.PlayPredicted(ent.Comp.SoundDevour, ent.Owner, ent.Owner); } - private void OnGibContents(Entity ent, ref BeingGibbedEvent args) + private void OnGibContents(Entity ent, ref GibbedBeforeDeletionEvent args) { if (ent.Comp.StomachStorageWhitelist == null) return; - // For some reason we have two different systems that should handle gibbing, - // and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process _containerSystem.EmptyContainer(ent.Comp.Stomach); } } diff --git a/Content.Shared/Dice/SharedDiceSystem.cs b/Content.Shared/Dice/SharedDiceSystem.cs index 71a51584d36..ed62355dca5 100644 --- a/Content.Shared/Dice/SharedDiceSystem.cs +++ b/Content.Shared/Dice/SharedDiceSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Examine; using Content.Shared.Interaction.Events; using Content.Shared.Popups; +using Content.Shared.Random.Helpers; using Content.Shared.Throwing; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; @@ -72,7 +73,7 @@ public void SetCurrentValue(Entity entity, int value) private void Roll(Entity entity, EntityUid? user = null) { - var rand = new System.Random((int)_timing.CurTick.Value); + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(entity)); var roll = rand.Next(1, entity.Comp.Sides + 1); SetCurrentSide(entity, roll); diff --git a/Content.Shared/DisplacementMap/DisplacementData.cs b/Content.Shared/DisplacementMap/DisplacementData.cs index 79f89a1d253..8bc922708f7 100644 --- a/Content.Shared/DisplacementMap/DisplacementData.cs +++ b/Content.Shared/DisplacementMap/DisplacementData.cs @@ -13,4 +13,7 @@ public sealed partial class DisplacementData [DataField] public string? ShaderOverride = "DisplacedDraw"; + + [DataField] + public string ShaderOverrideUnshaded = "DisplacedDrawUnshaded"; } diff --git a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs index 292cfd2b3cb..d4031b7d00f 100644 --- a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs +++ b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs @@ -453,7 +453,7 @@ public virtual bool CanInsert(EntityUid uid, DisposalUnitComponent component, En if (!storable && !HasComp(entity)) return false; - if (_whitelistSystem.IsBlacklistPass(component.Blacklist, entity) || + if (_whitelistSystem.IsWhitelistPass(component.Blacklist, entity) || _whitelistSystem.IsWhitelistFail(component.Whitelist, entity)) return false; diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs index 283a08df084..5e4b742e10f 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs @@ -2,18 +2,24 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; -using Content.Shared.Physics; -using Robust.Shared.Utility; +using Robust.Shared.Exceptions; +using Robust.Shared.Network; namespace Content.Shared.DoAfter; public abstract partial class SharedDoAfterSystem : EntitySystem { [Dependency] private readonly IDynamicTypeFactory _factory = default!; +#if EXCEPTION_TOLERANCE + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly IRuntimeLog _runtimeLog = default!; +#endif [Dependency] private readonly SharedGravitySystem _gravity = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly EntityQuery _handsQuery = default!; + private DoAfter[] _doAfters = Array.Empty(); public override void Update(float frameTime) @@ -21,13 +27,59 @@ public override void Update(float frameTime) base.Update(frameTime); var time = GameTiming.CurTime; - var xformQuery = GetEntityQuery(); - var handsQuery = GetEntityQuery(); var enumerator = EntityQueryEnumerator(); while (enumerator.MoveNext(out var uid, out var active, out var comp)) { - Update(uid, active, comp, time, xformQuery, handsQuery); + + try + { + Update(uid, active, comp, time); + } + // ReSharper disable once RedundantCatchClause +#if EXCEPTION_TOLERANCE + catch (Exception e) +#else + catch (Exception) +#endif + { +#if EXCEPTION_TOLERANCE + // Doafter in question failed to complete.. + // Doafters are kind of a critical game mechanic, so we specially handle failure. + _runtimeLog.LogException(e, $"{nameof(SharedDoAfterSystem)} on {ToPrettyString(uid)}"); + + if (_netManager.IsClient) + continue; // Move along, we can't cancel these ourselves and just need to not completely die. + + // Cancel all the doafters for this entity to avoid repeats. + // We don't try to remove them ourselves to keep the logic reasonable. + foreach (var (key, doAfter) in comp.DoAfters) + { + try + { + InternalCancel(doAfter, comp); + } + catch (Exception e2) + { + _runtimeLog.LogException(e2, $"{nameof(SharedDoAfterSystem)} failed to cleanup {doAfter} @ {key} while handling a failure."); + // REMARK: As written, InternalCancel will always do the necessary side effect of + // configuring the cancellation time. We need this side effect, so dear reader + // if you ever make it so InternalCancel can throw an exception before that + // happens, update this to set cancel time itself in a finally block. + // + // If the doafter is one using async, this CAN result in that task leaking forever. + // So we check that here, too. + if (comp.AwaitedDoAfters.Remove(doAfter.Index, out var tcs)) + { + tcs.TrySetCanceled(); + } + } + } +#else + throw; // No tolerance, just rethrow. +#endif + } + } } @@ -35,9 +87,7 @@ protected void Update( EntityUid uid, ActiveDoAfterComponent active, DoAfterComponent comp, - TimeSpan time, - EntityQuery xformQuery, - EntityQuery handsQuery) + TimeSpan time) { var dirty = false; @@ -70,7 +120,7 @@ protected void Update( continue; } - if (ShouldCancel(doAfter, xformQuery, handsQuery)) + if (ShouldCancel(doAfter)) { InternalCancel(doAfter, comp); dirty = true; @@ -112,7 +162,7 @@ private bool TryAttemptEvent(DoAfter doAfter) else RaiseLocalEvent(doAfter.AttemptEvent); - var ev = (CancellableEntityEventArgs) doAfter.AttemptEvent; + var ev = (CancellableEntityEventArgs)doAfter.AttemptEvent; if (!ev.Cancelled) return true; @@ -144,27 +194,24 @@ private void TryComplete(DoAfter doAfter, DoAfterComponent component) } } - private bool ShouldCancel(DoAfter doAfter, - EntityQuery xformQuery, - EntityQuery handsQuery) + private bool ShouldCancel(DoAfter doAfter) { var args = doAfter.Args; - //re-using xformQuery for Exists() checks. - if (args.Used is { } used && !xformQuery.HasComponent(used)) + if (args.Used is { } used && !Exists(used)) return true; - if (args.EventTarget is {Valid: true} eventTarget && !xformQuery.HasComponent(eventTarget)) + if (args.EventTarget is { Valid: true } eventTarget && !Exists(eventTarget)) return true; - if (!xformQuery.TryGetComponent(args.User, out var userXform)) + if (!TryComp(args.User, out TransformComponent? userXform)) return true; TransformComponent? targetXform = null; - if (args.Target is { } target && !xformQuery.TryGetComponent(target, out targetXform)) + if (args.Target is { } target && !TryComp(target, out targetXform)) return true; - if (args.Used is { } @using && !xformQuery.HasComp(@using)) + if (args.Used is { } @using && !Exists(@using)) return true; // TODO: Re-use existing xform query for these calculations. @@ -213,13 +260,13 @@ private bool ShouldCancel(DoAfter doAfter, // This does not mean their hand needs to be empty. if (args.NeedHand) { - if (!handsQuery.TryGetComponent(args.User, out var hands) || hands.Count == 0) + if (!_handsQuery.TryGetComponent(args.User, out var hands) || hands.Count == 0) return true; // If an item was in the user's hand to begin with, // check if the user is no longer holding the item. if (args.BreakOnDropItem && doAfter.InitialItem != null && !_hands.IsHolding((args.User, hands), doAfter.InitialItem)) - return true; + return true; // If the user changes which hand is active at all, interrupt the do-after if (args.BreakOnHandChange && hands.ActiveHandId != doAfter.InitialHand) diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index c1a3d6ecee4..8841d8dd268 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using Content.Shared.ActionBlocker; using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Hands.Components; +using Content.Shared.Interaction; using Content.Shared.Tag; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -34,6 +36,7 @@ public override void Initialize() SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnDoAfterGetState); SubscribeLocalEvent(OnDoAfterHandleState); + SubscribeLocalEvent(OnGetInteractingEntities); } private void OnUnpaused(EntityUid uid, DoAfterComponent component, ref EntityUnpausedEvent args) @@ -130,6 +133,25 @@ private void OnDoAfterHandleState(EntityUid uid, DoAfterComponent comp, ref Comp EnsureComp(uid); } + /// + /// Adds entities which have an active DoAfter matching the target. + /// + private void OnGetInteractingEntities(ref GetInteractingEntitiesEvent args) + { + var enumerator = EntityQueryEnumerator(); + while (enumerator.MoveNext(out _, out var comp)) + { + foreach (var doAfter in comp.DoAfters.Values) + { + if (doAfter.Cancelled || doAfter.Completed) + continue; + + if (doAfter.Args.Target == args.Target) + args.InteractingEntities.Add(doAfter.Args.User); + } + } + } + #region Creation /// /// Tasks that are delayed until the specified time has passed @@ -229,7 +251,7 @@ public bool TryStartDoAfter(DoAfterArgs args, [NotNullWhen(true)] out DoAfterId? doAfter.NetInitialItem = GetNetEntity(doAfter.InitialItem); // Initial checks - if (ShouldCancel(doAfter, GetEntityQuery(), GetEntityQuery())) + if (ShouldCancel(doAfter)) return false; if (args.AttemptFrequency == AttemptFrequency.StartAndEnd && !TryAttemptEvent(doAfter)) diff --git a/Content.Shared/Doors/Components/AirlockComponent.cs b/Content.Shared/Doors/Components/AirlockComponent.cs index 6b3fcfad7e4..136f49b4fc3 100644 --- a/Content.Shared/Doors/Components/AirlockComponent.cs +++ b/Content.Shared/Doors/Components/AirlockComponent.cs @@ -24,7 +24,7 @@ public sealed partial class AirlockComponent : Component [ViewVariables(VVAccess.ReadWrite)] [DataField, AutoNetworkedField] public bool EmergencyAccess = false; - + /// /// Sound to play when the airlock emergency access is turned on. /// @@ -83,6 +83,9 @@ public sealed partial class AirlockComponent : Component [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string AutoClosePort = "AutoClose"; + [DataField] + public LocId PryFailedPopup = "airlock-component-cannot-pry-is-powered-message"; + #region Graphics /// @@ -115,6 +118,13 @@ public sealed partial class AirlockComponent : Component [DataField] public string OpeningPanelSpriteState = "panel_opening"; + /// + /// The sprite state to use for the wire panel when the airlock is open. The + /// first frame will be used for when the airlock is closed. + /// + [DataField] + public string OpenPanelSpriteState = "panel_open"; + /// /// The sprite state used to animate the airlock frame when the airlock closes. /// diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index 964cd8716dd..7ee23f93f7e 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -140,7 +140,17 @@ public sealed partial class DoorComponent : Component /// /// The key used when playing door opening/closing/emagging/deny animations. /// - public const string AnimationKey = "door_animation"; + public const string OpenCloseKey = "door_animation_openclose"; + + /// + /// The key used when playing door deny animations. + /// + public const string DenyKey = "door_animation_deny"; + + /// + /// The key used when playing door emag animations. + /// + public const string EmagKey = "door_animation_emag"; /// /// The sprite state used for the door when it's open. @@ -153,7 +163,7 @@ public sealed partial class DoorComponent : Component /// The sprite states used for the door while it's open. /// [ViewVariables(VVAccess.ReadOnly)] - public List<(DoorVisualLayers, string)> OpenSpriteStates = default!; + public List<(Enum, string)> OpenSpriteStates = default!; /// /// The sprite state used for the door when it's closed. @@ -166,7 +176,7 @@ public sealed partial class DoorComponent : Component /// The sprite states used for the door while it's closed. /// [ViewVariables(VVAccess.ReadOnly)] - public List<(DoorVisualLayers, string)> ClosedSpriteStates = default!; + public List<(Enum, string)> ClosedSpriteStates = default!; /// /// The sprite state used for the door when it's opening. @@ -325,4 +335,5 @@ public enum DoorVisualLayers : byte BaseUnlit, BaseBolted, BaseEmergencyAccess, + BaseEmagging, } diff --git a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs index 5bbde04aedb..23c8c463e6b 100644 --- a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs +++ b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs @@ -1,6 +1,9 @@ +using Content.Shared.DeviceLinking.Events; using Content.Shared.Doors.Components; +using Content.Shared.Interaction; using Robust.Shared.Audio.Systems; using Content.Shared.Popups; +using Content.Shared.Power; using Content.Shared.Prying.Components; using Content.Shared.Wires; using Robust.Shared.Timing; @@ -27,66 +30,69 @@ public override void Initialize() SubscribeLocalEvent(OnBeforeDoorDenied); SubscribeLocalEvent(OnGetPryMod); SubscribeLocalEvent(OnBeforePry); + SubscribeLocalEvent(OnSignalReceived); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnActivate, before: new[] { typeof(SharedDoorSystem) }); } - private void OnBeforeDoorClosed(EntityUid uid, AirlockComponent airlock, BeforeDoorClosedEvent args) + private void OnBeforeDoorClosed(Entity ent, ref BeforeDoorClosedEvent args) { if (args.Cancelled) return; - if (!airlock.Safety) + if (!ent.Comp.Safety) args.PerformCollisionCheck = false; // only block based on bolts / power status when initially closing the door, not when its already // mid-transition. Particularly relevant for when the door was pried-closed with a crowbar, which bypasses // the initial power-check. - if (TryComp(uid, out DoorComponent? door) + if (HasComp(ent) && !args.Partial - && !CanChangeState(uid, airlock)) + && !CanChangeState(ent)) { args.Cancel(); } } - private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args) + private void OnStateChanged(Entity ent, ref DoorStateChangedEvent args) { // This is here so we don't accidentally bulldoze state values and mispredict. if (_timing.ApplyingState) return; // Only show the maintenance panel if the airlock is closed - if (TryComp(uid, out var wiresPanel)) + if (TryComp(ent, out var wiresPanel)) { - _wiresSystem.ChangePanelVisibility(uid, wiresPanel, component.OpenPanelVisible || args.State != DoorState.Open); + _wiresSystem.ChangePanelVisibility(ent, wiresPanel, ent.Comp.OpenPanelVisible || args.State != DoorState.Open); } // If the door is closed, we should look if the bolt was locked while closing - UpdateAutoClose(uid, component); + UpdateAutoClose((ent, ent.Comp)); // Make sure the airlock auto closes again next time it is opened if (args.State == DoorState.Closed) { - component.AutoClose = true; - Dirty(uid, component); + ent.Comp.AutoClose = true; + Dirty(ent); } } - private void OnBoltsChanged(EntityUid uid, AirlockComponent component, DoorBoltsChangedEvent args) + private void OnBoltsChanged(Entity ent, ref DoorBoltsChangedEvent args) { // If unbolted, reset the auto close timer if (!args.BoltsDown) - UpdateAutoClose(uid, component); + UpdateAutoClose((ent, ent.Comp)); } - private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args) + private void OnBeforeDoorOpened(Entity ent, ref BeforeDoorOpenedEvent args) { - if (!CanChangeState(uid, component)) + if (!CanChangeState(ent)) args.Cancel(); } - private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args) + private void OnBeforeDoorDenied(Entity ent, ref BeforeDoorDeniedEvent args) { - if (!CanChangeState(uid, component)) + if (!CanChangeState(ent)) args.Cancel(); } @@ -102,44 +108,86 @@ private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTi /// /// Updates the auto close timer. /// - public void UpdateAutoClose(EntityUid uid, AirlockComponent? airlock = null, DoorComponent? door = null) + public void UpdateAutoClose(Entity ent) { - if (!Resolve(uid, ref airlock, ref door)) + if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2)) return; - if (door.State != DoorState.Open) + if (ent.Comp2.State != DoorState.Open) return; - if (!airlock.AutoClose) + if (!ent.Comp1.AutoClose) return; - if (!CanChangeState(uid, airlock)) + if (!CanChangeState((ent.Owner, ent.Comp1))) return; var autoev = new BeforeDoorAutoCloseEvent(); - RaiseLocalEvent(uid, autoev); + RaiseLocalEvent(ent, autoev); if (autoev.Cancelled) return; - DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier); + DoorSystem.SetNextStateChange(ent, ent.Comp1.AutoCloseDelay * ent.Comp1.AutoCloseDelayModifier); } - private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args) + private void OnBeforePry(Entity ent, ref BeforePryEvent args) { if (args.Cancelled) return; - if (!component.Powered || args.PryPowered) + if (!ent.Comp.Powered || args.PryPowered) return; - args.Message = "airlock-component-cannot-pry-is-powered-message"; + args.Message = ent.Comp.PryFailedPopup; args.Cancelled = true; } - public void UpdateEmergencyLightStatus(EntityUid uid, AirlockComponent component) + private void OnSignalReceived(Entity ent, ref SignalReceivedEvent args) { - Appearance.SetData(uid, DoorVisuals.EmergencyLights, component.EmergencyAccess); + if (args.Port == ent.Comp.AutoClosePort && ent.Comp.AutoClose) + { + ent.Comp.AutoClose = false; + Dirty(ent); + } + } + + private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) + { + ent.Comp.Powered = args.Powered; + Dirty(ent); + + if (!TryComp(ent, out DoorComponent? door)) + return; + + if (!args.Powered) + { + // stop any scheduled auto-closing + if (door.State == DoorState.Open) + DoorSystem.SetNextStateChange(ent, null); + } + else + { + UpdateAutoClose((ent, ent.Comp, door)); + } + } + + private void OnActivate(Entity ent, ref ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + if (ent.Comp.KeepOpenIfClicked && ent.Comp.AutoClose) + { + // Disable auto close + ent.Comp.AutoClose = false; + Dirty(ent); + } + } + + public void UpdateEmergencyLightStatus(Entity ent) + { + Appearance.SetData(ent, DoorVisuals.EmergencyLights, ent.Comp.EmergencyAccess); } public void SetEmergencyAccess(Entity ent, bool value, EntityUid? user = null, bool predicted = false) @@ -152,7 +200,7 @@ public void SetEmergencyAccess(Entity ent, bool value, EntityU ent.Comp.EmergencyAccess = value; Dirty(ent, ent.Comp); // This only runs on the server apparently so we need this. - UpdateEmergencyLightStatus(ent, ent.Comp); + UpdateEmergencyLightStatus(ent); var sound = ent.Comp.EmergencyAccess ? ent.Comp.EmergencyOnSound : ent.Comp.EmergencyOffSound; if (predicted) @@ -174,8 +222,8 @@ public void SetSafety(AirlockComponent component, bool value) component.Safety = value; } - public bool CanChangeState(EntityUid uid, AirlockComponent component) + public bool CanChangeState(Entity ent) { - return component.Powered && !DoorSystem.IsBolted(uid); + return ent.Comp.Powered && !DoorSystem.IsBolted(ent); } } diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index 7f0ca5ac479..bacf703a2f1 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -2,7 +2,7 @@ using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Administration.Logs; -using Content.Shared.Damage; +using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Doors.Components; using Content.Shared.Emag.Systems; @@ -123,7 +123,7 @@ private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent ar if (!TryComp(uid, out var airlock)) return; - if (IsBolted(uid) || !airlock.Powered) + if (!airlock.Powered) return; if (door.State != DoorState.Closed) @@ -391,7 +391,7 @@ public void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? u Audio.PlayPvs(door.OpenSound, uid, AudioParams.Default.WithVolume(-5)); if (lastState == DoorState.Emagging && TryComp(uid, out var doorBoltComponent)) - SetBoltsDown((uid, doorBoltComponent), !doorBoltComponent.BoltsDown, user, true); + SetBoltsDown((uid, doorBoltComponent), true, user, true); } /// diff --git a/Content.Shared/DrawDepth/DrawDepth.cs b/Content.Shared/DrawDepth/DrawDepth.cs index 35fead5b3fd..288602353c8 100644 --- a/Content.Shared/DrawDepth/DrawDepth.cs +++ b/Content.Shared/DrawDepth/DrawDepth.cs @@ -112,17 +112,22 @@ public enum DrawDepth /// Overdoors = DrawDepthTag.Default + 10, + /// + /// Visible atmos gas. + /// + Gasses = DrawDepthTag.Default + 11, + /// /// Explosions, fire, melee swings. Whatever. /// - Effects = DrawDepthTag.Default + 11, + Effects = DrawDepthTag.Default + 12, - Ghosts = DrawDepthTag.Default + 12, + Ghosts = DrawDepthTag.Default + 13, /// /// Use this selectively if it absolutely needs to be drawn above (almost) everything else. Examples include /// the pointing arrow, the drag & drop ghost-entity, and some debug tools. /// - Overlays = DrawDepthTag.Default + 13, + Overlays = DrawDepthTag.Default + 14, } } diff --git a/Content.Shared/Drunk/SharedDrunkSystem.cs b/Content.Shared/Drunk/SharedDrunkSystem.cs index 96aff82fa04..9faeb9419d3 100644 --- a/Content.Shared/Drunk/SharedDrunkSystem.cs +++ b/Content.Shared/Drunk/SharedDrunkSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Speech.EntitySystems; using Content.Shared.StatusEffectNew; using Content.Shared.Traits.Assorted; using Robust.Shared.Prototypes; @@ -8,12 +7,6 @@ namespace Content.Shared.Drunk; public abstract class SharedDrunkSystem : EntitySystem { public static EntProtoId Drunk = "StatusEffectDrunk"; - public static EntProtoId Woozy = "StatusEffectWoozy"; - - /* I have no clue why this magic number was chosen, I copied it from slur system and needed it for the overlay - If you have a more intelligent magic number be my guest to completely explode this value. - There were no comments as to why this value was chosen three years ago. */ - public static float MagicNumber = 1100f; [Dependency] protected readonly StatusEffectsSystem Status = default!; diff --git a/Content.Shared/Electrocution/Components/ElectrifiedComponent.cs b/Content.Shared/Electrocution/Components/ElectrifiedComponent.cs index 1de5b47c57b..c780a263dea 100644 --- a/Content.Shared/Electrocution/Components/ElectrifiedComponent.cs +++ b/Content.Shared/Electrocution/Components/ElectrifiedComponent.cs @@ -112,10 +112,10 @@ public sealed partial class ElectrifiedComponent : Component public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks"); [DataField, AutoNetworkedField] - public SoundPathSpecifier AirlockElectrifyDisabled = new("/Audio/Machines/airlock_electrify_on.ogg"); + public SoundPathSpecifier AirlockElectrifyDisabled = new("/Audio/Machines/airlock_electrify_off.ogg"); [DataField, AutoNetworkedField] - public SoundPathSpecifier AirlockElectrifyEnabled = new("/Audio/Machines/airlock_electrify_off.ogg"); + public SoundPathSpecifier AirlockElectrifyEnabled = new("/Audio/Machines/airlock_electrify_on.ogg"); [DataField, AutoNetworkedField] public bool PlaySoundOnShock = true; diff --git a/Content.Shared/Electrocution/InsulatedSystem.cs b/Content.Shared/Electrocution/InsulatedSystem.cs new file mode 100644 index 00000000000..1e6ec6a1491 --- /dev/null +++ b/Content.Shared/Electrocution/InsulatedSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Examine; +using Content.Shared.Verbs; + +namespace Content.Shared.Electrocution; + +public sealed class InsulatedSystem : EntitySystem +{ + [Dependency] private readonly ExamineSystemShared _examine = default!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnDetailedExamine); + } + + private void OnDetailedExamine(EntityUid ent, InsulatedComponent component, ref GetVerbsEvent args) + { + if (!HasComp(ent)) + return; + + var iconTexture = "/Textures/Interface/VerbIcons/zap.svg.192dpi.png"; + + _examine.AddHoverExamineVerb(args, + component, + Loc.GetString("insulated-examinable-verb-text"), + Loc.GetString("insulated-examinable-verb-text-message"), + iconTexture + ); + } +} diff --git a/Content.Shared/Emoting/EmoteEvents.cs b/Content.Shared/Emoting/EmoteEvents.cs index ea3073f336e..931395ee7e1 100644 --- a/Content.Shared/Emoting/EmoteEvents.cs +++ b/Content.Shared/Emoting/EmoteEvents.cs @@ -1,27 +1,6 @@ -using Content.Shared.Chat.Prototypes; -using Content.Shared.Inventory; - -namespace Content.Shared.Emoting; +namespace Content.Shared.Emoting; public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs { public EntityUid Uid { get; } = uid; } - -/// -/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance. -/// -[ByRefEvent] -public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote) - : CancellableEntityEventArgs, IInventoryRelayEvent -{ - public readonly EntityUid Source = source; - public readonly EmotePrototype Emote = emote; - - /// - /// The equipment that is blocking emoting. Should only be non-null if the event was canceled. - /// - public EntityUid? Blocker = null; - - public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; -} diff --git a/Content.Shared/Emp/EmpDisabledComponent.cs b/Content.Shared/Emp/EmpDisabledComponent.cs index 9e5a56de837..71748c0be3f 100644 --- a/Content.Shared/Emp/EmpDisabledComponent.cs +++ b/Content.Shared/Emp/EmpDisabledComponent.cs @@ -5,24 +5,30 @@ namespace Content.Shared.Emp; /// /// While entity has this component it is "disabled" by EMP. -/// Add desired behaviour in other systems +/// Add desired behaviour in other systems. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState, AutoGenerateComponentPause] [Access(typeof(SharedEmpSystem))] public sealed partial class EmpDisabledComponent : Component { /// - /// Moment of time when component is removed and entity stops being "disabled" + /// Moment of time when the component is removed and entity stops being "disabled". /// - [DataField("timeLeft", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - [AutoPausedField] - public TimeSpan DisabledUntil; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan DisabledUntil = TimeSpan.Zero; - [DataField("effectCoolDown"), ViewVariables(VVAccess.ReadWrite)] - public float EffectCooldown = 3f; + /// + /// Default time between visual effect spawns. + /// This gets a random multiplier. + /// + [DataField, AutoNetworkedField] + public TimeSpan EffectCooldown = TimeSpan.FromSeconds(3); /// - /// When next effect will be spawned + /// When next effect will be spawned. + /// TODO: Particle system. /// [AutoPausedField] public TimeSpan TargetTime = TimeSpan.Zero; diff --git a/Content.Shared/Emp/EmpResistanceComponent.cs b/Content.Shared/Emp/EmpResistanceComponent.cs new file mode 100644 index 00000000000..9f83a1ad7b6 --- /dev/null +++ b/Content.Shared/Emp/EmpResistanceComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Emp; + +/// +/// An entity with this component resists or is fully immune to EMPs. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedEmpSystem))] +public sealed partial class EmpResistanceComponent : Component +{ + /// + /// The strength of the EMP gets multiplied by this value. + /// + [DataField, AutoNetworkedField] + public float StrengthMultiplier = 1f; + + /// + /// The duration of the EMP gets multiplied by this value. + /// + [DataField, AutoNetworkedField] + public float DurationMultiplier = 1f; +} diff --git a/Content.Shared/Emp/SharedEmpSystem.cs b/Content.Shared/Emp/SharedEmpSystem.cs index deb2afd7092..0c902798b15 100644 --- a/Content.Shared/Emp/SharedEmpSystem.cs +++ b/Content.Shared/Emp/SharedEmpSystem.cs @@ -1,21 +1,63 @@ using Content.Shared.Examine; +using Content.Shared.Rejuvenate; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Timing; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Shared.Emp; public abstract class SharedEmpSystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + [Dependency] private readonly EntityQuery _resistanceQuery = default!; + + private HashSet _entSet = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnRejuvenate); + + SubscribeLocalEvent(OnResistEmpAttempt); } - protected const string EmpDisabledEffectPrototype = "EffectEmpDisabled"; + public static readonly EntProtoId EmpPulseEffectPrototype = "EffectEmpPulse"; + public static readonly EntProtoId EmpDisabledEffectPrototype = "EffectEmpDisabled"; + public static readonly SoundSpecifier EmpSound = new SoundPathSpecifier("/Audio/Effects/Lightning/lightningbolt.ogg"); + + /// + /// Triggers an EMP pulse at the given location, by first raising an , then by raising on all entities in range. + /// + /// The location to trigger the EMP pulse at. + /// The range of the EMP pulse. + /// The amount of energy consumed by the EMP pulse. In Joule. + /// The duration of the EMP effects. + /// The player that caused the effect. Used for predicted audio. + public void EmpPulse(MapCoordinates mapCoordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null) + { + foreach (var uid in _lookup.GetEntitiesInRange(mapCoordinates, range)) + { + TryEmpEffects(uid, energyConsumption, duration, user); + } + // TODO: replace with PredictedSpawn once it works with animated sprites + if (_net.IsServer) + Spawn(EmpPulseEffectPrototype, mapCoordinates); + + var coordinates = _transform.ToCoordinates(mapCoordinates); + _audio.PlayPredicted(EmpSound, coordinates, user); + } /// /// Triggers an EMP pulse at the given location, by first raising an , then a raising on all entities in range. @@ -24,12 +66,138 @@ public override void Initialize() /// The range of the EMP pulse. /// The amount of energy consumed by the EMP pulse. /// The duration of the EMP effects. - public virtual void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration) + /// The player that caused the effect. Used for predicted audio. + /// Whether this pulse is being replicated on the client. + public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, TimeSpan duration, EntityUid? user = null, bool predicted = true) + { + _entSet.Clear(); + _lookup.GetEntitiesInRange(coordinates, range, _entSet); + foreach (var uid in _entSet) + { + TryEmpEffects(uid, energyConsumption, duration, user); + } + // TODO: replace with PredictedSpawn once it works with animated sprites + if (_net.IsServer) + Spawn(EmpPulseEffectPrototype, coordinates); + + if (predicted) + _audio.PlayPredicted(EmpSound, coordinates, user); + else + _audio.PlayPvs(EmpSound, coordinates); + } + + /// + /// Attempts to apply the effects of an EMP pulse onto an entity by first raising an , followed by raising a on it. + /// + /// The entity to apply the EMP effects on. + /// The amount of energy consumed by the EMP. + /// The duration of the EMP effects. + /// The player that caused the EMP. For prediction purposes. + /// If the entity was affected by the EMP. + public bool TryEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null) + { + var attemptEv = new EmpAttemptEvent(); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) + return false; + + return DoEmpEffects(uid, energyConsumption, duration, user); + } + + /// + /// Applies the effects of an EMP pulse onto an entity by raising a on it. + /// + /// The entity to apply the EMP effects on. + /// The amount of energy consumed by the EMP. + /// The duration of the EMP effects. + /// The player that caused the EMP. For prediction purposes. + /// If the entity was affected by the EMP. + public bool DoEmpEffects(EntityUid uid, float energyConsumption, TimeSpan duration, EntityUid? user = null) + { + var strMultiplier = 1f; + var durMultiplier = 1f; + if (_resistanceQuery.TryComp(uid, out var resistance)) + { + strMultiplier = resistance.StrengthMultiplier; + durMultiplier = resistance.DurationMultiplier; + } + var ev = new EmpPulseEvent(energyConsumption * strMultiplier, false, false, duration * durMultiplier, user); + RaiseLocalEvent(uid, ref ev); + + // TODO: replace with PredictedSpawn once it works with animated sprites + if (ev.Affected && _net.IsServer) + Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates); + + if (!ev.Disabled) + return ev.Affected; + + var disabled = EnsureComp(uid); + disabled.DisabledUntil = Timing.CurTime + duration * durMultiplier; + Dirty(uid, disabled); + + return ev.Affected; + } + + public override void Update(float frameTime) { + base.Update(frameTime); + + var curTime = Timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (curTime < comp.DisabledUntil) + continue; + + RemComp(uid); + } } private void OnExamine(Entity ent, ref ExaminedEvent args) { args.PushMarkup(Loc.GetString("emp-disabled-comp-on-examine")); } + + private void OnRemove(Entity ent, ref ComponentRemove args) + { + var ev = new EmpDisabledRemovedEvent(); + RaiseLocalEvent(ent, ref ev); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + RemCompDeferred(ent); + } + + private void OnResistEmpAttempt(Entity ent, ref EmpAttemptEvent args) + { + // We only cancel if the strength multiplier is 0, because then the effect basically doesn't exist. + // Allows us to make things resistant to the duration, but still lose charge to the EMP. + if (ent.Comp.StrengthMultiplier <= 0) + args.Cancelled = true; + } } + +/// +/// Raised on an entity before . Cancel this to prevent the emp event being raised. +/// +[ByRefEvent] +public record struct EmpAttemptEvent(bool Cancelled); + +/// +/// Raised on an entity when it gets hit by an EMP Pulse. +/// +/// The amount of energy to remove from batteries. In Joule. +/// Set this is true in the subscription to spawn a visual effect at the entity's location. +/// Set this to ture in the subscription to add to the entity. +/// The duration the entity will be disabled. +/// The player that caused the EMP. For prediction purposes. + +[ByRefEvent] +public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration, EntityUid? User); + +/// +/// Raised on an entity after is removed. +/// +[ByRefEvent] +public record struct EmpDisabledRemovedEvent(); diff --git a/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs b/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs new file mode 100644 index 00000000000..ece979a6a85 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/BreathingEntityCondition.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +public sealed partial class BreathingCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-breathing", ("isBreathing", !Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs new file mode 100644 index 00000000000..f00707f9078 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity's hunger is within a specified minimum and maximum. +/// +/// +public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var total = _hunger.GetHunger(entity.Comp); + args.Result = total >= args.Condition.Min && total <= args.Condition.Max; + } +} + +/// +public sealed partial class HungerCondition : EntityConditionBase +{ + [DataField] + public float Min; + + [DataField] + public float Max = float.PositiveInfinity; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-total-hunger", ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), ("min", Min)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs new file mode 100644 index 00000000000..cabf731939c --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/InternalsEntityConditionSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Body.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity is using internals. False if they are not or cannot use internals. +/// +/// +public sealed partial class InternalsOnEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = entity.Comp.GasTankEntity != null; + } +} + +/// +public sealed partial class InternalsCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-internals", ("usingInternals", !Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityConditionSystem.cs new file mode 100644 index 00000000000..bfbd431659b --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/MetabolizerTypeEntityConditionSystem.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Content.Shared.Localizations; +using Content.Shared.Metabolism; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +public sealed partial class MetabolizerTypeCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Type = default!; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var typeList = new List(); + + foreach (var type in Type) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + typeList.Add(proto.LocalizedName); + } + + var names = ContentLocalizationManager.FormatListToOr(typeList); + + return Loc.GetString("entity-condition-guidebook-organ-type", + ("name", names), + ("shouldhave", !Inverted)); + } +} + +/// +/// Returns true if this entity has any of the listed metabolizer types. +/// +/// +public sealed partial class MetabolizerTypeEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.MetabolizerTypes == null) + return; + + args.Result = entity.Comp.MetabolizerTypes.Overlaps(args.Condition.Type); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs new file mode 100644 index 00000000000..735720c0ea4 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/MobStateEntityConditionSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// Returns true if this entity's current mob state matches the condition's specified mob state. +/// +/// +public sealed partial class MobStateEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.CurrentState == args.Condition.Mobstate) + args.Result = true; + } +} + +/// +public sealed partial class MobStateCondition : EntityConditionBase +{ + [DataField] + public MobState Mobstate = MobState.Alive; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-mob-state-condition", ("state", Mobstate)); +} diff --git a/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs new file mode 100644 index 00000000000..a67f5b1c8cb --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs @@ -0,0 +1,41 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its damage of a given damage group is within a specified minimum and maximum. +/// +/// +public sealed partial class DamageGroupEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var value = _damageableSystem.GetDamagePerGroup(entity.Owner)[args.Condition.DamageGroup]; + args.Result = value >= args.Condition.Min && value <= args.Condition.Max; + } +} + +/// +public sealed partial class DamageGroupCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField(required: true)] + public ProtoId DamageGroup; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-group-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float()), + ("type", prototype.Index(DamageGroup).LocalizedName)); +} diff --git a/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs new file mode 100644 index 00000000000..dcf96ad51a6 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its damage of a given damage type is within a specified minimum and maximum. +/// +/// +public sealed partial class DamageTypeEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var value = _damageableSystem.GetAllDamage(entity.Owner).DamageDict.GetValueOrDefault(args.Condition.DamageType); + args.Result = value >= args.Condition.Min && value <= args.Condition.Max; + } +} + +/// +public sealed partial class DamageTypeCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField(required: true)] + public ProtoId DamageType; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-type-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float()), + ("type", prototype.Index(DamageType).LocalizedName)); +} diff --git a/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs new file mode 100644 index 00000000000..a698efa0329 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/JobEntityConditionSystem.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Content.Shared.Localizations; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Roles; +using Content.Shared.Roles.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity has any of the specified jobs. False if the entity has no mind, none of the specified jobs, or is jobless. +/// +/// +public sealed partial class HasJobEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + // We need a mind in our mind container... + if (!TryComp(entity.Comp.Mind, out var mind)) + return; + + foreach (var roleId in mind.MindRoleContainer.ContainedEntities) + { + if (!HasComp(roleId)) + continue; + + if (!TryComp(roleId, out var mindRole)) + { + Log.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}"); + continue; + } + + if (mindRole.JobPrototype == null) + { + Log.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}"); + continue; + } + + if (!args.Condition.Jobs.Contains(mindRole.JobPrototype.Value)) + continue; + + args.Result = true; + return; + } + } +} + +/// +public sealed partial class JobCondition : EntityConditionBase +{ + [DataField(required: true)] public List> Jobs = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var localizedNames = Jobs.Select(jobId => prototype.Index(jobId).LocalizedName).ToList(); + return Loc.GetString("entity-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames))); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/NestedCondition.cs b/Content.Shared/EntityConditions/Conditions/NestedCondition.cs new file mode 100644 index 00000000000..2af0d0d0e73 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/NestedCondition.cs @@ -0,0 +1,31 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Uses the conditions of an . +/// +public sealed partial class NestedCondition : EntityConditionBase +{ + /// + /// The condition prototype to use. + /// + [DataField(required: true)] + public ProtoId Proto; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + => prototype.Index(Proto).Condition.EntityConditionGuidebookText(prototype); +} + +/// +/// Handles . +/// +public sealed class NestedConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly SharedEntityConditionsSystem _conditions = default!; + + protected override void Condition(Entity ent, ref EntityConditionEvent args) + { + args.Result = _conditions.TryCondition(ent, args.Condition.Proto); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs new file mode 100644 index 00000000000..6477d9e2bc8 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs @@ -0,0 +1,46 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class ReagentEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var soln = entity.Comp.Solution; + var quant = soln.GetTotalPrototypeQuantity(args.Condition.Reagent); + + args.Result = quant >= args.Condition.Min && quant <= args.Condition.Max; + } +} + +/// +public sealed partial class ReagentCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField(required: true)] + public ProtoId Reagent; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + // Guard empty ProtoId — Resolve throws ArgumentNullException on null/empty keys. + if (string.IsNullOrEmpty(Reagent.Id) || !prototype.Resolve(Reagent, out var reagentProto)) + return String.Empty; + + return Loc.GetString("entity-condition-guidebook-reagent-threshold", + ("reagent", reagentProto.LocalizedName), + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float())); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs new file mode 100644 index 00000000000..36f4f51bf5d --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasAllTagsEntityConditionSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Localizations; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity has all the listed tags. +/// +/// +public sealed partial class HasAllTagsEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasAllTags(entity.Comp, args.Condition.Tags); + } +} + +/// +public sealed partial class AllTagsCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Tags = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var tagList = new List(); + + foreach (var type in Tags) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + tagList.Add(proto.ID); + } + + var names = ContentLocalizationManager.FormatList(tagList); + + return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted)); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs new file mode 100644 index 00000000000..dedce491fdf --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasAnyTagEntityConditionSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Localizations; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity have any of the listed tags. +/// +/// +public sealed partial class HasAnyTagEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasAnyTag(entity.Comp, args.Condition.Tags); + } +} + +/// +public sealed partial class AnyTagCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId[] Tags = []; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + var tagList = new List(); + + foreach (var type in Tags) + { + if (!prototype.Resolve(type, out var proto)) + continue; + + tagList.Add(proto.ID); + } + + var names = ContentLocalizationManager.FormatListToOr(tagList); + + return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted)); + } +} diff --git a/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs new file mode 100644 index 00000000000..6145982a030 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Tags/HasTagEntityConditionSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Tags; + +/// +/// Returns true if this entity has the listed tag. +/// +/// +public sealed partial class HasTagEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly TagSystem _tag = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + args.Result = _tag.HasTag(entity.Comp, args.Condition.Tag); + } +} + +/// +public sealed partial class TagCondition : EntityConditionBase +{ + [DataField(required: true)] + public ProtoId Tag; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-has-tag", ("tag", Tag), ("invert", Inverted)); +} diff --git a/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs new file mode 100644 index 00000000000..f87bbecf8c2 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TemperatureEntityConditionSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Temperature.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class TemperatureEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.CurrentTemperature >= args.Condition.Min && entity.Comp.CurrentTemperature <= args.Condition.Max) + args.Result = true; + } +} + +/// +/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum. +/// +/// +public sealed partial class SolutionTemperatureEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + if (entity.Comp.Solution.Temperature >= args.Condition.Min && entity.Comp.Solution.Temperature <= args.Condition.Max) + args.Result = true; + } +} + +/// +public sealed partial class TemperatureCondition : EntityConditionBase +{ + /// + /// Minimum allowed temperature + /// + [DataField] + public float Min = 0; + + /// + /// Maximum allowed temperature + /// + [DataField] + public float Max = float.PositiveInfinity; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-body-temperature", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); +} diff --git a/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs new file mode 100644 index 00000000000..fbb659b54f4 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TemplateEntityConditionSystem.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; +/// +/// A basic summary of this condition. +/// +/// +public sealed partial class TemplateEntityConditionSystem : EntityConditionSystem +{ + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + // Condition goes here. + } +} + +/// +public sealed partial class TemplateCondition : EntityConditionBase +{ + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => String.Empty; +} diff --git a/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs new file mode 100644 index 00000000000..a25f89cb608 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/TotalDamageEntityConditionSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions; + +/// +/// Returns true if this entity can take damage and if its total damage is within a specified minimum and maximum. +/// +/// +public sealed partial class TotalDamageEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + + protected override void Condition(Entity entity, ref EntityConditionEvent args) + { + var total = _damageableSystem.GetTotalDamage(entity.Owner); + args.Result = total >= args.Condition.Min && total <= args.Condition.Max; + } +} + +/// +public sealed partial class TotalDamageCondition : EntityConditionBase +{ + [DataField] + public FixedPoint2 Max = FixedPoint2.MaxValue; + + [DataField] + public FixedPoint2 Min = FixedPoint2.Zero; + + public override string EntityConditionGuidebookText(IPrototypeManager prototype) => + Loc.GetString("entity-condition-guidebook-total-damage", + ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()), + ("min", Min.Float())); +} diff --git a/Content.Shared/EntityConditions/EntityConditionPrototype.cs b/Content.Shared/EntityConditions/EntityConditionPrototype.cs new file mode 100644 index 00000000000..dc130d03df7 --- /dev/null +++ b/Content.Shared/EntityConditions/EntityConditionPrototype.cs @@ -0,0 +1,20 @@ +using Content.Shared.EntityConditions.Conditions; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions; + +/// +/// A prototype for entity conditions which can be reused via . +/// +[Prototype] +public sealed partial class EntityConditionPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = string.Empty; + + /// + /// The condition of this prototype. + /// + [DataField(required: true)] + public EntityCondition Condition = default!; +} diff --git a/Content.Shared/EntityConditions/SharedEntityConditionsSystem.Nested.cs b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.Nested.cs new file mode 100644 index 00000000000..72f84881b0d --- /dev/null +++ b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.Nested.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions; + +/// +/// Entity condition API counterpart using instead of . +/// +public sealed partial class SharedEntityConditionsSystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + + /// + /// TryCondition overload that uses a instead of . + /// + public bool TryCondition(EntityUid target, [ForbidLiteral] ProtoId id) + { + var proto = _proto.Index(id); + return TryCondition(target, proto.Condition); + } +} diff --git a/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs new file mode 100644 index 00000000000..f15b5acb4d4 --- /dev/null +++ b/Content.Shared/EntityConditions/SharedEntityConditionsSystem.cs @@ -0,0 +1,152 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions; + +/// +/// This handles entity effects. +/// Specifically it handles the receiving of events for causing entity effects, and provides +/// public API for other systems to take advantage of entity effects. +/// +public sealed partial class SharedEntityConditionsSystem : EntitySystem, IEntityConditionRaiser +{ + /// + /// Checks a list of conditions to verify that they all return true. + /// + /// Target entity we're checking conditions on + /// Conditions we're checking + /// Returns true if all conditions return true, false if any fail + public bool TryConditions(EntityUid target, EntityCondition[]? conditions) + { + // If there's no conditions we can't fail any of them... + if (conditions == null) + return true; + + foreach (var condition in conditions) + { + if (!TryCondition(target, condition)) + return false; + } + + return true; + } + + /// + /// Checks a list of conditions to see if any are true. + /// + /// Target entity we're checking conditions on + /// Conditions we're checking + /// Returns true if any conditions return true + public bool TryAnyCondition(EntityUid target, EntityCondition[]? conditions) + { + // If there's no conditions we can't meet any of them... + if (conditions == null) + return false; + + foreach (var condition in conditions) + { + if (TryCondition(target, condition)) + return true; + } + + return false; + } + + /// + /// Checks a single on an entity. + /// + /// Target entity we're checking conditions on + /// Condition we're checking + /// Returns true if we meet the condition and false otherwise + public bool TryCondition(EntityUid target, EntityCondition condition) + { + return condition.Inverted != condition.RaiseEvent(target, this); + } + + /// + /// Raises a condition to an entity. You should not be calling this unless you know what you're doing. + /// + public bool RaiseConditionEvent(EntityUid target, T effect) where T : EntityConditionBase + { + var effectEv = new EntityConditionEvent(effect); + RaiseLocalEvent(target, ref effectEv); + return effectEv.Result; + } +} + +/// +/// This is a basic abstract entity effect containing all the data an entity effect needs to affect entities with effects... +/// +/// The Component that is required for the effect +/// The Condition we're testing +public abstract partial class EntityConditionSystem : EntitySystem where T : Component where TCon : EntityConditionBase +{ + /// + public override void Initialize() + { + SubscribeLocalEvent>(Condition); + } + protected abstract void Condition(Entity entity, ref EntityConditionEvent args); +} + +/// +/// Used to raise an EntityCondition without losing the type of condition. +/// +public interface IEntityConditionRaiser +{ + bool RaiseConditionEvent(EntityUid target, T effect) where T : EntityConditionBase; +} + +/// +/// Used to store an so it can be raised without losing the type of the condition. +/// +/// The Condition wer are raising. +public abstract partial class EntityConditionBase : EntityCondition where T : EntityConditionBase +{ + public override bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser) + { + if (this is not T type) + return false; + + // If the result of the event matches the result we're looking for then we pass. + return raiser.RaiseConditionEvent(target, type); + } +} + +/// +/// A basic condition which can be checked for on an entity via events. +/// +[ImplicitDataDefinitionForInheritors] +public abstract partial class EntityCondition +{ + public abstract bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser); + + /// + /// If true, invert the result. So false returns true and true returns false! + /// + [DataField] + public bool Inverted; + + /// + /// A basic description of this condition, which displays in the guidebook. + /// + public abstract string EntityConditionGuidebookText(IPrototypeManager prototype); +} + +/// +/// An Event carrying an entity effect. +/// +/// The Condition we're checking +[ByRefEvent] +public record struct EntityConditionEvent(T Condition) where T : EntityConditionBase +{ + /// + /// The result of our check, defaults to false if nothing handles it. + /// + [DataField] + public bool Result; + + /// + /// The Condition being raised in this event + /// + public readonly T Condition = Condition; +} diff --git a/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs b/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs deleted file mode 100644 index 351e4ee12c5..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/BodyTemperature.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires the target entity to be above or below a certain temperature. -/// Used for things like cryoxadone and pyroxadone. -/// -public sealed partial class Temperature : EventEntityEffectCondition -{ - [DataField] - public float Min = 0; - - [DataField] - public float Max = float.PositiveInfinity; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-body-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs b/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs deleted file mode 100644 index 9de1bfdbf6b..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/BreathingCondition.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Condition for if the entity is successfully breathing. -/// -public sealed partial class Breathing : EventEntityEffectCondition -{ - /// - /// If true, the entity must not have trouble breathing to pass. - /// - [DataField] - public bool IsBreathing = true; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-breathing", - ("isBreathing", IsBreathing)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs b/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs deleted file mode 100644 index 379a2480271..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/HasTagCondition.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Tag; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class HasTag : EntityEffectCondition -{ - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Tag = default!; - - [DataField] - public bool Invert = false; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out var tag)) - return args.EntityManager.System().HasTag(tag, Tag) ^ Invert; - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - // this should somehow be made (much) nicer. - return Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", Tag), ("invert", Invert)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs b/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs deleted file mode 100644 index cb30ef70c78..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/InternalsCondition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Content.Shared.Body.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Condition for if the entity is or isn't wearing internals. -/// -public sealed partial class Internals : EntityEffectCondition -{ - /// - /// To pass, the entity's internals must have this same state. - /// - [DataField] - public bool UsingInternals = true; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out InternalsComponent? internalsComp)) - return !UsingInternals; // They have no internals to wear. - - var internalsState = internalsComp.GasTankEntity != null; // If gas tank is not null, they are wearing internals - return UsingInternals == internalsState; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-internals", ("usingInternals", UsingInternals)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs b/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs deleted file mode 100644 index 96f3be64c6f..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/JobCondition.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Linq; -using Content.Shared.Localizations; -using Content.Shared.Mind; -using Content.Shared.Mind.Components; -using Content.Shared.Roles; -using Content.Shared.Roles.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class JobCondition : EntityEffectCondition -{ - [DataField(required: true)] public List> Job; - - public override bool Condition(EntityEffectBaseArgs args) - { - args.EntityManager.TryGetComponent(args.TargetEntity, out var mindContainer); - - if (mindContainer is null - || !args.EntityManager.TryGetComponent(mindContainer.Mind, out var mind)) - return false; - - foreach (var roleId in mind.MindRoleContainer.ContainedEntities) - { - if (!args.EntityManager.HasComponent(roleId)) - continue; - - if (!args.EntityManager.TryGetComponent(roleId, out var mindRole)) - { - Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}"); - continue; - } - - if (mindRole.JobPrototype == null) - { - Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}"); - continue; - } - - if (Job.Contains(mindRole.JobPrototype.Value)) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList(); - return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames))); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs b/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs deleted file mode 100644 index efe7246b2ac..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/MobStateCondition.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class MobStateCondition : EntityEffectCondition -{ - [DataField] - public MobState Mobstate = MobState.Alive; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out MobStateComponent? mobState)) - { - if (mobState.CurrentState == Mobstate) - return true; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate)); - } -} - diff --git a/Content.Shared/EntityEffects/EffectConditions/OrganType.cs b/Content.Shared/EntityEffects/EffectConditions/OrganType.cs deleted file mode 100644 index f99eb5cc773..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/OrganType.cs +++ /dev/null @@ -1,28 +0,0 @@ -// using Content.Server.Body.Components; -using Content.Shared.Body.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType -/// -public sealed partial class OrganType : EventEntityEffectCondition -{ - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Type = default!; - - /// - /// Does this condition pass when the organ has the type, or when it doesn't have the type? - /// - [DataField] - public bool ShouldHave = true; - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-organ-type", - ("name", prototype.Index(Type).LocalizedName), - ("shouldhave", ShouldHave)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs b/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs deleted file mode 100644 index af71f20c8ec..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/ReagentThreshold.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Used for implementing reagent effects that require a certain amount of reagent before it should be applied. -/// For instance, overdoses. -/// -/// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the -/// one being metabolized. -/// -public sealed partial class ReagentThreshold : EntityEffectCondition -{ - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - // TODO use ReagentId - [DataField] - public string? Reagent; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var reagent = Reagent ?? reagentArgs.Reagent?.ID; - if (reagent == null) - return true; // No condition to apply. - - var quant = FixedPoint2.Zero; - if (reagentArgs.Source != null) - quant = reagentArgs.Source.GetTotalPrototypeQuantity(reagent); - - return quant >= Min && quant <= Max; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - ReagentPrototype? reagentProto = null; - if (Reagent is not null) - prototype.TryIndex(Reagent, out reagentProto); - - return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold", - ("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")), - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs b/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs deleted file mode 100644 index e2febd8f483..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/SolutionTemperature.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -/// -/// Requires the solution to be above or below a certain temperature. -/// Used for things like explosives. -/// -public sealed partial class SolutionTemperature : EntityEffectCondition -{ - [DataField] - public float Min = 0.0f; - - [DataField] - public float Max = float.PositiveInfinity; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - return reagentArgs?.Source != null && - reagentArgs.Source.Temperature >= Min && - reagentArgs.Source.Temperature <= Max; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs b/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs deleted file mode 100644 index a4baeb634a1..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/TotalDamage.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.EntityEffects; -using Content.Shared.Damage; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class TotalDamage : EntityEffectCondition -{ - [DataField] - public FixedPoint2 Max = FixedPoint2.MaxValue; - - [DataField] - public FixedPoint2 Min = FixedPoint2.Zero; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out DamageableComponent? damage)) - { - var total = damage.TotalDamage; - return total >= Min && total <= Max; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-damage", - ("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()), - ("min", Min.Float())); - } -} diff --git a/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs b/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs deleted file mode 100644 index cbeb334c473..00000000000 --- a/Content.Shared/EntityEffects/EffectConditions/TotalHunger.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.EntityEffects; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.EffectConditions; - -public sealed partial class Hunger : EntityEffectCondition -{ - [DataField] - public float Max = float.PositiveInfinity; - - [DataField] - public float Min = 0; - - public override bool Condition(EntityEffectBaseArgs args) - { - if (args.EntityManager.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) - { - var total = args.EntityManager.System().GetHunger(hunger); - return total >= Min && total <= Max; - } - - return false; - } - - public override string GuidebookExplanation(IPrototypeManager prototype) - { - return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", - ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), - ("min", Min)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs b/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs deleted file mode 100644 index 0f2d35d369f..00000000000 --- a/Content.Shared/EntityEffects/Effects/AddToSolutionReaction.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Chemistry.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class AddToSolutionReaction : EntityEffect - { - [DataField("solution")] - private string _solution = "reagents"; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) { - if (reagentArgs.Reagent == null) - return; - - // TODO see if this is correct - var solutionContainerSystem = reagentArgs.EntityManager.System(); - if (!solutionContainerSystem.TryGetSolution(reagentArgs.TargetEntity, _solution, out var solutionContainer)) - return; - - if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, reagentArgs.Reagent.ID, reagentArgs.Quantity, out var accepted)) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, accepted); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-add-to-solution-reaction", ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustAlert.cs b/Content.Shared/EntityEffects/Effects/AdjustAlert.cs deleted file mode 100644 index 282de0a06c2..00000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustAlert.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Content.Shared.Alert; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class AdjustAlert : EntityEffect -{ - /// - /// The specific Alert that will be adjusted - /// - [DataField(required: true)] - public ProtoId AlertType; - - /// - /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately. - /// - [DataField] - public bool Clear; - - /// - /// Visually display cooldown progress over the alert icon. - /// - [DataField] - public bool ShowCooldown; - - /// - /// The length of the cooldown or delay before removing the alert (in seconds). - /// - [DataField] - public float Time; - - //JUSTIFICATION: This just changes some visuals, doesn't need to be documented. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; - - public override void Effect(EntityEffectBaseArgs args) - { - var alertSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - if (!args.EntityManager.HasComponent(args.TargetEntity)) - return; - - if (Clear && Time <= 0) - { - alertSys.ClearAlert(args.TargetEntity, AlertType); - } - else - { - var timing = IoCManager.Resolve(); - (TimeSpan, TimeSpan)? cooldown = null; - - if ((ShowCooldown || Clear) && Time > 0) - cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time)); - - alertSys.ShowAlert(args.TargetEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); - } - - } -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs new file mode 100644 index 00000000000..633cde0877d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/AdjustAlertEntityEffectSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Adjusts a given alert on this entity. +/// +/// +public sealed partial class AdjustAlertEntityEffectSysten : EntityEffectSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time; + var clear = args.Effect.Clear; + var type = args.Effect.AlertType; + + if (clear && time <= TimeSpan.Zero) + { + _alerts.ClearAlert(entity.AsNullable(), type); + } + else + { + (TimeSpan, TimeSpan)? cooldown = null; + + if ((args.Effect.ShowCooldown || clear) && args.Effect.Time >= TimeSpan.Zero) + cooldown = (_timing.CurTime, _timing.CurTime + time); + + _alerts.ShowAlert(entity.AsNullable(), type, cooldown: cooldown, autoRemove: clear, showCooldown: args.Effect.ShowCooldown); + } + + } +} + +/// +public sealed partial class AdjustAlert : EntityEffectBase +{ + /// + /// The specific Alert that will be adjusted + /// + [DataField(required: true)] + public ProtoId AlertType; + + /// + /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately. + /// + [DataField] + public bool Clear; + + /// + /// Visually display cooldown progress over the alert icon. + /// + [DataField] + public bool ShowCooldown; + + /// + /// The length of the cooldown or delay before removing the alert (in seconds). + /// + [DataField] + public TimeSpan Time; +} diff --git a/Content.Shared/EntityEffects/Effects/AdjustReagent.cs b/Content.Shared/EntityEffects/Effects/AdjustReagent.cs deleted file mode 100644 index 897dda0aeec..00000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustReagent.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Content.Shared.Body.Prototypes; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class AdjustReagent : EntityEffect - { - /// - /// The reagent ID to remove. Only one of this and should be active. - /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Reagent = null; - // TODO use ReagentId - - /// - /// The metabolism group to remove, if the reagent satisfies any. - /// Only one of this and should be active. - /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? Group = null; - - [DataField(required: true)] - public FixedPoint2 Amount = default!; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Source == null) - return; - - var amount = Amount; - amount *= reagentArgs.Scale; - - if (Reagent != null) - { - if (amount < 0 && reagentArgs.Source.ContainsPrototype(Reagent)) - reagentArgs.Source.RemoveReagent(Reagent, -amount); - if (amount > 0) - reagentArgs.Source.AddReagent(Reagent, amount); - } - else if (Group != null) - { - var prototypeMan = IoCManager.Resolve(); - foreach (var quant in reagentArgs.Source.Contents.ToArray()) - { - var proto = prototypeMan.Index(quant.Reagent.Prototype); - if (proto.Metabolisms != null && proto.Metabolisms.ContainsKey(Group)) - { - if (amount < 0) - reagentArgs.Source.RemoveReagent(quant.Reagent, -amount); // Imperial space Chemistry Rework - if (amount > 0) - reagentArgs.Source.AddReagent(quant.Reagent, amount); - } - } - } - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if (Reagent is not null && prototype.TryIndex(Reagent, out ReagentPrototype? reagentProto)) - { - return Loc.GetString("reagent-effect-guidebook-adjust-reagent-reagent", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float())), - ("reagent", reagentProto.LocalizedName), - ("amount", MathF.Abs(Amount.Float()))); - } - else if (Group is not null && prototype.TryIndex(Group, out MetabolismGroupPrototype? groupProto)) - { - return Loc.GetString("reagent-effect-guidebook-adjust-reagent-group", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float())), - ("group", groupProto.LocalizedName), - ("amount", MathF.Abs(Amount.Float()))); - } - - throw new NotImplementedException(); - } - } -} - diff --git a/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs b/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs deleted file mode 100644 index 03dc226e934..00000000000 --- a/Content.Shared/EntityEffects/Effects/AdjustTemperature.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class AdjustTemperature : EventEntityEffect -{ - [DataField] - public float Amount; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-temperature", - ("chance", Probability), - ("deltasign", MathF.Sign(Amount)), - ("amount", MathF.Abs(Amount))); -} diff --git a/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs new file mode 100644 index 00000000000..884c5aaf2c9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/AdjustTemperatureEntityEffectsSystem.cs @@ -0,0 +1,37 @@ +using Content.Shared.Temperature.Components; +using Content.Shared.Temperature.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +// TODO: When we get a proper temperature/energy struct combine this with the solution temperature effect!!! +/// +/// Adjusts the temperature of this entity. +/// +/// +public sealed partial class AdjustTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedTemperatureSystem _temperature = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Effect.Amount * args.Scale; + + _temperature.ChangeHeat(entity, amount, true, entity.Comp); + } +} + +/// +public sealed partial class AdjustTemperature : EntityEffectBase +{ + /// + /// Amount we're adjusting temperature by. + /// + [DataField] + public float Amount; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-adjust-temperature", + ("chance", Probability), + ("deltasign", MathF.Sign(Amount)), + ("amount", Amount)); +} diff --git a/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs b/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs deleted file mode 100644 index 4db19be98ad..00000000000 --- a/Content.Shared/EntityEffects/Effects/AreaReactionEffect.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Basically smoke and foam reactions. -/// -public sealed partial class AreaReactionEffect : EventEntityEffect -{ - // Imperial Medieval Chemistry Begin - /// - /// Сколько единиц будет взято у мензурки - /// - [DataField] public float? Volume; - // Imperial Medieval Chemistry End - /// - /// How many seconds will the effect stay, counting after fully spreading. - /// - [DataField("duration")] public float Duration = 10; - - /// - /// How many units of reaction for 1 smoke entity. - /// - [DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5); - - /// - /// The entity prototype that will be spawned as the effect. - /// - [DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] - public string PrototypeId = default!; - - /// - /// Sound that will get played when this reaction effect occurs. - /// - [DataField("sound", required: true)] public SoundSpecifier Sound = default!; - - public override bool ShouldLog => true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-area-reaction", - ("duration", Duration) - ); - - public override LogImpact LogImpact => LogImpact.High; -} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs b/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs deleted file mode 100644 index 45ca7403631..00000000000 --- a/Content.Shared/EntityEffects/Effects/ArtifactDurabilityRestore.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Xenoarchaeology.Artifact.Components; -using Content.Shared.Xenoarchaeology.Artifact; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Restores durability in active artefact nodes. -/// -public sealed partial class ArtifactDurabilityRestore : EntityEffect -{ - /// - /// Amount of durability that will be restored per effect interaction. - /// - [DataField] - public int RestoredDurability = 1; - - public override void Effect(EntityEffectBaseArgs args) - { - var entMan = args.EntityManager; - var xenoArtifactSys = entMan.System(); - - if (!entMan.TryGetComponent(args.TargetEntity, out var xenoArtifact)) - return; - - foreach (var node in xenoArtifactSys.GetActiveNodes((args.TargetEntity, xenoArtifact))) - { - xenoArtifactSys.AdjustNodeDurability(node.Owner, RestoredDurability); - } - } - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-artifact-durability-restore", ("restored", RestoredDurability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs new file mode 100644 index 00000000000..563c053b368 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ArtifactEntityEffectsSystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Popups; +using Content.Shared.Xenoarchaeology.Artifact; +using Content.Shared.Xenoarchaeology.Artifact.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Restores durability on this artifact +/// +/// +public sealed partial class ArtifactDurabilityRestoreEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedXenoArtifactSystem _xenoArtifact = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var durability = args.Effect.RestoredDurability; + + foreach (var node in _xenoArtifact.GetActiveNodes(entity)) + { + _xenoArtifact.AdjustNodeDurability(node.Owner, durability); + } + } +} + +/// +/// Unlocks a node on this artifact. Only works this effect hasn't been applied before. +/// +/// +public sealed partial class ArtifactUnlockEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedXenoArtifactSystem _xenoArtifact = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (EnsureComp(entity, out var unlocking)) + { + if (unlocking.ArtifexiumApplied) + return; + + _popup.PopupEntity(Loc.GetString("artifact-activation-artifexium"), entity, PopupType.Medium); + } + else + { + _xenoArtifact.TriggerXenoArtifact(entity, null, force: true); + } + + _xenoArtifact.SetArtifexiumApplied((entity, unlocking), true); + } +} + +/// +public sealed partial class ArtifactDurabilityRestore : EntityEffectBase +{ + /// + /// Amount of durability that will be restored per effect interaction. + /// + [DataField] + public int RestoredDurability = 1; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-artifact-durability-restore", ("restored", RestoredDurability)); +} + +/// +public sealed partial class ArtifactUnlock : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-artifact-unlock", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs b/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs deleted file mode 100644 index 077e1ebfd20..00000000000 --- a/Content.Shared/EntityEffects/Effects/ArtifactUnlock.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Content.Shared.Xenoarchaeology.Artifact; -using Content.Shared.EntityEffects; -using Content.Shared.Popups; -using Content.Shared.Xenoarchaeology.Artifact.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Sets an artifact into the unlocking state and marks the artifexium effect as true. -/// This is a very specific behavior intended for a specific chem. -/// -public sealed partial class ArtifactUnlock : EntityEffect -{ - public override void Effect(EntityEffectBaseArgs args) - { - var entMan = args.EntityManager; - var xenoArtifactSys = entMan.System(); - var popupSys = entMan.System(); - - if (!entMan.TryGetComponent(args.TargetEntity, out var xenoArtifact)) - return; - - if (!entMan.TryGetComponent(args.TargetEntity, out var unlocking)) - { - xenoArtifactSys.TriggerXenoArtifact((args.TargetEntity, xenoArtifact), null, force: true); - unlocking = entMan.EnsureComponent(args.TargetEntity); - } - else if (!unlocking.ArtifexiumApplied) - { - popupSys.PopupEntity(Loc.GetString("artifact-activation-artifexium"), args.TargetEntity, PopupType.Medium); - } - - if (unlocking.ArtifexiumApplied) - return; - - xenoArtifactSys.SetArtifexiumApplied((args.TargetEntity, unlocking), true); - } - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-artifact-unlock", ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs new file mode 100644 index 00000000000..9614933247d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/CreateGasEntityEffect.cs @@ -0,0 +1,35 @@ +using Content.Shared.Atmos; +using Content.Shared.Atmos.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system. +/// +/// +public sealed partial class CreateGas : EntityEffectBase +{ + /// + /// The gas we're creating + /// + [DataField] + public Gas Gas; + + /// + /// Amount of moles we're creating + /// + [DataField] + public float Moles = 3f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var atmos = entSys.GetEntitySystem(); + var gasProto = atmos.GetGas(Gas); + + return Loc.GetString("entity-effect-guidebook-create-gas", + ("chance", Probability), + ("moles", Moles), + ("gas", Loc.GetString(gasProto.Name))); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs new file mode 100644 index 00000000000..b4b475ec59c --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/ExtinguishEntityEffectSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// This raises an extinguish event on a given entity, reducing FireStacks. +/// The amount of FireStacks reduced is modified by scale. +/// +/// +public sealed partial class ExtinguishEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var ev = new ExtinguishEvent + { + FireStacksAdjustment = args.Effect.FireStacksAdjustment * args.Scale, + }; + + RaiseLocalEvent(entity, ref ev); + } +} + +/// +public sealed partial class Extinguish : EntityEffectBase +{ + /// + /// Amount of FireStacks reduced. + /// + [DataField] + public float FireStacksAdjustment = -1.5f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-extinguish-reaction", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs new file mode 100644 index 00000000000..563201a40ff --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/FlammableEntityEffect.cs @@ -0,0 +1,29 @@ +using Content.Shared.Database; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system. +/// +/// +public sealed partial class Flammable : EntityEffectBase +{ + /// + /// Fire stack multiplier applied on an entity, + /// unless that entity is already on fire and is not null. + /// + [DataField] + public float Multiplier = 0.05f; + + /// + /// Fire stack multiplier applied if the entity is already on fire. Defaults to if null. + /// + [DataField] + public float? MultiplierOnExisting; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-flammable-reaction", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.Low; +} diff --git a/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs new file mode 100644 index 00000000000..7ca93e7c180 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Atmos/IgniteEntityEffect.cs @@ -0,0 +1,16 @@ +using Content.Shared.Database; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Atmos; + +/// +/// See serverside system +/// +/// +public sealed partial class Ignite : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-ignite", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.Medium; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs new file mode 100644 index 00000000000..a5752b858e5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Removes a given amount of chemicals from the bloodstream modified by scale. +/// Optionally ignores a given chemical. +/// +/// +public sealed partial class CleanBloodstreamEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var scale = args.Scale * args.Effect.CleanseRate; + + _bloodstream.FlushChemicals((entity, entity), scale, args.Effect.Excluded); + } +} + +/// +public sealed partial class CleanBloodstream : EntityEffectBase +{ + /// + /// Amount of reagent we're cleaning out of our bloodstream. + /// + [DataField] + public FixedPoint2 CleanseRate = 3.0f; + + /// + /// An optional chemical to ignore when doing removal. + /// + [DataField] + public ProtoId? Excluded; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-clean-bloodstream", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs new file mode 100644 index 00000000000..29fd994e8c9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/EyeDamageEntityEffectSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Eye.Blinding.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies eye damage by a given amount, modified by scale, floored to an integer. +/// +/// +public sealed partial class EyeDamageEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly BlindableSystem _blindable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = (int) Math.Floor(args.Effect.Amount * args.Scale); + _blindable.AdjustEyeDamage(entity.Owner, amount); + } +} + +/// +public sealed partial class EyeDamage : EntityEffectBase +{ + /// + /// The amount of eye damage we're adding or removing + /// + [DataField] + public int Amount = -1; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs new file mode 100644 index 00000000000..c684ffcdf28 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies bleed by a given amount multiplied by scale. This can increase or decrease bleed. +/// +/// +public sealed partial class ModifyBleedEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _bloodstream.TryModifyBleedAmount(entity.AsNullable(), args.Effect.Amount * args.Scale); + } +} + +/// +public sealed partial class ModifyBleed : EntityEffectBase +{ + /// + /// Amount of bleed we're applying or removing if negative. + /// + [DataField] + public float Amount = -1.0f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-modify-bleed-amount", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs new file mode 100644 index 00000000000..43098c9ddde --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Modifies the amount of blood in this entity's bloodstream by a given amount multiplied by scale. +/// This effect can increase or decrease blood level. +/// +/// +public sealed partial class ModifyBloodLevelEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _bloodstream.TryModifyBloodLevel(entity.AsNullable(), args.Effect.Amount * args.Scale); + } +} + +/// +public sealed partial class ModifyBloodLevel : EntityEffectBase +{ + /// + /// Amount of bleed we're applying or removing if negative. + /// + [DataField] + public FixedPoint2 Amount = 1.0f; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-modify-blood-level", ("chance", Probability), ("deltasign", MathF.Sign(Amount.Float()))); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs new file mode 100644 index 00000000000..29fa1de98d5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyLungGasEntityEffectSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Atmos; +using Content.Shared.Body.Components; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Adjust the amount of Moles stored in this set of lungs based on a given dictionary of gasses and ratios. +/// The amount of gas adjusted is modified by scale. +/// +/// +public sealed partial class ModifyLungGasEntityEffectSystem : EntityEffectSystem +{ + // TODO: This shouldn't be an entity effect, gasses should just metabolize and make a byproduct by default... + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Scale; + + foreach (var (gas, ratio) in args.Effect.Ratios) + { + var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier; + if (quantity < 0) + quantity = Math.Max(quantity, -entity.Comp.Air[(int) gas]); + entity.Comp.Air.AdjustMoles(gas, quantity); + } + } +} + +/// +public sealed partial class ModifyLungGas : EntityEffectBase +{ + [DataField(required: true)] + public Dictionary Ratios = default!; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs new file mode 100644 index 00000000000..9790fced4e5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/OxygenateEntityEffect.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// See serverside system. +/// +/// +public sealed partial class Oxygenate : EntityEffectBase +{ + /// + /// Factor of oxygenation per metabolized quantity. Lungs metabolize at about 50u per tick so we need an equal multiplier to cancel that out! + /// + [DataField] + public float Factor = 1f; +} diff --git a/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs new file mode 100644 index 00000000000..4fb41bd0102 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/ReduceRottingEntityEffectSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Atmos.Rotting; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Reduces the rotting timer on an entity by a number of seconds, modified by scale. +/// This cannot increase the amount of seconds a body has rotted. +/// +/// +public sealed partial class ReduceRottingEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedRottingSystem _rotting = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var amount = args.Effect.Seconds * args.Scale; + + _rotting.ReduceAccumulator(entity, amount); + } +} + +/// +public sealed partial class ReduceRotting : EntityEffectBase +{ + /// + /// Number of seconds removed from the rotting timer. + /// + [DataField] + public TimeSpan Seconds = TimeSpan.FromSeconds(10); + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-reduce-rotting", + ("chance", Probability), + ("time", Seconds.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs new file mode 100644 index 00000000000..167e7e540d5 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +// TODO: These systems are in the same file since satiation should be one system instead of two. Combine these when that happens. +// TODO: Arguably oxygen saturation should also be added here... +/// +/// Modifies the thirst level of a given entity, multiplied by scale. +/// +/// +public sealed partial class SatiateThirstEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly ThirstSystem _thirst = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _thirst.ModifyThirst(entity, entity.Comp, args.Effect.Factor * args.Scale); + } +} + +/// +/// Modifies the hunger level of a given entity, multiplied by scale. +/// +/// +public sealed partial class SatiateHungerEntityEffectsSystem : EntityEffectSystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _hunger.ModifyHunger(entity, args.Effect.Factor * args.Scale, entity.Comp); + } +} + +/// +/// A type of made for satiation effects. +/// +/// The effect inheriting this BaseEffect +/// +public abstract partial class Satiate : EntityEffectBase where T : EntityEffectBase +{ + public const float AverageSatiation = 3f; // Magic number. Not sure how it was calculated since I didn't make it. + + /// + /// Change in satiation. + /// + [DataField] + public float Factor = 1.5f; +} + +/// +public sealed partial class SatiateThirst : Satiate +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", Factor / AverageSatiation)); +} + +/// +public sealed partial class SatiateHunger : Satiate +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-satiate-hunger", ("chance", Probability), ("relative", Factor / AverageSatiation)); +} diff --git a/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs new file mode 100644 index 00000000000..02747057c55 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Body/VomitEntityEffectSystem.cs @@ -0,0 +1,37 @@ +using Content.Shared.Medical; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Body; + +/// +/// Makes an entity vomit and reduces hunger and thirst by a given amount, modified by scale. +/// +/// +public sealed partial class VomitEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly VomitSystem _vomit = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _vomit.Vomit(entity.Owner, args.Effect.ThirstAmount * args.Scale, args.Effect.HungerAmount * args.Scale); + } +} + +/// +public sealed partial class Vomit : EntityEffectBase +{ + /// + /// How much we adjust our thirst after vomiting. + /// + [DataField] + public float ThirstAmount = -8f; + + /// + /// How much we adjust our hunger after vomiting. + /// + [DataField] + public float HungerAmount = -8f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-vomit", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs new file mode 100644 index 00000000000..9b235e68d4f --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/BasePlantAdjustAttributeEntityEffect.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// A type of which modifies the attribute of a Seed in a PlantHolder. +/// These are not modified by scale as botany has no concept of scale. +/// +/// The effect inheriting this BaseEffect +/// +public abstract partial class BasePlantAdjustAttribute : EntityEffectBase where T : BasePlantAdjustAttribute +{ + /// + /// How much we're adjusting the given attribute by. + /// + [DataField] + public float Amount { get; protected set; } = 1; + + /// + /// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions. + /// + [DataField] + public abstract string GuidebookAttributeName { get; set; } + + /// + /// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number. + /// + [DataField] + public virtual bool GuidebookIsAttributePositive { get; protected set; } = true; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-attribute", + ("attribute", Loc.GetString(GuidebookAttributeName)), + ("amount", Amount.ToString("0.00")), + ("positive", GuidebookIsAttributePositive), + ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs new file mode 100644 index 00000000000..069cb320a9b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealth.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustHealth : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-health"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs new file mode 100644 index 00000000000..32e419e291b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevel.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationLevel : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-level"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs new file mode 100644 index 00000000000..4ea695d1351 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationMod.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustMutationMod : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-mod"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs new file mode 100644 index 00000000000..6f53076fb36 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutrition.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustNutrition : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-nutrition"; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs new file mode 100644 index 00000000000..c1661ec89e6 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPests.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustPests : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-pests"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs new file mode 100644 index 00000000000..4f42adf3e63 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotency.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles increase or decrease of plant potency. +/// +public sealed partial class PlantAdjustPotency : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-potency"; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs new file mode 100644 index 00000000000..04eccb03ec4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxins.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustToxins : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-toxins"; + + public override bool GuidebookIsAttributePositive { get; protected set; } = false; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs new file mode 100644 index 00000000000..1d6ef8ea095 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWater.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWater : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-water"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs new file mode 100644 index 00000000000..e6be25d8c90 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeeds.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAdjustWeeds : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-weeds"; + public override bool GuidebookIsAttributePositive { get; protected set; } = false; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs new file mode 100644 index 00000000000..76466f544de --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowth.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantAffectGrowth : BasePlantAdjustAttribute +{ + public override string GuidebookAttributeName { get; set; } = "plant-attribute-growth"; +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs new file mode 100644 index 00000000000..dcea56baade --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStat.cs @@ -0,0 +1,21 @@ +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantChangeStat : EntityEffectBase +{ + /// + /// This is the worst thing in the code base. + /// It's meant to be generic and expandable I guess? But it's looking for a specific datafield and then + /// sending it into an if else if else if statement that filters by object type and randomly flips bits. + /// + [DataField (required: true)] + public string TargetValue = string.Empty; + + [DataField] + public float MinValue; + + [DataField] + public float MaxValue; + + [DataField] + public int Steps; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs new file mode 100644 index 00000000000..7dcea240a68 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadone.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantCryoxadone : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-cryoxadone", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs new file mode 100644 index 00000000000..d45c0369f7d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles removal of seeds on a plant. +/// +public sealed partial class PlantDestroySeeds : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-seeds-remove", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs new file mode 100644 index 00000000000..4355a445937 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamine.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantDiethylamine : EntityEffectBase +{ + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-diethylamine", ("chance", Probability)); +} + diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs new file mode 100644 index 00000000000..b56b99038b8 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximine.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantPhalanximine : EntityEffectBase +{ + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-phalanximine", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzu.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzu.cs new file mode 100644 index 00000000000..2ab68708cc9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRemoveKudzu.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class PlantRemoveKudzu : EntityEffectBase +{ + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-remove-kudzu", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs new file mode 100644 index 00000000000..63b06b0ad14 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeeds.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +/// +/// Handles restoral of seeds on a plant. +/// +public sealed partial class PlantRestoreSeeds : EntityEffectBase +{ + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-seeds-add", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs new file mode 100644 index 00000000000..77bde39b125 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvest.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; + +public sealed partial class RobustHarvest : EntityEffectBase +{ + [DataField] + public int PotencyLimit = 50; + + [DataField] + public int PotencyIncrease = 3; + + [DataField] + public int PotencySeedlessThreshold = 30; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-plant-robust-harvest", + ("seedlesstreshold", PotencySeedlessThreshold), + ("limit", PotencyLimit), + ("increase", PotencyIncrease), + ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs new file mode 100644 index 00000000000..df119c6e40d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffect.cs @@ -0,0 +1,42 @@ +using Content.Shared.Localizations; +using Content.Shared.Random; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateChemicals : EntityEffectBase +{ + /// + /// The Reagent list this mutation draws from. + /// + [DataField] + public ProtoId RandomPickBotanyReagent = "RandomPickBotanyReagent"; + + /// + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var list = new List(); + + // If your table doesn't exist, no guidebook for you! + if (!prototype.Resolve(RandomPickBotanyReagent, out var table)) + return string.Empty; + + foreach (var fill in table.Fills) + { + foreach (var reagent in fill.Reagents) + { + if (!prototype.Resolve(reagent, out var proto)) + continue; + + list.Add(proto.LocalizedName); + } + } + + var names = ContentLocalizationManager.FormatListToOr(list); + + return Loc.GetString("entity-effect-guidebook-plant-mutate-chemicals", ("chance", Probability), ("name", names)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs new file mode 100644 index 00000000000..c617c05b334 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs @@ -0,0 +1,22 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateConsumeGases : EntityEffectBase +{ + [DataField] + public float MinValue = 0.01f; + + [DataField] + public float MaxValue = 0.5f; +} + +public sealed partial class PlantMutateExudeGases : EntityEffectBase +{ + [DataField] + public float MinValue = 0.01f; + + [DataField] + public float MaxValue = 0.5f; +} diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs new file mode 100644 index 00000000000..3602453b16a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffect.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateHarvest : EntityEffectBase; diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs new file mode 100644 index 00000000000..91e8947044d --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffect.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.EntityEffects.Effects.Botany; + +/// +/// See serverside system. +/// +public sealed partial class PlantMutateSpeciesChange : EntityEffectBase; diff --git a/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs b/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs deleted file mode 100644 index 3f8c58b74c8..00000000000 --- a/Content.Shared/EntityEffects/Effects/CauseZombieInfection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CauseZombieInfection : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-cause-zombie-infection", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs b/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs deleted file mode 100644 index 98181b86671..00000000000 --- a/Content.Shared/EntityEffects/Effects/ChemCleanBloodstream.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Basically smoke and foam reactions. -/// -public sealed partial class ChemCleanBloodstream : EventEntityEffect -{ - [DataField] - public float CleanseRate = 3.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-chem-clean-bloodstream", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs b/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs deleted file mode 100644 index 83b2aa96e5f..00000000000 --- a/Content.Shared/EntityEffects/Effects/ChemHealEyeDamage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Eye.Blinding.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Heal or apply eye damage -/// -public sealed partial class ChemHealEyeDamage : EntityEffect -{ - /// - /// How much eye damage to add. - /// - [DataField] - public int Amount = -1; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-cure-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount))); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - if (reagentArgs.Scale != 1f) // huh? - return; - - args.EntityManager.EntitySysManager.GetEntitySystem().AdjustEyeDamage(args.TargetEntity, Amount); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ChemVomit.cs b/Content.Shared/EntityEffects/Effects/ChemVomit.cs deleted file mode 100644 index 1cd6b2552c2..00000000000 --- a/Content.Shared/EntityEffects/Effects/ChemVomit.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Forces you to vomit. -/// -public sealed partial class ChemVomit : EventEntityEffect -{ - /// How many units of thirst to add each time we vomit - [DataField] - public float ThirstAmount = -8f; - /// How many units of hunger to add each time we vomit - [DataField] - public float HungerAmount = -8f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-chem-vomit", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs b/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs deleted file mode 100644 index 33173b17379..00000000000 --- a/Content.Shared/EntityEffects/Effects/CreateEntityReactionEffect.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class CreateEntityReactionEffect : EventEntityEffect -{ - /// - /// What entity to create. - /// - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Entity = default!; - - /// - /// How many entities to create per unit reaction. - /// - [DataField] - public uint Number = 1; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-create-entity-reaction-effect", - ("chance", Probability), - ("entname", IoCManager.Resolve().Index(Entity).Name), - ("amount", Number)); -} diff --git a/Content.Shared/EntityEffects/Effects/CreateGas.cs b/Content.Shared/EntityEffects/Effects/CreateGas.cs deleted file mode 100644 index 75d554cdb38..00000000000 --- a/Content.Shared/EntityEffects/Effects/CreateGas.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Atmos; -using Content.Shared.Atmos.EntitySystems; -using Content.Shared.Database; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CreateGas : EventEntityEffect -{ - [DataField(required: true)] - public Gas Gas = default!; - - /// - /// For each unit consumed, how many moles of gas should be created? - /// - [DataField] - public float Multiplier = 3f; - - public override bool ShouldLog => true; - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var atmos = entSys.GetEntitySystem(); - var gasProto = atmos.GetGas(Gas); - - return Loc.GetString("reagent-effect-guidebook-create-gas", - ("chance", Probability), - ("moles", Multiplier), - ("gas", gasProto.Name)); - } - - public override LogImpact LogImpact => LogImpact.High; -} diff --git a/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs b/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs deleted file mode 100644 index dd2d21854c6..00000000000 --- a/Content.Shared/EntityEffects/Effects/CureZombieInfection.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class CureZombieInfection : EventEntityEffect -{ - [DataField] - public bool Innoculate; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if(Innoculate) - return Loc.GetString("reagent-effect-guidebook-innoculate-zombie-infection", ("chance", Probability)); - - return Loc.GetString("reagent-effect-guidebook-cure-zombie-infection", ("chance", Probability)); - } -} - diff --git a/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs new file mode 100644 index 00000000000..66911d7785e --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs @@ -0,0 +1,91 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Damage; + +/// +/// Heal the damage types in a damage group by up to a specified total on this entity. +/// The amount healed per type is weighted by the amount of damage for that type scaling linearly. +/// Total adjustment is modified by scale. +/// +/// +public sealed partial class DistributedHealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + foreach (var (group, amount) in args.Effect.Damage) + { + _damageable.HealDistributed(entity.AsNullable(), amount * args.Scale, group); + } + } +} + +/// +public sealed partial class DistributedHealthChange : EntityEffectBase +{ + /// + /// Damage to heal, collected into entire damage groups. + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> Damage = new(); + + /// + /// Should this effect ignore damage modifiers? + /// + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damagableSystem = entSys.GetEntitySystem(); + var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; + var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; + + foreach (var (group, amount) in Damage) + { + var groupProto = prototype.Index(group); + + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", groupProto.LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + // We use health change since in practice it's not even and distributed is a mouthful. + // Also because healing groups not using even or distributed healing should be kill. + var healsordeals = heals ? deals ? "both" : "heals" : deals ? "deals" : "none"; + return Loc.GetString("entity-effect-guidebook-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs new file mode 100644 index 00000000000..7aa9288a0db --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs @@ -0,0 +1,88 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Damage; + +/// +/// Evenly heal the damage types in a damage group by up to a specified total on this entity. +/// Total adjustment is modified by scale. +/// +/// +public sealed partial class EvenHealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + foreach (var (group, amount) in args.Effect.Damage) + { + _damageable.HealEvenly(entity.AsNullable(), amount * args.Scale, group); + } + } +} + +/// +public sealed partial class EvenHealthChange : EntityEffectBase +{ + /// + /// Damage to heal, collected into entire damage groups. + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> Damage = new(); + + /// + /// Should this effect ignore damage modifiers? + /// + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damagableSystem = entSys.GetEntitySystem(); + var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; + var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; + + foreach (var (group, amount) in Damage) + { + var groupProto = prototype.Index(group); + + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", groupProto.LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + var healsordeals = heals ? deals ? "both" : "heals" : deals ? "deals" : "none"; + return Loc.GetString("entity-effect-guidebook-even-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs new file mode 100644 index 00000000000..b09b16f788b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs @@ -0,0 +1,93 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Damage; + +/// +/// Adjust the damages on this entity by specified amounts. +/// Amounts are modified by scale. +/// +/// +public sealed partial class HealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var damageSpec = new DamageSpecifier(args.Effect.Damage); + + damageSpec *= args.Scale; + + _damageable.TryChangeDamage( + entity.AsNullable(), + damageSpec, + args.Effect.IgnoreResistances, + interruptsDoAfters: false); + } +} + +/// +public sealed partial class HealthChange : EntityEffectBase +{ + /// + /// Damage to apply every cycle. Damage Ignores resistances. + /// + [DataField(required: true)] + public DamageSpecifier Damage = default!; + + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damageSpec = new DamageSpecifier(Damage); + + var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; + var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; + + damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); + + foreach (var (kind, amount) in damageSpec.DamageDict) + { + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", prototype.Index(kind).LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); + + return Loc.GetString("entity-effect-guidebook-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Drunk.cs b/Content.Shared/EntityEffects/Effects/Drunk.cs deleted file mode 100644 index aa15df8f3d8..00000000000 --- a/Content.Shared/EntityEffects/Effects/Drunk.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Drunk; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Drunk : EntityEffect -{ - /// - /// BoozePower is how long each metabolism cycle will make the drunk effect last for. - /// - [DataField] - public TimeSpan BoozePower = TimeSpan.FromSeconds(3f); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-drunk", ("chance", Probability)); - - public override void Effect(EntityEffectBaseArgs args) - { - var boozePower = BoozePower; - - if (args is EntityEffectReagentArgs reagentArgs) - boozePower *= reagentArgs.Scale.Float(); - - var drunkSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower); - } -} diff --git a/Content.Shared/EntityEffects/Effects/Electrocute.cs b/Content.Shared/EntityEffects/Effects/Electrocute.cs deleted file mode 100644 index 32e0ff1172f..00000000000 --- a/Content.Shared/EntityEffects/Effects/Electrocute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Electrocution; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Electrocute : EntityEffect -{ - [DataField] public int ElectrocuteTime = 2; - - [DataField] public int ElectrocuteDamageScale = 5; - - /// - /// true - refresh electrocute time, false - accumulate electrocute time - /// - [DataField] public bool Refresh = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime)); - - public override bool ShouldLog => true; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - reagentArgs.EntityManager.System().TryDoElectrocution(reagentArgs.TargetEntity, null, - Math.Max((reagentArgs.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); - - if (reagentArgs.Reagent != null) - reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity); - } else - { - args.EntityManager.System().TryDoElectrocution(args.TargetEntity, null, - Math.Max(ElectrocuteDamageScale, 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/Emote.cs b/Content.Shared/EntityEffects/Effects/Emote.cs deleted file mode 100644 index 404eaf9c2fc..00000000000 --- a/Content.Shared/EntityEffects/Effects/Emote.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Content.Shared.Chat; -using Content.Shared.Chat.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Tries to force someone to emote (scream, laugh, etc). Still respects whitelists/blacklists and other limits unless specially forced. -/// -public sealed partial class Emote : EventEntityEffect -{ - /// - /// The emote the entity will preform. - /// - [DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string EmoteId; - - /// - /// If the emote should be recorded in chat. - /// - [DataField] - public bool ShowInChat; - - /// - /// If the forced emote will be listed in the guidebook. - /// - [DataField] - public bool ShowInGuidebook; - - /// - /// If true, the entity will preform the emote even if they normally can't. - /// - [DataField] - public bool Force = false; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - if (!ShowInGuidebook) - return null; // JUSTIFICATION: Emoting is mostly flavor, so same reason popup messages are not in here. - - var emotePrototype = prototype.Index(EmoteId); - return Loc.GetString("reagent-effect-guidebook-emote", ("chance", Probability), ("emote", Loc.GetString(emotePrototype.Name))); - } -} diff --git a/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs new file mode 100644 index 00000000000..d8474e41631 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EmoteEntityEffectSystem.cs @@ -0,0 +1,58 @@ +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Makes this entity emote. +/// +/// +public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedChatSystem _chat = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if (args.Effect.ShowInChat) + _chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force); + else + _chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId); + } +} + +/// +public sealed partial class Emote : EntityEffectBase +{ + /// + /// The emote the entity will preform. + /// + [DataField("emote", required: true)] + public ProtoId EmoteId; + + /// + /// If the emote should be recorded in chat. + /// + [DataField] + public bool ShowInChat; + + /// + /// If the forced emote will be listed in the guidebook. + /// + [DataField] + public bool ShowInGuidebook; + + /// + /// If true, the entity will preform the emote even if they normally can't. + /// + [DataField] + public bool Force; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + if (!ShowInGuidebook || !prototype.Resolve(EmoteId, out var emote)) + return null; // JUSTIFICATION: Emoting is mostly flavor, so same reason popup messages are not in here. + + return Loc.GetString("entity-effect-guidebook-emote", ("chance", Probability), ("emote", Loc.GetString(emote.Name))); + } +} diff --git a/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs b/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs deleted file mode 100644 index eee0aeb51bd..00000000000 --- a/Content.Shared/EntityEffects/Effects/EmpReactionEffect.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class EmpReactionEffect : EventEntityEffect -{ - /// - /// Impulse range per unit of quantity - /// - [DataField("rangePerUnit")] - public float EmpRangePerUnit = 0.5f; - - /// - /// Maximum impulse range - /// - [DataField("maxRange")] - public float EmpMaxRange = 10; - - /// - /// How much energy will be drain from sources - /// - [DataField] - public float EnergyConsumption = 12500; - - /// - /// Amount of time entities will be disabled - /// - [DataField("duration")] - public float DisableDuration = 15; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-emp-reaction-effect", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs new file mode 100644 index 00000000000..3f5d5b3b57b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/BaseSpawnEntityEntityEffect.cs @@ -0,0 +1,36 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// A type of for effects that spawn entities by prototype. +/// +/// The entity effect inheriting this BaseEffect +/// +public abstract partial class BaseSpawnEntityEntityEffect : EntityEffectBase where T : BaseSpawnEntityEntityEffect +{ + /// + /// Amount of entities we're spawning + /// + [DataField] + public int Number = 1; + + /// + /// Prototype of the entity we're spawning + /// + [DataField (required: true)] + public EntProtoId Entity; + + /// + /// Whether this spawning is predicted. Set false to not predict the spawn. + /// Entities with animations or that have random elements when spawned should set this to false. + /// + [DataField] + public bool Predicted = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-spawn-entity", + ("chance", Probability), + ("entname", IoCManager.Resolve().Index(Entity).Name), + ("amount", Number)); +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs new file mode 100644 index 00000000000..9887ecdc608 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityEntityEffectSystem.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a number of entities of a given prototype at the coordinates of this entity. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + PredictedSpawnNextToOrDrop(proto, entity, entity.Comp); + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + SpawnNextToOrDrop(proto, entity, entity.Comp); + } + } + } +} + +/// +public sealed partial class SpawnEntity : BaseSpawnEntityEntityEffect; diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs new file mode 100644 index 00000000000..6909d503df4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerEntityEffectSystem.cs @@ -0,0 +1,51 @@ +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a given number of entities of a given prototype in a specified container owned by this entity. +/// Returns if the prototype cannot spawn in the specified container. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityInContainerEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + var container = args.Effect.ContainerName; + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + // Stop trying to spawn if it fails + if (!PredictedTrySpawnInContainer(proto, entity, container, out _, entity.Comp)) + return; + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + // Stop trying to spawn if it fails + if (!TrySpawnInContainer(proto, entity, container, out _, entity.Comp)) + return; + } + } + } +} + +/// +public sealed partial class SpawnEntityInContainer : BaseSpawnEntityEntityEffect +{ + /// + /// Name of the container we're trying to spawn into. + /// + [DataField(required: true)] + public string ContainerName; +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs new file mode 100644 index 00000000000..e5b1e119c14 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInContainerOrDropEntityEffectSystem.cs @@ -0,0 +1,49 @@ +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns a given number of entities of a given prototype in a specified container owned by this entity. +/// Acts like if it cannot spawn the prototype in the specified container. +/// Amount is modified by scale. +/// +/// +public sealed partial class SpawnEntityInContainerOrDropEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Number * (int)Math.Floor(args.Scale); + var proto = args.Effect.Entity; + var container = args.Effect.ContainerName; + + var xform = Transform(entity); + + if (args.Effect.Predicted) + { + for (var i = 0; i < quantity; i++) + { + PredictedSpawnInContainerOrDrop(proto, entity, container, xform, entity.Comp); + } + } + else if (_net.IsServer) + { + for (var i = 0; i < quantity; i++) + { + SpawnInContainerOrDrop(proto, entity, container, xform, entity.Comp); + } + } + } +} + +/// +public sealed partial class SpawnEntityInContainerOrDrop : BaseSpawnEntityEntityEffect +{ + /// + /// Name of the container we're trying to spawn into. + /// + [DataField(required: true)] + public string ContainerName; +} diff --git a/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs new file mode 100644 index 00000000000..7514dae4e9b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/EntitySpawning/SpawnEntityInInventoryEntityEffectSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared.Inventory; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.EntitySpawning; + +/// +/// Spawns an entity of a given prototype in a specified inventory slot owned by this entity. +/// Fails if it cannot spawn the entity in the given slot. +/// +/// +public sealed partial class SpawnEntityInInventoryEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly InventorySystem _inventory = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _inventory.SpawnItemInSlot(entity, args.Effect.Slot, args.Effect.Entity); + } +} + +/// +public sealed partial class SpawnEntityInInventory : EntityEffectBase +{ + /// + /// Name of the slot we're spawning the item into. + /// + [DataField(required: true)] + public string Slot = string.Empty; // Rider is drunk and keeps yelling at me to fill this out or make required: true but, it is required true so it's just being an asshole. + + /// + /// Prototype ID of item to spawn. + /// + [DataField(required: true)] + public EntProtoId Entity; +} diff --git a/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs b/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs deleted file mode 100644 index 968d559939f..00000000000 --- a/Content.Shared/EntityEffects/Effects/EvenHealthChange.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Content.Shared.Localizations; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Version of that distributes the healing to groups -/// -public sealed partial class EvenHealthChange : EntityEffect -{ - /// - /// Damage to heal, collected into entire damage groups. - /// - [DataField(required: true)] - public Dictionary, FixedPoint2> Damage = new(); - - /// - /// Should this effect scale the damage by the amount of chemical in the solution? - /// Useful for touch reactions, like styptic powder or acid. - /// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs. - /// - [DataField] - public bool ScaleByQuantity; - - /// - /// Should this effect ignore damage modifiers? - /// - [DataField] - public bool IgnoreResistances = true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var damages = new List(); - var heals = false; - var deals = false; - - var damagableSystem = entSys.GetEntitySystem(); - var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; - var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; - - foreach (var (group, amount) in Damage) - { - var groupProto = prototype.Index(group); - - var sign = FixedPoint2.Sign(amount); - var mod = 1f; - - if (sign < 0) - { - heals = true; - mod = universalReagentHealModifier; - } - else if (sign > 0) - { - deals = true; - mod = universalReagentDamageModifier; - } - - damages.Add( - Loc.GetString("health-change-display", - ("kind", groupProto.LocalizedName), - ("amount", MathF.Abs(amount.Float() * mod)), - ("deltasign", sign) - )); - } - - var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); - return Loc.GetString("reagent-effect-guidebook-even-health-change", - ("chance", Probability), - ("changes", ContentLocalizationManager.FormatList(damages)), - ("healsordeals", healsordeals)); - } - - public override void Effect(EntityEffectBaseArgs args) - { - if (!args.EntityManager.TryGetComponent(args.TargetEntity, out var damageable)) - return; - - var protoMan = IoCManager.Resolve(); - - var scale = FixedPoint2.New(1); - - if (args is EntityEffectReagentArgs reagentArgs) - { - scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale; - } - - var damagableSystem = args.EntityManager.System(); - var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; - var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; - - var dspec = new DamageSpecifier(); - - foreach (var (group, amount) in Damage) - { - var groupProto = protoMan.Index(group); - var groupDamage = new Dictionary(); - foreach (var damageId in groupProto.DamageTypes) - { - var damageAmount = damageable.Damage.DamageDict.GetValueOrDefault(damageId); - if (damageAmount != FixedPoint2.Zero) - groupDamage.Add(damageId, damageAmount); - } - - var sum = groupDamage.Values.Sum(); - foreach (var (damageId, damageAmount) in groupDamage) - { - var existing = dspec.DamageDict.GetOrNew(damageId); - dspec.DamageDict[damageId] = existing + damageAmount / sum * amount; - } - } - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in dspec.DamageDict) - { - if (val < 0f) - { - dspec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - dspec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - damagableSystem.TryChangeDamage( - args.TargetEntity, - dspec * scale, - IgnoreResistances, - interruptsDoAfters: false); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs b/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs new file mode 100644 index 00000000000..ecad4e47adc --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ExplodeEntityEffect.cs @@ -0,0 +1,33 @@ +using Content.Shared.Database; +using Content.Shared.EntityEffects.Effects.Transform; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// +public sealed partial class Explode : EntityEffectBase +{ + /// + /// Optional override for the explosion intensity. + /// + [DataField] + public float? Intensity; + + /// + /// Optional override for the explosion radius. + /// + [DataField] + public float? Radius; + + /// + /// Delete the entity with the explosion? + /// + [DataField] + public bool Delete = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-explosion", ("chance", Probability)); + + public override LogImpact? Impact => LogImpact.High; +} diff --git a/Content.Shared/EntityEffects/Effects/ExplosionReactionEffect.cs b/Content.Shared/EntityEffects/Effects/ExplosionReactionEffect.cs deleted file mode 100644 index bc9af150925..00000000000 --- a/Content.Shared/EntityEffects/Effects/ExplosionReactionEffect.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Content.Shared.Database; -using Content.Shared.Explosion; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using System.Text.Json.Serialization; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class ExplosionReactionEffect : EventEntityEffect -{ - /// - /// The type of explosion. Determines damage types and tile break chance scaling. - /// - [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string ExplosionType = default!; - - /// - /// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break - /// chance. - /// - [DataField] - public float MaxIntensity = 5; - - /// - /// How quickly intensity drops off as you move away from the epicenter - /// - [DataField] - public float IntensitySlope = 1; - - /// - /// The maximum total intensity that this chemical reaction can achieve. Basically here to prevent people - /// from creating a nuke by collecting enough potassium and water. - /// - /// - /// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles. - /// - [DataField] - public float MaxTotalIntensity = 100; - - /// - /// The intensity of the explosion per unit reaction. - /// - [DataField] - public float IntensityPerUnit = 1; - - /// - /// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger - /// explosives that don't space tiles, without having to create a new explosion-type prototype. - /// - [DataField] - public float TileBreakScale = 1f; - - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-explosion-reaction-effect", ("chance", Probability)); - public override LogImpact LogImpact => LogImpact.High; -} diff --git a/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs b/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs deleted file mode 100644 index 11e776ac908..00000000000 --- a/Content.Shared/EntityEffects/Effects/ExtinguishReaction.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Atmos; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class ExtinguishReaction : EntityEffect - { - /// - /// Amount of firestacks reduced. - /// - [DataField] - public float FireStacksAdjustment = -1.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-extinguish-reaction", ("chance", Probability)); - - public override void Effect(EntityEffectBaseArgs args) - { - var ev = new ExtinguishEvent - { - FireStacksAdjustment = FireStacksAdjustment, - }; - - if (args is EntityEffectReagentArgs reagentArgs) - { - ev.FireStacksAdjustment *= (float)reagentArgs.Quantity; - } - - args.EntityManager.EventBus.RaiseLocalEvent(args.TargetEntity, ref ev); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/FlammableReaction.cs b/Content.Shared/EntityEffects/Effects/FlammableReaction.cs deleted file mode 100644 index 9f7d504f4e3..00000000000 --- a/Content.Shared/EntityEffects/Effects/FlammableReaction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Shared.Database; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class FlammableReaction : EventEntityEffect -{ - [DataField] - public float Multiplier = 0.05f; - - // The fire stack multiplier if fire stacks already exist on target, only works if 0 or greater - [DataField] - public float MultiplierOnExisting = -1f; - - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-flammable-reaction", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; -} diff --git a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs b/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs deleted file mode 100644 index c238e940106..00000000000 --- a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -[DataDefinition] -public sealed partial class FlashReactionEffect : EventEntityEffect -{ - /// - /// Flash range per unit of reagent. - /// - [DataField] - public float RangePerUnit = 0.2f; - - /// - /// Maximum flash range. - /// - [DataField] - public float MaxRange = 10f; - - /// - /// How much to entities are slowed down. - /// - [DataField] - public float SlowTo = 0.5f; - - /// - /// The time entities will be flashed. - /// The default is chosen to be better than the hand flash so it is worth using it for grenades etc. - /// - [DataField] - public TimeSpan Duration = TimeSpan.FromSeconds(4); - - /// - /// The prototype ID used for the visual effect. - /// - [DataField] - public EntProtoId? FlashEffectPrototype = "ReactionFlash"; - - /// - /// The sound the flash creates. - /// - [DataField] - public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg"); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-flash-reaction-effect", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/Glow.cs b/Content.Shared/EntityEffects/Effects/Glow.cs deleted file mode 100644 index 394d406700c..00000000000 --- a/Content.Shared/EntityEffects/Effects/Glow.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Makes a mob glow. -/// -public sealed partial class Glow : EntityEffect -{ - [DataField] - public float Radius = 2f; - - [DataField] - public Color Color = Color.Black; - - private static readonly List Colors = new() - { - Color.White, - Color.Red, - Color.Yellow, - Color.Green, - Color.Blue, - Color.Purple, - Color.Pink - }; - - public override void Effect(EntityEffectBaseArgs args) - { - if (Color == Color.Black) - { - var random = IoCManager.Resolve(); - Color = random.Pick(Colors); - } - - var lightSystem = args.EntityManager.System(); - var light = lightSystem.EnsureLight(args.TargetEntity); - lightSystem.SetRadius(args.TargetEntity, Radius, light); - lightSystem.SetColor(args.TargetEntity, Color, light); - lightSystem.SetCastShadows(args.TargetEntity, false, light); // this is expensive, and botanists make lots of plants - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs new file mode 100644 index 00000000000..05e4fe29ca0 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/GlowEntityEffectSystem.cs @@ -0,0 +1,55 @@ +using Robust.Shared.Network; +using Robust.Shared.Random; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Causes this entity to glow. +/// +/// +public sealed partial class GlowEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPointLightSystem _lightSystem = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var color = args.Effect.Color; + + if (color == Color.Black) + { + // TODO: When we get proper predicted RNG remove this check... + if (_net.IsClient) + return; + + color = _random.Pick(Colors); + } + + var light = _lightSystem.EnsureLight(entity); + _lightSystem.SetRadius(entity, args.Effect.Radius, light); + _lightSystem.SetColor(entity, color, light); + _lightSystem.SetCastShadows(entity, false, light); // this is expensive, and botanists make lots of plants + } + + public static readonly List Colors = new() + { + Color.White, + Color.Red, + Color.Yellow, + Color.Green, + Color.Blue, + Color.Purple, + Color.Pink + }; +} + +/// +public sealed partial class Glow : EntityEffectBase +{ + [DataField] + public float Radius = 2f; + + [DataField] + public Color Color = Color.Black; +} diff --git a/Content.Shared/EntityEffects/Effects/HealthChange.cs b/Content.Shared/EntityEffects/Effects/HealthChange.cs deleted file mode 100644 index 17c24f6b5af..00000000000 --- a/Content.Shared/EntityEffects/Effects/HealthChange.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.EntityEffects; -using Content.Shared.FixedPoint; -using Content.Shared.Localizations; -using Robust.Shared.Prototypes; -using System.Linq; -using System.Text.Json.Serialization; - -namespace Content.Shared.EntityEffects.Effects -{ - /// - /// Default metabolism used for medicine reagents. - /// - public sealed partial class HealthChange : EntityEffect - { - /// - /// Damage to apply every cycle. Damage Ignores resistances. - /// - [DataField(required: true)] - [JsonPropertyName("damage")] - public DamageSpecifier Damage = default!; - - /// - /// Should this effect scale the damage by the amount of chemical in the solution? - /// Useful for touch reactions, like styptic powder or acid. - /// Only usable if the EntityEffectBaseArgs is an EntityEffectReagentArgs. - /// - [DataField] - [JsonPropertyName("scaleByQuantity")] - public bool ScaleByQuantity; - - [DataField] - [JsonPropertyName("ignoreResistances")] - public bool IgnoreResistances = true; - - protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - var damages = new List(); - var heals = false; - var deals = false; - - var damageSpec = new DamageSpecifier(Damage); - - var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; - var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in damageSpec.DamageDict) - { - if (val < 0f) - { - damageSpec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - damageSpec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); - - foreach (var (kind, amount) in damageSpec.DamageDict) - { - var sign = FixedPoint2.Sign(amount); - - if (sign < 0) - heals = true; - if (sign > 0) - deals = true; - - damages.Add( - Loc.GetString("health-change-display", - ("kind", prototype.Index(kind).LocalizedName), - ("amount", MathF.Abs(amount.Float())), - ("deltasign", sign) - )); - } - - var healsordeals = heals ? (deals ? "both" : "heals") : (deals ? "deals" : "none"); - - return Loc.GetString("reagent-effect-guidebook-health-change", - ("chance", Probability), - ("changes", ContentLocalizationManager.FormatList(damages)), - ("healsordeals", healsordeals)); - } - - public override void Effect(EntityEffectBaseArgs args) - { - var scale = FixedPoint2.New(1); - var damageSpec = new DamageSpecifier(Damage); - - if (args is EntityEffectReagentArgs reagentArgs) - { - scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale; - } - - var universalReagentDamageModifier = args.EntityManager.System().UniversalReagentDamageModifier; - var universalReagentHealModifier = args.EntityManager.System().UniversalReagentHealModifier; - - if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1) - { - foreach (var (type, val) in damageSpec.DamageDict) - { - if (val < 0f) - { - damageSpec.DamageDict[type] = val * universalReagentHealModifier; - } - if (val > 0f) - { - damageSpec.DamageDict[type] = val * universalReagentDamageModifier; - } - } - } - - args.EntityManager.System() - .TryChangeDamage( - args.TargetEntity, - damageSpec * scale, - IgnoreResistances, - interruptsDoAfters: false); - } - } -} diff --git a/Content.Shared/EntityEffects/Effects/Ignite.cs b/Content.Shared/EntityEffects/Effects/Ignite.cs deleted file mode 100644 index 707ecc3208b..00000000000 --- a/Content.Shared/EntityEffects/Effects/Ignite.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Database; -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Ignites a mob. -/// -public sealed partial class Ignite : EventEntityEffect -{ - public override bool ShouldLog => true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-ignite", ("chance", Probability)); - - public override LogImpact LogImpact => LogImpact.Medium; -} diff --git a/Content.Shared/EntityEffects/Effects/MakeSentient.cs b/Content.Shared/EntityEffects/Effects/MakeSentient.cs deleted file mode 100644 index 9c70eb452b3..00000000000 --- a/Content.Shared/EntityEffects/Effects/MakeSentient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared.Mind.Components; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class MakeSentient : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-make-sentient", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs b/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs new file mode 100644 index 00000000000..4beb21454ca --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/MakeSentientEntityEffect.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +public sealed partial class MakeSentient : EntityEffectBase +{ + /// + /// Description for the ghost role created by this effect. + /// + [DataField] + public LocId RoleDescription = "ghost-role-information-cognizine-description"; + + /// + /// Whether we give the target the ability to speak coherently. + /// + [DataField] + public bool AllowSpeech = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-make-sentient", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs new file mode 100644 index 00000000000..05fd826f2db --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/MetaData/DestructibleActEntityEffectSystem.cs @@ -0,0 +1,40 @@ +using Content.Shared.Destructible; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.MetaData; + + +/// +public sealed partial class DestructibleActEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedDestructibleSystem _destructible = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + if ((args.Effect.Acts & ThresholdActs.Breakage) != 0) + _destructible.BreakEntity(entity); + + if ((args.Effect.Acts & ThresholdActs.Destruction) != 0) + _destructible.DestroyEntity(entity.AsNullable()); + } +} + +/// +/// Destroys or breaks an entity. +/// +public sealed partial class DestructibleAct : EntityEffectBase +{ + /// + /// What acts should be triggered upon activation. + /// + [DataField] + public ThresholdActs Acts; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + if ((Acts & ThresholdActs.Destruction) != 0) + return Loc.GetString("entity-effect-guidebook-destroy", ("chance", Probability)); + + return Loc.GetString("entity-effect-guidebook-break", ("chance", Probability)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs b/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs deleted file mode 100644 index 9f156520958..00000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyBleedAmount.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyBleedAmount : EventEntityEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public float Amount = -1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-bleed-amount", ("chance", Probability), - ("deltasign", MathF.Sign(Amount))); -} diff --git a/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs b/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs deleted file mode 100644 index 06c026f1284..00000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyBloodLevel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyBloodLevel : EventEntityEffect -{ - [DataField] - public bool Scaled = false; - - [DataField] - public FixedPoint2 Amount = 1.0f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-modify-blood-level", ("chance", Probability), - ("deltasign", MathF.Sign(Amount.Float()))); -} diff --git a/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs b/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs deleted file mode 100644 index 45dc8c84c67..00000000000 --- a/Content.Shared/EntityEffects/Effects/ModifyLungGas.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Shared.Atmos; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class ModifyLungGas : EventEntityEffect -{ - [DataField("ratios", required: true)] - public Dictionary Ratios = default!; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; -} diff --git a/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs b/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs deleted file mode 100644 index 5e72746e327..00000000000 --- a/Content.Shared/EntityEffects/Effects/MovespeedModifier.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Content.Shared.Chemistry.Components; -using Content.Shared.Movement.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Default metabolism for stimulants and tranqs. Attempts to find a MovementSpeedModifier on the target, -/// adding one if not there and to change the movespeed -/// -public sealed partial class MovespeedModifier : EntityEffect -{ - /// - /// How much the entities' walk speed is multiplied by. - /// - [DataField] - public float WalkSpeedModifier { get; set; } = 1; - - /// - /// How much the entities' run speed is multiplied by. - /// - [DataField] - public float SprintSpeedModifier { get; set; } = 1; - - /// - /// How long the modifier applies (in seconds). - /// Is scaled by reagent amount if used with an EntityEffectReagentArgs. - /// - [DataField] - public float StatusLifetime = 2f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return Loc.GetString("reagent-effect-guidebook-movespeed-modifier", - ("chance", Probability), - ("walkspeed", WalkSpeedModifier), - ("time", StatusLifetime)); - } - - /// - /// Remove reagent at set rate, changes the movespeed modifiers and adds a MovespeedModifierMetabolismComponent if not already there. - /// - public override void Effect(EntityEffectBaseArgs args) - { - var status = args.EntityManager.EnsureComponent(args.TargetEntity); - - // Only refresh movement if we need to. - var modified = !status.WalkSpeedModifier.Equals(WalkSpeedModifier) || - !status.SprintSpeedModifier.Equals(SprintSpeedModifier); - - status.WalkSpeedModifier = WalkSpeedModifier; - status.SprintSpeedModifier = SprintSpeedModifier; - - // only going to scale application time - var statusLifetime = StatusLifetime; - - if (args is EntityEffectReagentArgs reagentArgs) - { - statusLifetime *= reagentArgs.Scale.Float(); - } - - IncreaseTimer(status, statusLifetime, args.EntityManager, args.TargetEntity); - - if (modified) - args.EntityManager.System().RefreshMovementSpeedModifiers(args.TargetEntity); - } - private void IncreaseTimer(MovespeedModifierMetabolismComponent status, float time, IEntityManager entityManager, EntityUid uid) - { - var gameTiming = IoCManager.Resolve(); - - var offsetTime = Math.Max(status.ModifierTimer.TotalSeconds, gameTiming.CurTime.TotalSeconds); - - status.ModifierTimer = TimeSpan.FromSeconds(offsetTime + time); - - entityManager.Dirty(uid, status); - } -} diff --git a/Content.Shared/EntityEffects/Effects/NestedEffect.cs b/Content.Shared/EntityEffects/Effects/NestedEffect.cs new file mode 100644 index 00000000000..86b09a068e4 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/NestedEffect.cs @@ -0,0 +1,65 @@ +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Applies the effect of an . +/// +public sealed partial class NestedEffect : EntityEffectBase +{ + /// + /// The effect prototype to use. + /// + [DataField(required: true)] + public ProtoId Proto; + + private List _conditions = new(); + private List _effects = new(); + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var proto = prototype.Index(Proto); + if (proto.GuidebookText is {} key) + return Loc.GetString(key, ("chance", Probability)); + + _effects.Clear(); + foreach (var effect in proto.Effects) + { + if (effect.EntityEffectGuidebookText(prototype, entSys) is not {} text) + continue; + + // basically GuidebookReagentEffectDescription but independent of reagents and no linq + _conditions.Clear(); + if (effect.Conditions is {} conditions) + { + foreach (var condition in conditions) + { + _conditions.Add(condition.EntityConditionGuidebookText(prototype)); + } + } + + var desc = Loc.GetString("guidebook-nested-effect-description", + ("effect", text), + ("chance", effect.Probability), + ("conditionCount", _conditions.Count), + ("conditions", ContentLocalizationManager.FormatList(_conditions))); + _effects.Add(desc); + } + + return _effects.Count == 0 ? null : string.Join("\n", _effects); + } +} + +/// +/// Handles . +/// +public sealed class NestedEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedEntityEffectsSystem _effects = default!; + + protected override void Effect(Entity ent, ref EntityEffectEvent args) + { + _effects.TryApplyEffect(ent, args.Effect.Proto, args.Scale, args.User); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Oxygenate.cs b/Content.Shared/EntityEffects/Effects/Oxygenate.cs deleted file mode 100644 index e990a3fec6a..00000000000 --- a/Content.Shared/EntityEffects/Effects/Oxygenate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Oxygenate : EventEntityEffect -{ - [DataField] - public float Factor = 1f; - - // JUSTIFICATION: This is internal magic that players never directly interact with. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; -} diff --git a/Content.Shared/EntityEffects/Effects/Paralyze.cs b/Content.Shared/EntityEffects/Effects/Paralyze.cs deleted file mode 100644 index 2a2270016b7..00000000000 --- a/Content.Shared/EntityEffects/Effects/Paralyze.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Stunnable; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Paralyze : EntityEffect -{ - [DataField] public double ParalyzeTime = 2; - - /// - /// true - refresh paralyze time, false - accumulate paralyze time - /// - [DataField] public bool Refresh = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString( - "reagent-effect-guidebook-paralyze", - ("chance", Probability), - ("time", ParalyzeTime) - ); - - public override void Effect(EntityEffectBaseArgs args) - { - var paralyzeTime = ParalyzeTime; - - if (args is EntityEffectReagentArgs reagentArgs) - { - paralyzeTime *= (double)reagentArgs.Scale; - } - - var stunSystem = args.EntityManager.System(); - _ = Refresh - ? stunSystem.TryUpdateParalyzeDuration(args.TargetEntity, TimeSpan.FromSeconds(paralyzeTime)) - : stunSystem.TryAddParalyzeDuration(args.TargetEntity, TimeSpan.FromSeconds(paralyzeTime)); - } -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs deleted file mode 100644 index 2c8046452b0..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.EntityEffects; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Diagnostics.CodeAnalysis; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -[ImplicitDataDefinitionForInheritors] -public abstract partial class PlantAdjustAttribute : EventEntityEffect where T : PlantAdjustAttribute -{ - [DataField] - public float Amount { get; protected set; } = 1; - - /// - /// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions. - /// - [DataField] - public abstract string GuidebookAttributeName { get; set; } - - /// - /// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number. - /// - [DataField] - public virtual bool GuidebookIsAttributePositive { get; protected set; } = true; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - string color; - if (GuidebookIsAttributePositive ^ Amount < 0.0) - { - color = "green"; - } - else - { - color = "red"; - } - return Loc.GetString("reagent-effect-guidebook-plant-attribute", ("attribute", Loc.GetString(GuidebookAttributeName)), ("amount", Amount.ToString("0.00")), ("colorName", color), ("chance", Probability)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs deleted file mode 100644 index c1e71894e5f..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustHealth.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustHealth : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-health"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs deleted file mode 100644 index 6610adf708a..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationLevel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustMutationLevel : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-level"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs deleted file mode 100644 index 91be222807f..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustMutationMod.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustMutationMod : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-mutation-mod"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs deleted file mode 100644 index db01d5d060b..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustNutrition.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustNutrition : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-nutrition"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs deleted file mode 100644 index 18c00550d53..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustPests : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-pests"; - public override bool GuidebookIsAttributePositive { get; protected set; } = false; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs deleted file mode 100644 index 971f05f32d0..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustPotency.cs +++ /dev/null @@ -1,12 +0,0 @@ -// using Content.Server.Botany.Systems; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles increase or decrease of plant potency. -/// - -public sealed partial class PlantAdjustPotency : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-potency"; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs deleted file mode 100644 index 9123b5847d6..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustToxins : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-toxins"; - public override bool GuidebookIsAttributePositive { get; protected set; } = false; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs deleted file mode 100644 index 610d02231b8..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWater.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustWater : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-water"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs deleted file mode 100644 index 70ff0747dd3..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-weeds"; - public override bool GuidebookIsAttributePositive { get; protected set; } = false; -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs deleted file mode 100644 index 36b8f57d08d..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantAffectGrowth.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantAffectGrowth : PlantAdjustAttribute -{ - public override string GuidebookAttributeName { get; set; } = "plant-attribute-growth"; -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs deleted file mode 100644 index 66cfb66d4b1..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantChangeStat.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantChangeStat : EventEntityEffect -{ - [DataField] - public string TargetValue; - - [DataField] - public float MinValue; - - [DataField] - public float MaxValue; - - [DataField] - public int Steps; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - throw new NotImplementedException(); - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs deleted file mode 100644 index 0dabf1f22b2..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantCryoxadone.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantCryoxadone : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-cryoxadone", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs deleted file mode 100644 index 9f16c354020..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDestroySeeds.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles removal of seeds on a plant. -/// - -public sealed partial class PlantDestroySeeds : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-plant-seeds-remove", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs deleted file mode 100644 index 9feccbf2245..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantDiethylamine.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantDiethylamine : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-diethylamine", ("chance", Probability)); -} - diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs deleted file mode 100644 index 9dc5140063a..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantPhalanximine.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class PlantPhalanximine : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-phalanximine", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs deleted file mode 100644 index 51ce353dbb9..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/PlantRestoreSeeds.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -/// -/// Handles restoral of seeds on a plant. -/// -public sealed partial class PlantRestoreSeeds : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-plant-seeds-add", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs b/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs deleted file mode 100644 index 6ba37c9be0b..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMetabolism/RobustHarvest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects.PlantMetabolism; - -public sealed partial class RobustHarvest : EventEntityEffect -{ - [DataField] - public int PotencyLimit = 50; - - [DataField] - public int PotencyIncrease = 3; - - [DataField] - public int PotencySeedlessThreshold = 30; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString("reagent-effect-guidebook-plant-robust-harvest", ("seedlesstreshold", PotencySeedlessThreshold), ("limit", PotencyLimit), ("increase", PotencyIncrease), ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs b/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs deleted file mode 100644 index 9a3408bb8ee..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateChemicals.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Random; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// changes the chemicals available in a plant's produce -/// -public sealed partial class PlantMutateChemicals : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs b/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs deleted file mode 100644 index 5eb5b1d07cf..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateGases.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Linq; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// changes the gases that a plant or produce create. -/// -public sealed partial class PlantMutateExudeGasses : EventEntityEffect -{ - [DataField] - public float MinValue = 0.01f; - - [DataField] - public float MaxValue = 0.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} - -/// -/// changes the gases that a plant or produce consumes. -/// -public sealed partial class PlantMutateConsumeGasses : EventEntityEffect -{ - [DataField] - public float MinValue = 0.01f; - - [DataField] - public float MaxValue = 0.5f; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs b/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs deleted file mode 100644 index 84d62931852..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantMutateHarvest.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Upgrades a plant's harvest type. -/// -public sealed partial class PlantMutateHarvest : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs b/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs deleted file mode 100644 index e2acc4cb7cd..00000000000 --- a/Content.Shared/EntityEffects/Effects/PlantSpeciesChange.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Changes a plant into one of the species its able to mutate into. -/// -public sealed partial class PlantSpeciesChange : EventEntityEffect -{ - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - return "TODO"; - } -} diff --git a/Content.Shared/EntityEffects/Effects/Polymorph.cs b/Content.Shared/EntityEffects/Effects/Polymorph.cs deleted file mode 100644 index 65711ff99a9..00000000000 --- a/Content.Shared/EntityEffects/Effects/Polymorph.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Polymorph; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.EntityEffects.Effects; - -public sealed partial class Polymorph : EventEntityEffect -{ - /// - /// What polymorph prototype is used on effect - /// - [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string PolymorphPrototype { get; set; } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-make-polymorph", - ("chance", Probability), ("entityname", - prototype.Index(prototype.Index(PolymorphPrototype).Configuration.Entity).Name)); -} diff --git a/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs b/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs new file mode 100644 index 00000000000..c0d80dffaaf --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/PolymorphEntityEffect.cs @@ -0,0 +1,19 @@ +using Content.Shared.Polymorph; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +public sealed partial class Polymorph : EntityEffectBase +{ + /// + /// What polymorph prototype is used on effect + /// + [DataField(required: true)] + public ProtoId Prototype; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-make-polymorph", + ("chance", Probability), + ("entityname", prototype.Index(prototype.Index(Prototype).Configuration.Entity).Name)); +} diff --git a/Content.Shared/EntityEffects/Effects/PopupMessage.cs b/Content.Shared/EntityEffects/Effects/PopupMessage.cs deleted file mode 100644 index a837f816e46..00000000000 --- a/Content.Shared/EntityEffects/Effects/PopupMessage.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared.Popups; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Shared.EntityEffects.Effects -{ - public sealed partial class PopupMessage : EntityEffect - { - [DataField(required: true)] - public string[] Messages = default!; - - [DataField] - public PopupRecipients Type = PopupRecipients.Local; - - [DataField] - public PopupType VisualType = PopupType.Small; - - // JUSTIFICATION: This is purely cosmetic. - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => null; - - public override void Effect(EntityEffectBaseArgs args) - { - var popupSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - var random = IoCManager.Resolve(); - - var msg = random.Pick(Messages); - var msgArgs = new (string, object)[] - { - ("entity", args.TargetEntity), - }; - - if (args is EntityEffectReagentArgs reagentArgs) - { - msgArgs = new (string, object)[] - { - ("entity", reagentArgs.TargetEntity), - ("organ", reagentArgs.OrganEntity.GetValueOrDefault()), - }; - } - - if (Type == PopupRecipients.Local) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, args.TargetEntity, VisualType); - else if (Type == PopupRecipients.Pvs) - popupSys.PopupEntity(Loc.GetString(msg, msgArgs), args.TargetEntity, VisualType); - } - } - - public enum PopupRecipients - { - Pvs, - Local - } -} diff --git a/Content.Shared/EntityEffects/Effects/ReduceRotting.cs b/Content.Shared/EntityEffects/Effects/ReduceRotting.cs deleted file mode 100644 index b5f2a7ae7ef..00000000000 --- a/Content.Shared/EntityEffects/Effects/ReduceRotting.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; -using Content.Shared.Atmos.Rotting; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Reduces the rotting accumulator on the patient, making them revivable. -/// -public sealed partial class ReduceRotting : EntityEffect -{ - [DataField("seconds")] - public double RottingAmount = 10; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-reduce-rotting", - ("chance", Probability), - ("time", RottingAmount)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - if (reagentArgs.Scale != 1f) - return; - } - - var rottingSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - rottingSys.ReduceAccumulator(args.TargetEntity, TimeSpan.FromSeconds(RottingAmount)); - } -} diff --git a/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs b/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs deleted file mode 100644 index 009cf914d5d..00000000000 --- a/Content.Shared/EntityEffects/Effects/ResetNarcolepsy.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Reset narcolepsy timer -/// -public sealed partial class ResetNarcolepsy : EventEntityEffect -{ - /// - /// The # of seconds the effect resets the narcolepsy timer to - /// - [DataField("TimerReset")] - public TimeSpan TimerReset = TimeSpan.FromSeconds(600); - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-reset-narcolepsy", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs new file mode 100644 index 00000000000..973b139fa48 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/ResetNarcolepsyEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Traits.Assorted; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// Resets the narcolepsy timer on a given entity. +/// The new duration of the timer is modified by scale. +/// +/// +public sealed partial class ResetNarcolepsyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly NarcolepsySystem _narcolepsy = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var timer = args.Effect.TimerReset * args.Scale; + + _narcolepsy.AdjustNarcolepsyTimer(entity.AsNullable(), timer); + } +} + +/// +public sealed partial class ResetNarcolepsy : EntityEffectBase +{ + /// + /// The time we set our narcolepsy timer to. + /// + [DataField("TimerReset")] + public TimeSpan TimerReset = TimeSpan.FromSeconds(600); + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-reset-narcolepsy", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/SatiateHunger.cs b/Content.Shared/EntityEffects/Effects/SatiateHunger.cs deleted file mode 100644 index 3e7af8833c7..00000000000 --- a/Content.Shared/EntityEffects/Effects/SatiateHunger.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Attempts to find a HungerComponent on the target, -/// and to update it's hunger values. -/// -public sealed partial class SatiateHunger : EntityEffect -{ - private const float DefaultNutritionFactor = 3.0f; - - /// - /// How much hunger is satiated. - /// Is multiplied by quantity if used with EntityEffectReagentArgs. - /// - [DataField("factor")] public float NutritionFactor { get; set; } = DefaultNutritionFactor; - - //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - public override void Effect(EntityEffectBaseArgs args) - { - var entman = args.EntityManager; - if (!entman.TryGetComponent(args.TargetEntity, out HungerComponent? hunger)) - return; - if (args is EntityEffectReagentArgs reagentArgs) - { - entman.System().ModifyHunger(reagentArgs.TargetEntity, NutritionFactor * (float) reagentArgs.Quantity, hunger); - } - else - { - entman.System().ModifyHunger(args.TargetEntity, NutritionFactor, hunger); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-satiate-hunger", ("chance", Probability), ("relative", NutritionFactor / DefaultNutritionFactor)); -} diff --git a/Content.Shared/EntityEffects/Effects/SatiateThirst.cs b/Content.Shared/EntityEffects/Effects/SatiateThirst.cs deleted file mode 100644 index 21a055b528c..00000000000 --- a/Content.Shared/EntityEffects/Effects/SatiateThirst.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, -/// and to update it's thirst values. -/// -public sealed partial class SatiateThirst : EntityEffect -{ - private const float DefaultHydrationFactor = 3.0f; - - /// How much thirst is satiated each tick. Not currently tied to - /// rate or anything. - [DataField("factor")] - public float HydrationFactor { get; set; } = DefaultHydrationFactor; - - /// Satiate thirst if a ThirstComponent can be found - public override void Effect(EntityEffectBaseArgs args) - { - var uid = args.TargetEntity; - if (args.EntityManager.TryGetComponent(uid, out ThirstComponent? thirst)) - args.EntityManager.System().ModifyThirst(uid, thirst, HydrationFactor); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", HydrationFactor / DefaultHydrationFactor)); -} diff --git a/Content.Shared/EntityEffects/Effects/Slipify.cs b/Content.Shared/EntityEffects/Effects/Slipify.cs deleted file mode 100644 index c152b8b0100..00000000000 --- a/Content.Shared/EntityEffects/Effects/Slipify.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared.Physics; -using Content.Shared.Slippery; -using Content.Shared.StepTrigger.Components; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Makes a mob slippery. -/// -public sealed partial class Slipify : EntityEffect -{ - public override void Effect(EntityEffectBaseArgs args) - { - var fixtureSystem = args.EntityManager.System(); - var colWakeSystem = args.EntityManager.System(); - var slippery = args.EntityManager.EnsureComponent(args.TargetEntity); - args.EntityManager.Dirty(args.TargetEntity, slippery); - args.EntityManager.EnsureComponent(args.TargetEntity); - // Need a fixture with a slip layer in order to actually do the slipping - var fixtures = args.EntityManager.EnsureComponent(args.TargetEntity); - var body = args.EntityManager.EnsureComponent(args.TargetEntity); - var shape = fixtures.Fixtures["fix1"].Shape; - fixtureSystem.TryCreateFixture(args.TargetEntity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: fixtures, body: body); - // Need to disable collision wake so that mobs can collide with and slip on it - var collisionWake = args.EntityManager.EnsureComponent(args.TargetEntity); - colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - { - throw new NotImplementedException(); - } -} diff --git a/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs new file mode 100644 index 00000000000..602e5e923a8 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/SlipifyEntityEffectSystem.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Content.Shared.Physics; +using Content.Shared.Slippery; +using Content.Shared.StepTrigger.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// This effect permanently creates a slippery fixture for this entity and then makes this entity slippery like soap. +/// +/// +public sealed partial class SlipifyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly CollisionWakeSystem _collisionWake = default!; + [Dependency] private readonly FixtureSystem _fixture = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + EnsureComp(entity, out var slippery); + slippery.SlipData = args.Effect.Slippery; + + Dirty(entity, slippery); + + EnsureComp(entity); + + if (entity.Comp.Fixtures.FirstOrDefault(x => x.Value.Hard).Value.Shape is not { } shape) + return; + + _fixture.TryCreateFixture(entity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: entity.Comp); + + // Need to disable collision wake so that mobs can collide with and slip on it + EnsureComp(entity, out var collisionWake); + _collisionWake.SetEnabled(entity, false, collisionWake); + } +} + +/// +public sealed partial class Slipify : EntityEffectBase +{ + [DataField] + public SlipperyEffectEntry Slippery = new(); +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs new file mode 100644 index 00000000000..ef96723cfff --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AddReagentToSolutionEntityEffectSystem.cs @@ -0,0 +1,59 @@ +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +// TODO: This should be removed and changed to an "AbsorbentSolutionComponent" +/// +/// Creates a reagent in a specified solution owned by this entity. +/// Quantity is modified by scale. +/// +/// +public sealed class AddReagentToSolutionEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = args.Effect.Solution; + var reagent = args.Effect.Reagent; + + if (!_solutionContainer.TryGetSolution((entity, entity), solution, out var solutionContainer)) + return; + + _solutionContainer.TryAddReagent(solutionContainer.Value, reagent, args.Scale * args.Effect.StrengthModifier); + } +} + +/// +public sealed partial class AddReagentToSolution : EntityEffectBase +{ + /// + /// Prototype of the reagent we're adding. + /// + [DataField(required: true)] + public ProtoId Reagent; + + /// + /// Solution we're looking for + /// + [DataField(required: true)] + public string? Solution = "reagents"; + + /// + /// A modifier for how much reagent we're creating. + /// + [DataField] + public float StrengthModifier = 1.0f; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return prototype.Resolve(Reagent, out ReagentPrototype? proto) + ? Loc.GetString("entity-effect-guidebook-add-to-solution-reaction", + ("chance", Probability), + ("reagent", proto.LocalizedName)) + : null; + } +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs new file mode 100644 index 00000000000..334a63a95a9 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AdjustReagentEntityEffectSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +/// +/// Adjust a reagent in this solution by an amount modified by scale. +/// Quantity is modified by scale. +/// +/// +public sealed partial class AdjustReagentEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var quantity = args.Effect.Amount * args.Scale; + var reagent = args.Effect.Reagent; + + if (quantity > 0) + _solutionContainer.TryAddReagent(entity, reagent, quantity); + else + _solutionContainer.RemoveReagent(entity, reagent, -quantity); + } +} + +/// +public sealed partial class AdjustReagent : EntityEffectBase +{ + /// + /// The reagent ID to add or remove. + /// + [DataField(required: true)] + public ProtoId Reagent; + + [DataField(required: true)] + public FixedPoint2 Amount; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return prototype.Resolve(Reagent, out ReagentPrototype? proto) + ? Loc.GetString("entity-effect-guidebook-adjust-reagent-reagent", + ("chance", Probability), + ("deltasign", MathF.Sign(Amount.Float())), + ("reagent", proto.LocalizedName), + ("amount", MathF.Abs(Amount.Float()))) + : null; + } +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs new file mode 100644 index 00000000000..13175cfed1c --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/AreaReactionEntityEffect.cs @@ -0,0 +1,37 @@ +using Content.Shared.Database; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +/// +public sealed partial class AreaReactionEffect : EntityEffectBase +{ + /// + /// How many seconds will the effect stay, counting after fully spreading. + /// + [DataField("duration")] public float Duration = 10; + + /// + /// How big of a reaction scale we need for 1 smoke entity. + /// + [DataField] public float OverflowThreshold = 2.5f; + + /// + /// The entity prototype that is being spread over an area. + /// + [DataField(required: true)] + public EntProtoId PrototypeId; + + /// + /// Sound that will get played when this reaction effect occurs. + /// + [DataField(required: true)] public SoundSpecifier Sound = default!; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-area-reaction", + ("duration", Duration) + ); + + public override LogImpact? Impact => LogImpact.High; +} diff --git a/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs b/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs new file mode 100644 index 00000000000..c0188fdfb45 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Solution/SolutionTemperatureEntityEffectsSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Solution; + +// TODO: Energy conservation!!! Once HeatContainers are merged nuke this and everything in SolutionContainerSystem to respect energy conservation! +/// +/// Sets the temperature of this solution to a fixed value. +/// +/// +public sealed class SetSolutionTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + _solutionContainer.SetTemperature(entity, args.Effect.Temperature); + } +} + +/// +public sealed partial class SetSolutionTemperature : EntityEffectBase +{ + /// + /// The temperature to set the solution to. + /// + [DataField(required: true)] + public float Temperature; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-set-solution-temperature-effect", + ("chance", Probability), + ("temperature", Temperature)); +} + +/// +/// Adjusts the temperature of this solution by a given amount. +/// The temperature adjustment is modified by scale. +/// +/// +public sealed class AdjustSolutionTemperatureEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = entity.Comp.Solution; + var temperature = Math.Clamp(solution.Temperature + args.Scale * args.Effect.Delta, args.Effect.MinTemp, args.Effect.MaxTemp); + + _solutionContainer.SetTemperature(entity, temperature); + } +} + +/// +public sealed partial class AdjustSolutionTemperature : EntityEffectBase +{ + /// + /// The change in temperature. + /// + [DataField(required: true)] + public float Delta; + + /// + /// The minimum temperature this effect can reach. + /// + [DataField] + public float MinTemp; + + /// + /// The maximum temperature this effect can reach. + /// + [DataField] + public float MaxTemp = float.PositiveInfinity; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), + ("deltasign", MathF.Sign(Delta)), + ("mintemp", MinTemp), + ("maxtemp", MaxTemp)); +} + +/// +/// Adjusts the thermal energy of this solution by a given amount. +/// The energy adjustment is modified by scale. +/// +/// +public sealed class AdjustSolutionThermalEnergyEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var solution = entity.Comp.Solution; + + var delta = args.Scale * args.Effect.Delta; + + // Don't adjust thermal energy if we're already at or above max temperature. + switch (delta) + { + case > 0: + if (solution.Temperature >= args.Effect.MaxTemp) + return; + break; + case < 0: + if (solution.Temperature <= args.Effect.MinTemp) + return; + break; + default: + return; + } + + _solutionContainer.AddThermalEnergyClamped(entity, delta, args.Effect.MinTemp, args.Effect.MaxTemp); + } +} + +/// +public sealed partial class AdjustSolutionThermalEnergy : EntityEffectBase +{ + /// + /// The change in thermal energy. + /// + [DataField(required: true)] + public float Delta; + + /// + /// The minimum temperature this effect can reach. + /// + [DataField] + public float MinTemp; + + /// + /// The maximum temperature this effect can reach. + /// + [DataField] + public float MaxTemp = float.PositiveInfinity; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-adjust-solution-temperature-effect", + ("chance", Probability), + ("deltasign", MathF.Sign(Delta)), + ("mintemp", MinTemp), + ("maxtemp", MaxTemp)); +} diff --git a/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs b/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs deleted file mode 100644 index 30ac6c3d77d..00000000000 --- a/Content.Shared/EntityEffects/Effects/SolutionTemperatureEffects.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects; - -/// -/// Sets the temperature of the solution involved with the reaction to a new value. -/// -[DataDefinition] -public sealed partial class SetSolutionTemperatureEffect : EntityEffect -{ - /// - /// The temperature to set the solution to. - /// - [DataField("temperature", required: true)] private float _temperature; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-set-solution-temperature-effect", - ("chance", Probability), ("temperature", _temperature)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null) - return; - - solution.Temperature = _temperature; - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } -} - -/// -/// Adjusts the temperature of the solution involved in the reaction. -/// -[DataDefinition] -public sealed partial class AdjustSolutionTemperatureEffect : EntityEffect -{ - /// - /// The change in temperature. - /// - [DataField("delta", required: true)] private float _delta; - - /// - /// The minimum temperature this effect can reach. - /// - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// - /// The maximum temperature this effect can reach. - /// - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// - [DataField("scaled")] private bool _scaled; - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null || solution.Volume == 0) - return; - - var deltaT = _scaled ? _delta * (float) reagentArgs.Quantity : _delta; - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } -} - -/// -/// Adjusts the thermal energy of the solution involved in the reaction. -/// -public sealed partial class AdjustSolutionThermalEnergyEffect : EntityEffect -{ - /// - /// The change in energy. - /// - [DataField("delta", required: true)] private float _delta; - - /// - /// The minimum temperature this effect can reach. - /// - [DataField("minTemp")] private float _minTemp = 0.0f; - - /// - /// The maximum temperature this effect can reach. - /// - [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity; - - /// - /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount. - /// - [DataField("scaled")] private bool _scaled; - - public override void Effect(EntityEffectBaseArgs args) - { - if (args is EntityEffectReagentArgs reagentArgs) - { - var solution = reagentArgs.Source; - if (solution == null || solution.Volume == 0) - return; - - if (_delta > 0 && solution.Temperature >= _maxTemp) - return; - if (_delta < 0 && solution.Temperature <= _minTemp) - return; - - var heatCap = solution.GetHeatCapacity(null); - var deltaT = _scaled - ? _delta / heatCap * (float) reagentArgs.Quantity - : _delta / heatCap; - - solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp); - - return; - } - - // TODO: Someone needs to figure out how to do this for non-reagent effects. - throw new NotImplementedException(); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect", - ("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp)); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs new file mode 100644 index 00000000000..b028da1595a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/BaseStatusEffectEntityEffect.cs @@ -0,0 +1,35 @@ +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Entity effect that specifically deals with new status effects. +/// +/// The entity effect type, typically for status effects which need systems to pass arguments +public abstract partial class BaseStatusEntityEffect : EntityEffectBase where T : BaseStatusEntityEffect +{ + /// + /// How long the modifier applies (in seconds). + /// Is scaled by reagent amount if used with an EntityEffectReagentArgs. + /// + [DataField] + public TimeSpan? Time = TimeSpan.FromSeconds(2); + + /// + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Update; + + /// + /// Delay before the effect starts. If another effect is added with a shorter delay, it takes precedence. + /// + [DataField] + public TimeSpan Delay; +} + +public enum StatusEffectMetabolismType +{ + Update, + Add, + Remove, + Set, +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs new file mode 100644 index 00000000000..553300be723 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/DrunkEntityEffectSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Drunk; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies the drunk status effect to this entity. +/// The duration of the effect is equal to modified by scale. +/// +/// +public sealed partial class DrunkEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedDrunkSystem _drunk = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var boozePower = args.Effect.BoozePower * args.Scale; + + _drunk.TryApplyDrunkenness(entity, boozePower); + } +} + +/// +public sealed partial class Drunk : EntityEffectBase +{ + /// + /// BoozePower is how long each metabolism cycle will make the drunk effect last for. + /// + [DataField] + public TimeSpan BoozePower = TimeSpan.FromSeconds(3f); + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-drunk", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs new file mode 100644 index 00000000000..a5259257829 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ElectrocuteEntityEffectSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Electrocution; +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Electrocutes this entity for a given amount of damage and time. +/// The shock damage applied by this effect is modified by scale. +/// +/// +public sealed partial class ElectrocuteEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedElectrocutionSystem _electrocution = default!; + + // TODO: When electrocution is new status, change this to new status + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var effect = args.Effect; + + _electrocution.TryDoElectrocution(entity, + null, + (int)(args.Scale * effect.ShockDamage), + effect.ElectrocuteTime, + effect.Refresh, + siemensCoefficient: effect.SiemensCoefficient, + ignoreInsulation: effect.BypassInsulation); + } +} + +/// +public sealed partial class Electrocute : EntityEffectBase +{ + /// + /// Time we electrocute this entity + /// + [DataField] + public TimeSpan ElectrocuteTime = TimeSpan.FromSeconds(2); + + /// + /// Shock damage we apply to the entity. + /// + [DataField] + public int ShockDamage = 5; + + /// + /// Do we refresh the duration? Or add more duration if it already exists. + /// + [DataField] + public bool Refresh = true; + + /// + /// Should we by bypassing insulation? + /// + [DataField] + public bool BypassInsulation = true; + + /// + /// How much electricity is being passed through the body basically. Lower means less oomph. + /// + [DataField] + public float SiemensCoefficient = 1f; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("entity-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime.TotalSeconds), ("stuns", SiemensCoefficient > 0.5f)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs deleted file mode 100644 index b7700236040..00000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.StatusEffect; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Adds a generic status effect to the entity, -/// not worrying about things like how to affect the time it lasts for -/// or component fields or anything. Just adds a component to an entity -/// for a given time. Easy. -/// -/// -/// Can be used for things like adding accents or something. I don't know. Go wild. -/// -[Obsolete("Use ModifyStatusEffect with StatusEffectNewSystem instead")] -public sealed partial class GenericStatusEffect : EntityEffect -{ - [DataField(required: true)] - public string Key = default!; - - [DataField] - public string Component = String.Empty; - - [DataField] - public float Time = 2.0f; - - /// - /// true - refresh status effect time, false - accumulate status effect time - /// - [DataField] - public bool Refresh = true; - - /// - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - public override void Effect(EntityEffectBaseArgs args) - { - var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - if (Type == StatusEffectMetabolismType.Add && Component != String.Empty) - { - statusSys.TryAddStatusEffect(args.TargetEntity, Key, TimeSpan.FromSeconds(time), Refresh, Component); - } - else if (Type == StatusEffectMetabolismType.Remove) - { - statusSys.TryRemoveTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); - } - else if (Type == StatusEffectMetabolismType.Set) - { - statusSys.TrySetTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time)); - } - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( - "reagent-effect-guidebook-status-effect", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", $"reagent-effect-status-effect-{Key}")); -} - -public enum StatusEffectMetabolismType -{ - Add, - Remove, - Set -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs new file mode 100644 index 00000000000..2d870a433c2 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffectEntityEffectSystem.cs @@ -0,0 +1,64 @@ +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a Generic Status Effect to this entity, which is a timed Component. +/// The amount of time the Component is applied is modified by scale. +/// +/// +[Obsolete("Use ModifyStatusEffect instead")] +public sealed partial class GenericStatusEffectEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + if (args.Effect.Component != String.Empty) + _status.TryAddStatusEffect(entity, args.Effect.Key, time, true, args.Effect.Component); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Component != String.Empty) + _status.TryAddStatusEffect(entity, args.Effect.Key, time, false, args.Effect.Component); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.Key, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetTime(entity, args.Effect.Key, time); + break; + } + } +} + +/// +public sealed partial class GenericStatusEffect : EntityEffectBase +{ + [DataField(required: true)] + public string Key = default!; + + [DataField] + public string Component = String.Empty; + + [DataField] + public TimeSpan Time = TimeSpan.FromSeconds(2f); + + /// + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Update; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => Loc.GetString( + "entity-effect-guidebook-status-effect-old", + ("chance", Probability), + ("type", Type), + ("time", Time.TotalSeconds), + ("key", $"entity-effect-status-effect-{Key}")); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs deleted file mode 100644 index 891c791b07b..00000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/Jitter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Jittering; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Adds the jitter status effect to a mob. -/// This doesn't use generic status effects because it needs to -/// take in some parameters that JitterSystem needs. -/// -public sealed partial class Jitter : EntityEffect -{ - [DataField] - public float Amplitude = 10.0f; - - [DataField] - public float Frequency = 4.0f; - - [DataField] - public float Time = 2.0f; - - /// - /// true - refresh jitter time, false - accumulate jitter time - /// - [DataField] - public bool Refresh = true; - - public override void Effect(EntityEffectBaseArgs args) - { - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - args.EntityManager.EntitySysManager.GetEntitySystem() - .DoJitter(args.TargetEntity, TimeSpan.FromSeconds(time), Refresh, Amplitude, Frequency); - } - - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => - Loc.GetString("reagent-effect-guidebook-jittering", ("chance", Probability)); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs new file mode 100644 index 00000000000..3b445da7475 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/JitterEntityEffectSystem.cs @@ -0,0 +1,45 @@ +using Content.Shared.Jittering; +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +// TODO: When Jittering is moved to new Status, make this use StatusEffectsContainerComponent. +/// +/// Applies the Jittering Status Effect to this entity. +/// The amount of time the Jittering is applied is modified by scale. +/// +/// +public sealed partial class JitterEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedJitteringSystem _jittering = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + _jittering.DoJitter(entity, TimeSpan.FromSeconds(time), args.Effect.Refresh, args.Effect.Amplitude, args.Effect.Frequency); + } +} + +/// +public sealed partial class Jitter : EntityEffectBase +{ + [DataField] + public float Amplitude = 10.0f; + + [DataField] + public float Frequency = 4.0f; + + [DataField] + public float Time = 2.0f; + + /// + /// true - refresh jitter time, false - accumulate jitter time + /// + [DataField] + public bool Refresh = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Loc.GetString("entity-effect-guidebook-jittering", ("chance", Probability)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs new file mode 100644 index 00000000000..5e01774ce43 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyKnockdownEntityEffectSystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Standing; +using Content.Shared.Stunnable; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies knockdown to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyKnockdownEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly SharedStunSystem _stun = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, time, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, drop: args.Effect.Drop); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, time, false, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, false, drop: args.Effect.Drop); + break; + case StatusEffectMetabolismType.Remove: + _stun.AddKnockdownTime(entity.Owner, - time ?? TimeSpan.Zero); + break; + case StatusEffectMetabolismType.Set: + if (args.Effect.Crawling) + _stun.TryCrawling(entity.Owner, drop: args.Effect.Drop); + else + _stun.TryKnockdown(entity.Owner, time, drop: args.Effect.Drop); + _stun.SetKnockdownTime(entity.Owner, time ?? TimeSpan.Zero); + break; + } + } +} + +/// +public sealed partial class ModifyKnockdown : BaseStatusEntityEffect +{ + /// + /// Should we only affect those with crawler component? Note if this is false, it will paralyze non-crawler's instead. + /// + [DataField] + public bool Crawling; + + /// + /// Should we drop items when we fall? + /// + [DataField] + public bool Drop; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null + : Loc.GetString( + "entity-effect-guidebook-knockdown", + ("chance", Probability), + ("type", Type), + ("time", Time.Value.TotalSeconds) + ); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs new file mode 100644 index 00000000000..c6260eb71dd --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyParalysisEntityEffectSystem.cs @@ -0,0 +1,50 @@ +using Content.Shared.StatusEffectNew; +using Content.Shared.Stunnable; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies the paralysis status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyParalysisEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _stun.TryUpdateParalyzeDuration(entity, time); + break; + case StatusEffectMetabolismType.Add: + _stun.TryAddParalyzeDuration(entity, time); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, SharedStunSystem.StunId, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, SharedStunSystem.StunId, time); + break; + } + } +} + +/// +public sealed partial class ModifyParalysis : BaseStatusEntityEffect +{ + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null // Not gonna make a whole new looc for something that shouldn't ever exist. + : Loc.GetString( + "entity-effect-guidebook-paralyze", + ("chance", Probability), + ("time", Time.Value.TotalSeconds) + ); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs deleted file mode 100644 index a232d34925d..00000000000 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Content.Shared.StatusEffectNew; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityEffects.Effects.StatusEffects; - -/// -/// Changes status effects on entities: Adds, removes or sets time. -/// -[UsedImplicitly] -public sealed partial class ModifyStatusEffect : EntityEffect -{ - [DataField(required: true)] - public EntProtoId EffectProto; - - /// - /// Time for which status effect should be applied. Behaviour changes according to . - /// - [DataField] - public float Time = 2.0f; - - /// - /// true - refresh status effect time (update to greater value), false - accumulate status effect time. - /// - [DataField] - public bool Refresh = true; - - /// - /// Should this effect add the status effect, remove time from it, or set its cooldown? - /// - [DataField] - public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; - - /// - public override void Effect(EntityEffectBaseArgs args) - { - var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem(); - - var time = Time; - if (args is EntityEffectReagentArgs reagentArgs) - time *= reagentArgs.Scale.Float(); - - var duration = TimeSpan.FromSeconds(time); - switch (Type) - { - case StatusEffectMetabolismType.Add: - if (Refresh) - statusSys.TryUpdateStatusEffectDuration(args.TargetEntity, EffectProto, duration); - else - statusSys.TryAddStatusEffectDuration(args.TargetEntity, EffectProto, duration); - break; - case StatusEffectMetabolismType.Remove: - statusSys.TryAddTime(args.TargetEntity, EffectProto, -duration); - break; - case StatusEffectMetabolismType.Set: - statusSys.TrySetStatusEffectDuration(args.TargetEntity, EffectProto, duration); - break; - } - } - - /// - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString( - "reagent-effect-guidebook-status-effect", - ("chance", Probability), - ("type", Type), - ("time", Time), - ("key", prototype.Index(EffectProto).Name) - ); -} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs new file mode 100644 index 00000000000..b42c3f29501 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffectEntityEffectSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a given status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class ModifyStatusEffectEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var time = args.Effect.Time * args.Scale; + var delay = args.Effect.Delay; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _status.TryUpdateStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + case StatusEffectMetabolismType.Add: + if (time != null) + _status.TryAddStatusEffectDuration(entity, args.Effect.EffectProto, time.Value, delay); + else + _status.TryUpdateStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.EffectProto, time); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, args.Effect.EffectProto, time, delay); + break; + } + } +} + +/// +public sealed partial class ModifyStatusEffect : BaseStatusEntityEffect +{ + /// + /// Prototype of the status effect we're modifying. + /// + [DataField(required: true)] + public EntProtoId EffectProto; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? Loc.GetString( + "entity-effect-guidebook-status-effect-indef", + ("chance", Probability), + ("type", Type), + ("key", prototype.Index(EffectProto).Name), + ("delay", Delay.TotalSeconds)) + : Loc.GetString( + "entity-effect-guidebook-status-effect", + ("chance", Probability), + ("type", Type), + ("time", Time.Value.TotalSeconds), + ("key", prototype.Index(EffectProto).Name), + ("delay", Delay.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs new file mode 100644 index 00000000000..ed9923f856a --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/MovementSpeedModifierEntityEffectSystem.cs @@ -0,0 +1,92 @@ +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.StatusEffectNew; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Applies a given movement speed modifier status effect to this entity. +/// Duration is modified by scale. +/// +/// +public sealed partial class MovementSpeedModifierEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _status = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + var proto = args.Effect.EffectProto; + var sprintMod = args.Effect.SprintSpeedModifier; + var walkMod = args.Effect.WalkSpeedModifier; + + switch (args.Effect.Type) + { + case StatusEffectMetabolismType.Update: + _movementModStatus.TryUpdateMovementSpeedModDuration( + entity, + proto, + args.Effect.Time * args.Scale, + sprintMod, + walkMod); + break; + case StatusEffectMetabolismType.Add: + if (args.Effect.Time != null) + { + _movementModStatus.TryAddMovementSpeedModDuration( + entity, + proto, + args.Effect.Time.Value * args.Scale, + sprintMod, + walkMod); + } + else + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + entity, + proto, + args.Effect.Time * args.Scale, + sprintMod, + walkMod); + } + break; + case StatusEffectMetabolismType.Remove: + _status.TryRemoveTime(entity, args.Effect.EffectProto, args.Effect.Time * args.Scale); + break; + case StatusEffectMetabolismType.Set: + _status.TrySetStatusEffectDuration(entity, proto, args.Effect.Time * args.Scale); + break; + } + } +} + +/// +public sealed partial class MovementSpeedModifier : BaseStatusEntityEffect +{ + /// + /// How much the entities' walk speed is multiplied by. + /// + [DataField] + public float WalkSpeedModifier = 1f; + + /// + /// How much the entities' run speed is multiplied by. + /// + [DataField] + public float SprintSpeedModifier = 1f; + + /// + /// Movement speed modifier prototype we're adding. Adding in case we ever want more than one prototype that boosts speed. + /// + [DataField] + public EntProtoId EffectProto = MovementModStatusSystem.ReagentSpeed; + + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Time == null + ? null // Not gonna make a whole new looc for something that shouldn't ever exist. + : Loc.GetString("entity-effect-guidebook-movespeed-modifier", + ("chance", Probability), + ("sprintspeed", SprintSpeedModifier), + ("time", Time.Value.TotalSeconds)); +} diff --git a/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs new file mode 100644 index 00000000000..ea7c2dc8d4b --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/TemplateEntityEffectSystem.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects; + +/// +/// A brief summary of the effect. +/// +/// +public sealed partial class TemplateEntityEffectSystem : EntityEffectSystem +{ + protected override void Effect(Entity entity, ref EntityEffectEvent