mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65e0c64ea | ||
|
|
4c388bc03d | ||
|
|
658dee1591 | ||
|
|
b6e5cca127 |
34
.github/workflows/build-all-configurations.yml
vendored
34
.github/workflows/build-all-configurations.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Build All Configurations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
targetOS: [Windows, Linux, MacOS]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build Debug
|
||||
run: dotnet build --no-restore --configuration Debug /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}
|
||||
- name: Build Tools
|
||||
run: dotnet build --no-restore --configuration Tools /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}
|
||||
- name: Build Release
|
||||
run: dotnet build --no-restore --configuration Release /p:WarningsAsErrors=nullable /p:TargetOS=${{ matrix.targetOS }}
|
||||
44
.github/workflows/build-docfx.yml
vendored
44
.github/workflows/build-docfx.yml
vendored
@@ -5,30 +5,30 @@ on:
|
||||
- cron: "0 0 * * 0"
|
||||
jobs:
|
||||
docfx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||
- name: Build Project
|
||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||
|
||||
- name: Build DocFX
|
||||
uses: nikeee/docfx-action@v1.0.0
|
||||
with:
|
||||
args: Robust.Docfx/docfx.json
|
||||
- name: Build DocFX
|
||||
uses: nikeee/docfx-action@v1.0.0
|
||||
with:
|
||||
args: Robust.Docfx/docfx.json
|
||||
|
||||
- name: Publish Docfx Documentation on GitHub Pages
|
||||
uses: maxheld83/ghpages@master
|
||||
env:
|
||||
BUILD_DIR: Robust.Docfx/_robust-site
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
- name: Publish Docfx Documentation on GitHub Pages
|
||||
uses: maxheld83/ghpages@master
|
||||
env:
|
||||
BUILD_DIR: Robust.Docfx/_robust-site
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
|
||||
37
.github/workflows/build-test.yml
vendored
37
.github/workflows/build-test.yml
vendored
@@ -2,32 +2,33 @@ name: Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
|
||||
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||
- name: Robust.UnitTesting
|
||||
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
|
||||
- name: Robust.Analyzers.Tests
|
||||
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
- 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
|
||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||
- name: Robust.UnitTesting
|
||||
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
|
||||
- name: Robust.Analyzers.Tests
|
||||
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
78
.github/workflows/codeql-analysis.yml
vendored
78
.github/workflows/codeql-analysis.yml
vendored
@@ -11,8 +11,14 @@
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
workflow_dispatch
|
||||
#on:
|
||||
# push:
|
||||
# branches: [ master ]
|
||||
# pull_request:
|
||||
# # The branches below must be a subset of the branches above
|
||||
# branches: [ master ]
|
||||
# schedule:
|
||||
# - cron: '30 18 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -22,50 +28,50 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["csharp"]
|
||||
language: [ 'csharp' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
73
.github/workflows/publish-client.yml
vendored
73
.github/workflows/publish-client.yml
vendored
@@ -3,50 +3,51 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Parse version
|
||||
id: parse_version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
|
||||
echo ("::set-output name=version::{0}" -f $ver)
|
||||
- name: Parse version
|
||||
id: parse_version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
|
||||
echo ("::set-output name=version::{0}" -f $ver)
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Package client
|
||||
run: Tools/package_client_build.py -p windows mac linux
|
||||
- name: Package client
|
||||
run: Tools/package_client_build.py -p windows mac linux
|
||||
|
||||
- name: Shuffle files around
|
||||
run: |
|
||||
mkdir "release/${{ steps.parse_version.outputs.version }}"
|
||||
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
|
||||
- name: Shuffle files around
|
||||
run: |
|
||||
mkdir "release/${{ steps.parse_version.outputs.version }}"
|
||||
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
|
||||
|
||||
- name: Upload files to Suns
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
source: "release/${{ steps.parse_version.outputs.version }}"
|
||||
target: "/var/lib/robust-builds/builds/"
|
||||
strip_components: 1
|
||||
- name: Upload files to Suns
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
source: "release/${{ steps.parse_version.outputs.version }}"
|
||||
target: "/var/lib/robust-builds/builds/"
|
||||
strip_components: 1
|
||||
|
||||
- name: Update manifest JSON
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
|
||||
|
||||
- name: Update manifest JSON
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: suns.spacestation14.com
|
||||
username: robust-build-push
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
|
||||
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
|
||||
|
||||
55
.github/workflows/test-content.yml
vendored
55
.github/workflows/test-content.yml
vendored
@@ -2,39 +2,40 @@ name: Test content master against engine
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out content
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
repository: space-wizards/space-station-14
|
||||
submodules: recursive
|
||||
- name: Check out content
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
repository: space-wizards/space-station-14
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Disable submodule autoupdate
|
||||
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Disable submodule autoupdate
|
||||
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
|
||||
|
||||
- name: Check out engine version
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch origin ${{ github.sha }}
|
||||
git checkout FETCH_HEAD
|
||||
git submodule update --init --recursive
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Tools --no-restore
|
||||
- name: Content.Tests
|
||||
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
|
||||
- name: Content.IntegrationTests
|
||||
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
|
||||
- name: Check out engine version
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch origin ${{ github.sha }}
|
||||
git checkout FETCH_HEAD
|
||||
git submodule update --init --recursive
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --configuration Tools --no-restore
|
||||
- name: Content.Tests
|
||||
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
|
||||
- name: Content.IntegrationTests
|
||||
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
|
||||
|
||||
@@ -10,69 +10,65 @@
|
||||
<ManagePackageVersionsCentrally />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.8" />
|
||||
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
|
||||
<PackageVersion Include="Linguini.Bundle" Version="0.8.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0" />
|
||||
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.12.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.24059.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.ILVerification" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.ILVerification" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="NUnit" Version="4.3.2" />
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="4.5.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.70" />
|
||||
<PackageVersion Include="NUnit" Version="4.0.1" />
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageVersion Include="Nett" Version="0.15.0" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
|
||||
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
|
||||
<PackageVersion Include="Pidgin" Version="3.3.0" />
|
||||
<PackageVersion Include="Pidgin" Version="3.2.2" />
|
||||
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
|
||||
<PackageVersion Include="Robust.Natives.Cef" Version="131.3.5" />
|
||||
<PackageVersion Include="Robust.Natives.Cef" Version="120.1.9" />
|
||||
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
|
||||
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.10" />
|
||||
<PackageVersion Include="Serilog" Version="4.2.0" />
|
||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
|
||||
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.7" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
|
||||
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
|
||||
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageVersion Include="PolySharp" Version="1.15.0" />
|
||||
|
||||
<!-- Transitive deps that we need to pin versions for to avoid NuGet warnings. -->
|
||||
<PackageVersion Include="System.Formats.Asn1" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.12.0" />
|
||||
<PackageVersion Include="PolySharp" Version="1.14.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
<!-- Engine-specific properties. Content should not use this file. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13</LangVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Import this at the end of any project files in Robust and Content. -->
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -71,6 +71,6 @@
|
||||
</PropertyGroup>
|
||||
<Exec
|
||||
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath="$(IntermediateOutputPath.TrimEnd('\'))/""/>
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
Submodule NetSerializer updated: ced82e76ad...7f51deaeca
1473
RELEASE-NOTES.md
1473
RELEASE-NOTES.md
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,6 @@
|
||||
kind: canvas
|
||||
light_mode: unshaded
|
||||
|
||||
# Simple mix blend
|
||||
- type: shader
|
||||
id: Mix
|
||||
kind: canvas
|
||||
blend_mode: Mix
|
||||
|
||||
- type: shader
|
||||
id: shaded
|
||||
kind: canvas
|
||||
|
||||
@@ -18,9 +18,3 @@
|
||||
description: entity-category-desc-hide
|
||||
hideSpawnMenu: true
|
||||
inheritable: false
|
||||
|
||||
# Entity prototypes added by the fork. With CVar you can hide all entities without this category
|
||||
- type: entityCategory
|
||||
id: ForkFiltered
|
||||
name: entity-category-name-fork
|
||||
description: entity-category-desc-fork
|
||||
@@ -20,26 +20,6 @@ zzzz-object-pronoun = { GENDER($ent) ->
|
||||
*[neuter] it
|
||||
}
|
||||
|
||||
# Used internally by the DAT-OBJ() function.
|
||||
# Not used in en-US. Created to support other languages.
|
||||
# (e.g., "to him," "for her")
|
||||
zzzz-dat-object = { GENDER($ent) ->
|
||||
[male] him
|
||||
[female] her
|
||||
[epicene] them
|
||||
*[neuter] it
|
||||
}
|
||||
|
||||
# Used internally by the GENITIVE() function.
|
||||
# Not used in en-US. Created to support other languages.
|
||||
# e.g., "у него" (Russian), "seines Vaters" (German).
|
||||
zzzz-genitive = { GENDER($ent) ->
|
||||
[male] his
|
||||
[female] her
|
||||
[epicene] their
|
||||
*[neuter] its
|
||||
}
|
||||
|
||||
# Used internally by the POSS-PRONOUN() function.
|
||||
zzzz-possessive-pronoun = { GENDER($ent) ->
|
||||
[male] his
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
color-hue-chroma-lightness = {$lightness} {$chroma} {$hue}
|
||||
color-hue-chroma = {$chroma} {$hue}
|
||||
color-hue-lightness = {$lightness} {$hue}
|
||||
color-very-dark = very dark
|
||||
color-dark = dark
|
||||
color-light = light
|
||||
color-very-light = very light
|
||||
color-mixed-hue = {$a} {$b}
|
||||
color-pale = pale
|
||||
color-gray-adjective = gray
|
||||
color-strong = strong
|
||||
color-pink = pink
|
||||
color-red = red
|
||||
color-orange = orange
|
||||
color-yellow = yellow
|
||||
color-green = green
|
||||
color-cyan = cyan
|
||||
color-blue = blue
|
||||
color-purple = purple
|
||||
color-brown = brown
|
||||
color-white = white
|
||||
color-gray = gray
|
||||
color-black = black
|
||||
|
||||
color-pink-color-red = pinkish red
|
||||
color-red-color-orange = reddish orange
|
||||
color-orange-color-yellow = orangeish yellow
|
||||
color-yellow-color-green = yellowish green
|
||||
color-green-color-cyan = greenish cyan
|
||||
color-cyan-color-blue = cyanish blue
|
||||
color-blue-color-purple = blueish purple
|
||||
color-purple-color-pink = purpleish pink
|
||||
@@ -1,7 +1,5 @@
|
||||
### Localization for engine console commands
|
||||
|
||||
cmd-hint-float = [float]
|
||||
|
||||
## generic command errors
|
||||
|
||||
cmd-invalid-arg-number-error = Invalid number of arguments.
|
||||
@@ -13,7 +11,6 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
|
||||
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
|
||||
cmd-parse-failure-grid = {$arg} is not a valid grid.
|
||||
cmd-parse-failure-cultureinfo = "{$arg}" is not valid CultureInfo.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
cmd-parse-failure-session = There is no session with username: {$username}
|
||||
|
||||
@@ -159,7 +156,6 @@ cmd-savemap-not-exist = Target map does not exist.
|
||||
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
|
||||
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
|
||||
cmd-savemap-success = Map successfully saved.
|
||||
cmd-savemap-error = Could not save map! See server log for details.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
@@ -297,7 +293,7 @@ cmd-lsgrid-desc = Lists grids.
|
||||
cmd-lsgrid-help = lsgrid
|
||||
|
||||
cmd-addmap-desc = Adds a new empty map to the round. If the mapID already exists, this command does nothing.
|
||||
cmd-addmap-help = addmap <mapID> [pre-init]
|
||||
cmd-addmap-help = addmap <mapID> [initialize]
|
||||
|
||||
cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace.
|
||||
cmd-rmmap-help = rmmap <mapId>
|
||||
@@ -431,20 +427,11 @@ cmd-entfo-help = Usage: entfo <entityuid>
|
||||
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
|
||||
|
||||
cmd-fuck-desc = Throws an exception
|
||||
cmd-fuck-help = Usage: fuck
|
||||
cmd-fuck-help = Throws an exception
|
||||
|
||||
cmd-showpos-desc = Show the position of all entities on the screen.
|
||||
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
|
||||
cmd-showpos-help = Usage: showpos
|
||||
|
||||
cmd-showrot-desc = Show the rotation of all entities on the screen.
|
||||
cmd-showrot-help = Usage: showrot
|
||||
|
||||
cmd-showvel-desc = Show the local velocity of all entites on the screen.
|
||||
cmd-showvel-help = Usage: showvel
|
||||
|
||||
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
|
||||
cmd-showangvel-help = Usage: showangvel
|
||||
|
||||
cmd-sggcell-desc = Lists entities on a snap grid cell.
|
||||
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
|
||||
|
||||
@@ -575,8 +562,3 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
|
||||
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
|
||||
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
|
||||
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
|
||||
|
||||
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager
|
||||
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
|
||||
cmd-localization_set_culture-culture-name = <cultureName>
|
||||
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })
|
||||
|
||||
@@ -4,21 +4,9 @@ entity-spawn-window-title = Entity Spawn Panel
|
||||
entity-spawn-window-search-bar-placeholder = search
|
||||
entity-spawn-window-clear-button = Clear
|
||||
entity-spawn-window-replace-button-text = Replace
|
||||
entity-spawn-window-erase-button-text = Erase Mode
|
||||
entity-spawn-window-override-menu-tooltip = Override placement
|
||||
|
||||
## TileSpawnWindow
|
||||
|
||||
tile-spawn-window-title = Place Tiles
|
||||
tile-spawn-window-mirror-button-text = Mirror Tiles
|
||||
|
||||
## Console
|
||||
|
||||
console-line-edit-placeholder = Command Here
|
||||
|
||||
## OutputPanel
|
||||
|
||||
output-panel-scroll-down-button-text = Scroll Down
|
||||
|
||||
## Common Used
|
||||
|
||||
window-erase-button-text = Erase Mode
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## "Textures" dev window tab
|
||||
|
||||
dev-window-tab-textures-title = Textures
|
||||
dev-window-tab-textures-reload = Reload
|
||||
dev-window-tab-textures-filter = Filter
|
||||
dev-window-tab-textures-summary = Total (est): { $bytes }
|
||||
dev-window-tab-textures-info = Width: { $width } Height: { $height }
|
||||
PixelType: { $pixelType } sRGB: { $srgb }
|
||||
Name: { $name }
|
||||
Est. memory usage: { $bytes }
|
||||
@@ -7,6 +7,3 @@ entity-category-desc-spawner = Entity prototypes that spawn other entities.
|
||||
|
||||
entity-category-name-hide = Hidden
|
||||
entity-category-desc-hide = Entity prototypes that should be hidden from entity spawn menus
|
||||
|
||||
entity-category-name-fork = Fork Filtered
|
||||
entity-category-desc-fork = Entity prototypes added by the fork. With CVar you can hide all entities without this category
|
||||
@@ -1,8 +1,4 @@
|
||||
command-help-usage =
|
||||
Usage:
|
||||
command-help-invertible =
|
||||
The behaviour of this command can be inverted using the "not" prefix.
|
||||
command-description-tpto =
|
||||
command-description-tpto =
|
||||
Teleport the given entities to some target entity.
|
||||
command-description-player-list =
|
||||
Returns a list of all player sessions.
|
||||
@@ -23,7 +19,7 @@ command-description-buildinfo =
|
||||
command-description-cmd-list =
|
||||
Returns a list of all commands, for this side.
|
||||
command-description-explain =
|
||||
Explains the given expression, providing command descriptions and signatures. This only works for valid expressions, it can't explain commands that it fails to parse.
|
||||
Explains the given expression, providing command descriptions and signatures.
|
||||
command-description-search =
|
||||
Searches through the input for the provided value.
|
||||
command-description-stopwatch =
|
||||
@@ -42,7 +38,8 @@ command-description-as =
|
||||
command-description-count =
|
||||
Counts the amount of entries in it's input, returning an integer.
|
||||
command-description-map =
|
||||
Maps the input over the given block.
|
||||
Maps the input over the given block, with the provided expected return type.
|
||||
This command may be modified to not need an explicit return type in the future.
|
||||
command-description-select =
|
||||
Selects N objects or N% of objects from the input.
|
||||
One can additionally invert this command with not to make it select everything except N objects instead.
|
||||
@@ -56,8 +53,10 @@ command-description-entities =
|
||||
Returns all entities on the server.
|
||||
command-description-paused =
|
||||
Filters the input entities by whether or not they are paused.
|
||||
This command can be inverted with not.
|
||||
command-description-with =
|
||||
Filters the input entities by whether or not they have the given component.
|
||||
This command can be inverted with not.
|
||||
command-description-fuck =
|
||||
Throws an exception.
|
||||
command-description-ecscomp-listty =
|
||||
@@ -96,8 +95,6 @@ command-description-vars =
|
||||
Provides a list of all variables set in this session.
|
||||
command-description-any =
|
||||
Returns true if there's any values in the input, otherwise false.
|
||||
command-description-contains =
|
||||
Returns whether the input enumerable contains the specified value.
|
||||
command-description-ArrowCommand =
|
||||
Assigns the input to a variable.
|
||||
command-description-isempty =
|
||||
@@ -122,8 +119,6 @@ command-description-splat =
|
||||
"Splats" a block, value, or variable, creating N copies of it in a list.
|
||||
command-description-val =
|
||||
Casts the given value, block, or variable to the given type. This is mostly a workaround for current limitations of variables.
|
||||
command-description-var =
|
||||
Returns the contents of the given variable. This will attempt to automatically infer a variables type. Compound commands that modify a variable may need to use the 'val' command instead.
|
||||
command-description-actor-controlled =
|
||||
Filters entities by whether or not they're actively controlled.
|
||||
command-description-actor-session =
|
||||
@@ -148,7 +143,7 @@ command-description-max =
|
||||
Returns the maximum of two values.
|
||||
command-description-BitAndCommand =
|
||||
Performs bitwise AND.
|
||||
command-description-bitor =
|
||||
command-description-BitOrCommand =
|
||||
Performs bitwise OR.
|
||||
command-description-BitXorCommand =
|
||||
Performs bitwise XOR.
|
||||
@@ -195,8 +190,6 @@ command-description-spawn-at =
|
||||
Spawns an entity at the given coordinates.
|
||||
command-description-spawn-on =
|
||||
Spawns an entity on the given entity, at it's coordinates.
|
||||
command-description-spawn-in =
|
||||
Spawns an entity in the given container on the given entity, dropping it at its coordinates if it doesn't fit
|
||||
command-description-spawn-attached =
|
||||
Spawns an entity attached to the given entity, at (0 0) relative to it.
|
||||
command-description-mappos =
|
||||
@@ -204,11 +197,11 @@ command-description-mappos =
|
||||
command-description-pos =
|
||||
Returns an entity's coordinates.
|
||||
command-description-tp-coords =
|
||||
Teleports the given entities to the target coordinates.
|
||||
Teleports the target to the given coordinates.
|
||||
command-description-tp-to =
|
||||
Teleports the given entities to the target entity.
|
||||
Teleports the target to the given other entity.
|
||||
command-description-tp-into =
|
||||
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
|
||||
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
|
||||
command-description-comp-get =
|
||||
Gets the given component from the given entity.
|
||||
command-description-comp-add =
|
||||
@@ -226,9 +219,9 @@ command-description-MulVecCommand =
|
||||
command-description-DivVecCommand =
|
||||
Divides every element in the input by a scalar (single value).
|
||||
command-description-rng-to =
|
||||
Returns a number between the input (inclusive) and the argument (exclusive).
|
||||
Returns a number from its input to its argument (i.e. n..m inclusive)
|
||||
command-description-rng-from =
|
||||
Returns a number between the argument (inclusive) and the input (exclusive))
|
||||
Returns a number to its input from its argument (i.e. m..n inclusive)
|
||||
command-description-rng-prob =
|
||||
Returns a boolean based on the input probability/chance (from 0 to 1)
|
||||
command-description-sum =
|
||||
@@ -278,7 +271,7 @@ command-description-ModVecCommand =
|
||||
Performs the modulus operation over the input with the given constant right-hand value.
|
||||
command-description-BitAndNotCommand =
|
||||
Performs bitwise AND-NOT over the input.
|
||||
command-description-bitornot =
|
||||
command-description-BitOrNotCommand =
|
||||
Performs bitwise OR-NOT over the input.
|
||||
command-description-BitXnorCommand =
|
||||
Performs bitwise XNOR over the input.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
popup-copy-button = Copy
|
||||
popup-title = Alert!
|
||||
@@ -136,7 +136,6 @@ cmd-savemap-not-exist = O mapa de destino não existe.
|
||||
cmd-savemap-init-warning = Tentativa de salvar um mapa pós-inicialização sem forçar o salvamento.
|
||||
cmd-savemap-attempt = Tentando salvar o mapa {$mapId} em {$path}.
|
||||
cmd-savemap-success = Mapa salvo com sucesso.
|
||||
cmd-savemap-error = Não foi possível salvar o mapa! Consulte o log do servidor para obter detalhes.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing.Verifiers;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.AccessAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
using Robust.Analyzers;
|
||||
|
||||
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.AccessAnalyzer>;
|
||||
using static Microsoft.CodeAnalysis.Testing.DiagnosticResult;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -12,7 +16,7 @@ public sealed class AccessAnalyzer_Test
|
||||
{
|
||||
public Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<AccessAnalyzer, DefaultVerifier>()
|
||||
var test = new CSharpAnalyzerTest<AccessAnalyzer, NUnitVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ByRefEventAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture, TestOf(typeof(ByRefEventAnalyzer))]
|
||||
public sealed class ByRefEventAnalyzerTest
|
||||
{
|
||||
private const string EventBusDef = """
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public readonly struct EntityUid;
|
||||
|
||||
public sealed class EntitySystem
|
||||
{
|
||||
public void RaiseLocalEvent<TEvent>(EntityUid uid, ref TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull { }
|
||||
|
||||
public void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull { }
|
||||
}
|
||||
|
||||
public sealed class EntityEventBus
|
||||
{
|
||||
public void RaiseLocalEvent<TEvent>(EntityUid uid, ref TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull { }
|
||||
|
||||
public void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull { }
|
||||
}
|
||||
""";
|
||||
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<ByRefEventAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.GameObjects.EventBusAttributes.cs"
|
||||
);
|
||||
|
||||
test.TestState.Sources.Add(("EntityEventBus.cs", EventBusDef));
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSuccess()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly struct RefEvent;
|
||||
public readonly struct ValueEvent;
|
||||
|
||||
public static class Foo
|
||||
{
|
||||
public static void Bar(EntityEventBus bus)
|
||||
{
|
||||
bus.RaiseLocalEvent(default(EntityUid), new ValueEvent());
|
||||
var refEv = new RefEvent();
|
||||
bus.RaiseLocalEvent(default(EntityUid), ref refEv);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestWrong()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly struct RefEvent;
|
||||
public readonly struct ValueEvent;
|
||||
|
||||
public static class Foo
|
||||
{
|
||||
public static void Bar(EntityEventBus bus)
|
||||
{
|
||||
bus.RaiseLocalEvent(default(EntityUid), new RefEvent());
|
||||
var valueEv = new ValueEvent();
|
||||
bus.RaiseLocalEvent(default(EntityUid), ref valueEv);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(
|
||||
code,
|
||||
// /0/Test0.cs(11,49): error RA0015: Tried to raise a by-ref event 'RefEvent' by value
|
||||
VerifyCS.Diagnostic(ByRefEventAnalyzer.ByRefEventRaisedByValueRule).WithSpan(11, 49, 11, 63).WithArguments("RefEvent"),
|
||||
// /0/Test0.cs(13,49): error RA0016: Tried to raise a value event 'ValueEvent' by-ref
|
||||
VerifyCS.Diagnostic(ByRefEventAnalyzer.ByValueEventRaisedByRefRule).WithSpan(13, 49, 13, 60).WithArguments("ValueEvent")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -107,7 +106,6 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -149,7 +147,6 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -191,7 +188,6 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DataDefinitionAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class DataDefinitionAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DataDefinitionAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
private const string TestTypeDefs = """
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.ViewVariables
|
||||
{
|
||||
public sealed class ViewVariablesAttribute : Attribute
|
||||
{
|
||||
public readonly VVAccess Access = VVAccess.ReadOnly;
|
||||
|
||||
public ViewVariablesAttribute() { }
|
||||
|
||||
public ViewVariablesAttribute(VVAccess access)
|
||||
{
|
||||
Access = access;
|
||||
}
|
||||
}
|
||||
public enum VVAccess : byte
|
||||
{
|
||||
ReadOnly = 0,
|
||||
ReadWrite = 1,
|
||||
}
|
||||
}
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute(string? tag = null) : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
public sealed class NotYamlSerializableAttribute : Attribute;
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task NoVVReadOnlyTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Bad;
|
||||
|
||||
[DataField]
|
||||
public int Good;
|
||||
|
||||
[DataField, ViewVariables]
|
||||
public int Good2;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public int Good3;
|
||||
|
||||
[ViewVariables]
|
||||
public int Good4;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(7, 17, 7, 50).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyFieldTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public readonly int Bad;
|
||||
|
||||
[DataField]
|
||||
public int Good;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PartialDataDefinitionTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed class Foo { }
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(4,15): error RA0017: Type Foo is a DataDefinition but is not partial
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataDefinitionPartialRule).WithSpan(4, 15, 4, 20).WithArguments("Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NestedPartialDataDefinitionTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
public sealed class Foo
|
||||
{
|
||||
[DataDefinition]
|
||||
public sealed partial class Nested { }
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(3,15): error RA0018: Type Foo contains nested data definition Nested but is not partial
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.NestedDataDefinitionPartialRule).WithSpan(3, 15, 3, 20).WithArguments("Foo", "Nested")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RedundantDataFieldTagTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField("someValue")]
|
||||
public int SomeValue;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,6): info RA0027: Data field SomeValue in data definition Foo has an explicitly set tag that matches autogenerated tag
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldRedundantTagRule).WithSpan(6, 6, 6, 28).WithArguments("SomeValue", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyPropertyTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public int Bad { get; }
|
||||
|
||||
[DataField]
|
||||
public int Good { get; private set; }
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(7, 20, 7, 28).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotYamlSerializableTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[NotYamlSerializable]
|
||||
public sealed class NotSerializableClass { }
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public NotSerializableClass BadField;
|
||||
|
||||
[DataField]
|
||||
public NotSerializableClass BadProperty { get; set; }
|
||||
|
||||
public NotSerializableClass GoodField; // Not a DataField, not a problem
|
||||
|
||||
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
|
||||
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing.Verifiers;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -13,7 +14,7 @@ public sealed class DependencyAssignAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, DefaultVerifier>()
|
||||
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, NUnitVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DuplicateDependencyAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
[TestOf(typeof(DuplicateDependencyAnalyzer))]
|
||||
public sealed class DuplicateDependencyAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.IoC.DependencyAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed class Foo
|
||||
{
|
||||
[Dependency]
|
||||
private object? Field;
|
||||
|
||||
[Dependency]
|
||||
private object? Field2;
|
||||
|
||||
[Dependency]
|
||||
private string? DifferentField;
|
||||
|
||||
private string? NonDependency1;
|
||||
private string? NonDependency2;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(9,21): warning RA0032: Another [Dependency] field of type 'object?' already exists in this type as field 'Field'
|
||||
VerifyCS.Diagnostic().WithSpan(9, 21, 9, 27).WithArguments("object?", "Field"));
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class ForbidLiteralAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
|
||||
);
|
||||
|
||||
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
private const string TestTypeDefs = """
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public sealed class TestClass
|
||||
{
|
||||
public static void OneParameterForbidden([ForbidLiteral] string value) { }
|
||||
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
|
||||
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
|
||||
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
|
||||
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
|
||||
}
|
||||
|
||||
public record struct StringWrapper(string value)
|
||||
{
|
||||
private readonly string _value = value;
|
||||
|
||||
public static implicit operator string(StringWrapper wrapper)
|
||||
{
|
||||
return wrapper._value;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task TestOneParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.OneParameterForbidden(_constValue);
|
||||
TestClass.OneParameterForbidden(StaticValue);
|
||||
TestClass.OneParameterForbidden(WrappedValue);
|
||||
TestClass.OneParameterForbidden("baz");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTwoParametersFirstForbidden()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
|
||||
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
|
||||
TestClass.TwoParametersFirstForbidden("foo", "whatever");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTwoParametersBothForbidden()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
|
||||
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
|
||||
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
|
||||
TestClass.TwoParametersBothForbidden("whatever", _constValue);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
|
||||
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestListParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
|
||||
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
|
||||
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestParamsListParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
|
||||
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
|
||||
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class MustCallBaseAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.IoC.MustCallBaseAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public class Foo
|
||||
{
|
||||
[MustCallBase]
|
||||
public virtual void Function()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[MustCallBase(true)]
|
||||
public virtual void Function2()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Bar : Foo
|
||||
{
|
||||
public override void Function()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void Function2()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Baz : Foo
|
||||
{
|
||||
public override void Function()
|
||||
{
|
||||
base.Function();
|
||||
}
|
||||
}
|
||||
|
||||
public class Bal : Bar
|
||||
{
|
||||
public override void Function2()
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(20,26): warning RA0028: Overriders of this function must always call the base function
|
||||
VerifyCS.Diagnostic().WithSpan(20, 26, 20, 34),
|
||||
// /0/Test0.cs(41,26): warning RA0028: Overriders of this function must always call the base function
|
||||
VerifyCS.Diagnostic().WithSpan(41, 26, 41, 35));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing.Verifiers;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -13,7 +14,7 @@ public sealed class NoUncachedRegexAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, DefaultVerifier>()
|
||||
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, NUnitVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ObsoleteInheritanceAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer that implements <c>[ObsoleteInheritance]</c> checking, to give obsoletion warnings for inheriting types
|
||||
/// that should never have been virtual.
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class ObsoleteInheritanceAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<ObsoleteInheritanceAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestBasic()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
[ObsoleteInheritance]
|
||||
public class Base;
|
||||
|
||||
public class NotAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself
|
||||
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.Rule).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestMessage()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
[ObsoleteInheritance("Sus")]
|
||||
public class Base;
|
||||
|
||||
public class NotAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself: "Sus"
|
||||
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.RuleWithMessage).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base", "Sus")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestNormal()
|
||||
{
|
||||
const string code = """
|
||||
public class Base;
|
||||
|
||||
public class AllowedAllowed : Base;
|
||||
""";
|
||||
|
||||
await Verifier(code);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferNonGenericVariantForAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class PreferNonGenericVariantForTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<PreferNonGenericVariantForAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public class Bar { };
|
||||
public class Baz { };
|
||||
public class Okay { };
|
||||
|
||||
public static class Foo
|
||||
{
|
||||
[PreferNonGenericVariantFor(typeof(Bar), typeof(Baz))]
|
||||
public static void DoFoo<T>() { }
|
||||
}
|
||||
|
||||
public class Test
|
||||
{
|
||||
public void DoBad()
|
||||
{
|
||||
Foo.DoFoo<Bar>();
|
||||
}
|
||||
|
||||
public void DoGood()
|
||||
{
|
||||
Foo.DoFoo<Okay>();
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(17,9): warning RA0029: Use the non-generic variant of this method for type Bar
|
||||
VerifyCS.Diagnostic().WithSpan(17, 9, 17, 25).WithArguments("Bar")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class PreferOtherTypeAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<PreferOtherTypeAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public class EntityPrototype { };
|
||||
public class EntProtoId { };
|
||||
public class ReagentPrototype { };
|
||||
|
||||
[PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))]
|
||||
public class ProtoId<T> { };
|
||||
|
||||
public class Test
|
||||
{
|
||||
public ProtoId<EntityPrototype> Bad = new();
|
||||
|
||||
public ProtoId<ReagentPrototype> Good = new();
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(12,12): warning RA0031: Use the specific type EntProtoId instead of ProtoId when the type argument is EntityPrototype
|
||||
VerifyCS.Diagnostic().WithSpan(12, 12, 12, 48).WithArguments("EntProtoId", "ProtoId", "EntityPrototype")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
public sealed class PreferOtherTypeFixerTest
|
||||
{
|
||||
private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpCodeFixTest<PreferOtherTypeAnalyzer, PreferOtherTypeFixer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
FixedState =
|
||||
{
|
||||
Sources = { fixedCode },
|
||||
}
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs"
|
||||
);
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.FixedState,
|
||||
"Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs"
|
||||
);
|
||||
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public class EntityPrototype { };
|
||||
public class EntProtoId { };
|
||||
public class ReagentPrototype { };
|
||||
|
||||
[PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))]
|
||||
public class ProtoId<T> { };
|
||||
|
||||
public class Test
|
||||
{
|
||||
public ProtoId<EntityPrototype> Foo = new();
|
||||
}
|
||||
""";
|
||||
|
||||
const string fixedCode = """
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public class EntityPrototype { };
|
||||
public class EntProtoId { };
|
||||
public class ReagentPrototype { };
|
||||
|
||||
[PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))]
|
||||
public class ProtoId<T> { };
|
||||
|
||||
public class Test
|
||||
{
|
||||
public EntProtoId Foo = new();
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code, fixedCode,
|
||||
// /0/Test0.cs(12,12): error RA0031: Use the specific type EntProtoId instead of ProtoId when the type argument is EntityPrototype
|
||||
VerifyCS.Diagnostic().WithSpan(12, 12, 12, 48).WithArguments("EntProtoId", "ProtoId", "EntityPrototype"));
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeInstantiationAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
[TestOf(typeof(PrototypeInstantiationAnalyzer))]
|
||||
public sealed class PrototypeInstantiationAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new RTAnalyzerTest<PrototypeInstantiationAnalyzer>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Prototypes.Attributes.cs",
|
||||
"Robust.Shared.Prototypes.IPrototype.cs",
|
||||
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
[Prototype]
|
||||
public sealed class FooPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
}
|
||||
|
||||
public static class Bad
|
||||
{
|
||||
public static FooPrototype Real()
|
||||
{
|
||||
return new FooPrototype();
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(15,16): warning RA0039: Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.
|
||||
VerifyCS.Diagnostic().WithSpan(15, 16, 15, 34));
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeNetSerializableAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
[TestOf(typeof(PrototypeNetSerializableAnalyzer))]
|
||||
public sealed class PrototypeNetSerializableAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new RTAnalyzerTest<PrototypeNetSerializableAnalyzer>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Serialization.NetSerializableAttribute.cs",
|
||||
"Robust.Shared.Prototypes.Attributes.cs",
|
||||
"Robust.Shared.Prototypes.IPrototype.cs",
|
||||
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
|
||||
);
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
[Prototype]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class FooPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,21): warning RA0037: Type FooPrototype is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.
|
||||
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleNetSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"),
|
||||
// /0/Test0.cs(7,21): warning RA0038: Type FooPrototype is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.
|
||||
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
public sealed class RTAnalyzerTest<TAnalyzer> : CSharpAnalyzerTest<TAnalyzer, DefaultVerifier>
|
||||
where TAnalyzer : DiagnosticAnalyzer, new()
|
||||
{
|
||||
protected override ParseOptions CreateParseOptions()
|
||||
{
|
||||
var baseOptions = (CSharpParseOptions) base.CreateParseOptions();
|
||||
return baseOptions.WithPreprocessorSymbols("ROBUST_ANALYZERS_TEST");
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,7 @@
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Serialization\NetSerializableAttribute.cs" LogicalName="Robust.Shared.Serialization.NetSerializableAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Prototypes\Attributes.cs" LogicalName="Robust.Shared.Prototypes.Attributes.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Prototypes\IPrototype.cs" LogicalName="Robust.Shared.Prototypes.IPrototype.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Serialization\Manager\Attributes\DataFieldAttribute.cs" LogicalName="Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -34,17 +24,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
|
||||
<PackageReference Include="NUnit"/>
|
||||
<PackageReference Include="NUnit3TestAdapter"/>
|
||||
<PackageReference Include="NUnit.Analyzers"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
|
||||
<!-- Needed to fix transitive dependency versions -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
|
||||
<PackageReference Include="System.Formats.Asn1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -24,7 +24,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure that methods subscribing to a ref event have the ref keyword for the event argument."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
|
||||
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
|
||||
Diagnostics.IdByRefEventRaisedByValue,
|
||||
"By-ref event raised by value",
|
||||
"Tried to raise a by-ref event '{0}' by value",
|
||||
@@ -34,7 +34,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to use the ref keyword when raising ref events."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
|
||||
private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
|
||||
Diagnostics.IdValueEventRaisedByRef,
|
||||
"Value event raised by-ref",
|
||||
"Tried to raise a value event '{0}' by-ref",
|
||||
@@ -54,44 +54,32 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterCompilationStartAction(compilationContext =>
|
||||
{
|
||||
var raiseMethods = compilationContext.Compilation
|
||||
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
|
||||
.GetMembers()
|
||||
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
|
||||
.Cast<IMethodSymbol>();
|
||||
|
||||
var busRaiseMethods = compilationContext.Compilation
|
||||
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntityEventBus")?
|
||||
.GetMembers()
|
||||
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
|
||||
.Cast<IMethodSymbol>();
|
||||
|
||||
if (raiseMethods == null)
|
||||
return;
|
||||
|
||||
if (busRaiseMethods != null)
|
||||
raiseMethods = raiseMethods.Concat(busRaiseMethods);
|
||||
|
||||
var raiseMethodsArray = raiseMethods.ToArray();
|
||||
|
||||
compilationContext.RegisterOperationAction(
|
||||
ctx => CheckEventRaise(ctx, raiseMethodsArray),
|
||||
OperationKind.Invocation);
|
||||
});
|
||||
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
|
||||
}
|
||||
|
||||
private static void CheckEventRaise(
|
||||
OperationAnalysisContext context,
|
||||
IReadOnlyCollection<IMethodSymbol> raiseMethods)
|
||||
private void CheckEventRaise(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation operation)
|
||||
return;
|
||||
|
||||
if (!operation.TargetMethod.Name.Contains("RaiseLocalEvent"))
|
||||
var raiseMethods = context.Compilation
|
||||
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
|
||||
.GetMembers()
|
||||
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
|
||||
.Cast<IMethodSymbol>();
|
||||
|
||||
var busRaiseMethods = context.Compilation
|
||||
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntityEventBus")?
|
||||
.GetMembers()
|
||||
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
|
||||
.Cast<IMethodSymbol>();
|
||||
|
||||
if (raiseMethods == null)
|
||||
return;
|
||||
|
||||
if (busRaiseMethods != null)
|
||||
raiseMethods = raiseMethods.Concat(busRaiseMethods);
|
||||
|
||||
if (!raiseMethods.Any(m => m.Equals(operation.TargetMethod.OriginalDefinition, Default)))
|
||||
{
|
||||
// If you try to do this normally by concatenating like busRaiseMethods above
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
using Robust.Shared.Serialization.Manager.Definition;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
@@ -17,12 +16,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute";
|
||||
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
|
||||
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
|
||||
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
|
||||
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
private const string ViewVariablesAttributeName = "ViewVariables";
|
||||
|
||||
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
|
||||
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
|
||||
Diagnostics.IdDataDefinitionPartial,
|
||||
"Type must be partial",
|
||||
"Type {0} is a DataDefinition but is not partial",
|
||||
@@ -32,7 +27,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to mark any type that is a data definition as partial."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
|
||||
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
|
||||
Diagnostics.IdNestedDataDefinitionPartial,
|
||||
"Type must be partial",
|
||||
"Type {0} contains nested data definition {1} but is not partial",
|
||||
@@ -42,7 +37,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to mark any type containing a nested data definition as partial."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldWritableRule = new(
|
||||
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
|
||||
Diagnostics.IdDataFieldWritable,
|
||||
"Data field must not be readonly",
|
||||
"Data field {0} in data definition {1} is readonly",
|
||||
@@ -52,7 +47,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to remove the readonly modifier."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
|
||||
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
|
||||
Diagnostics.IdDataFieldPropertyWritable,
|
||||
"Data field property must have a setter",
|
||||
"Data field property {0} in data definition {1} does not have a setter",
|
||||
@@ -62,7 +57,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to add a setter."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
||||
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
|
||||
Diagnostics.IdDataFieldRedundantTag,
|
||||
"Data field has redundant tag specified",
|
||||
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
|
||||
@@ -71,30 +66,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
true,
|
||||
"Make sure to remove the tag string from the data field attribute."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldNoVVReadWriteRule = new(
|
||||
Diagnostics.IdDataFieldNoVVReadWrite,
|
||||
"Data field has VV ReadWrite",
|
||||
"Data field {0} in data definition {1} has ViewVariables attribute with ReadWrite access, which is redundant",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Info,
|
||||
true,
|
||||
"Make sure to remove the ViewVariables attribute."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
|
||||
Diagnostics.IdDataFieldYamlSerializable,
|
||||
"Data field type is not YAML serializable",
|
||||
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
true,
|
||||
"Make sure to use a type that is YAML serializable."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
|
||||
DataFieldRedundantTagRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -102,31 +76,23 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterSymbolStartAction(symbolContext =>
|
||||
{
|
||||
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return;
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
|
||||
|
||||
if (!IsDataDefinition(typeSymbol))
|
||||
return;
|
||||
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
|
||||
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
|
||||
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
|
||||
}, SymbolKind.NamedType);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
|
||||
}
|
||||
|
||||
private static void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
|
||||
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (context.Node is not TypeDeclarationSyntax declaration)
|
||||
return;
|
||||
|
||||
if (context.ContainingSymbol is not INamedTypeSymbol type)
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
|
||||
if (!IsDataDefinition(type))
|
||||
return;
|
||||
|
||||
if (!IsPartial(declaration))
|
||||
@@ -137,7 +103,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
var containingType = type.ContainingType;
|
||||
while (containingType != null)
|
||||
{
|
||||
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
var containingTypeDeclaration = (TypeDeclarationSyntax) containingType.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
if (!IsPartial(containingTypeDeclaration))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
|
||||
@@ -147,111 +113,70 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeDataField(SyntaxNodeAnalysisContext context)
|
||||
private void AnalyzeDataField(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (context.Node is not FieldDeclarationSyntax field)
|
||||
return;
|
||||
|
||||
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
|
||||
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
|
||||
if (typeDeclaration == null)
|
||||
return;
|
||||
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
|
||||
if (!IsDataDefinition(type))
|
||||
return;
|
||||
|
||||
foreach (var variable in field.Declaration.Variables)
|
||||
{
|
||||
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
|
||||
|
||||
if (fieldSymbol == null)
|
||||
continue;
|
||||
|
||||
if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute))
|
||||
continue;
|
||||
|
||||
if (IsReadOnlyDataField(type, fieldSymbol))
|
||||
{
|
||||
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
|
||||
if (HasRedundantTag(fieldSymbol))
|
||||
{
|
||||
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasVVReadWrite(fieldSymbol))
|
||||
{
|
||||
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
|
||||
continue;
|
||||
|
||||
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
|
||||
fieldSymbol.Name,
|
||||
type.Name,
|
||||
fieldTypeSymbol.MetadataName
|
||||
));
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
|
||||
private void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (context.Node is not PropertyDeclarationSyntax property)
|
||||
return;
|
||||
|
||||
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
|
||||
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
|
||||
if (typeDeclaration == null)
|
||||
return;
|
||||
|
||||
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
|
||||
return;
|
||||
|
||||
if (type.IsRecord || type.IsValueType)
|
||||
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
|
||||
if (!IsDataDefinition(type) || type.IsRecord || type.IsValueType)
|
||||
return;
|
||||
|
||||
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(property);
|
||||
if (propertySymbol == null)
|
||||
return;
|
||||
|
||||
if (!IsDataField(propertySymbol, out _, out var datafieldAttribute))
|
||||
return;
|
||||
|
||||
if (IsReadOnlyDataField(type, propertySymbol))
|
||||
{
|
||||
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasRedundantTag(propertySymbol, datafieldAttribute))
|
||||
if (HasRedundantTag(propertySymbol))
|
||||
{
|
||||
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (HasVVReadWrite(propertySymbol))
|
||||
{
|
||||
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
|
||||
return;
|
||||
|
||||
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
|
||||
propertySymbol.Name,
|
||||
type.Name,
|
||||
propertyTypeSymbol.Name
|
||||
));
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
|
||||
{
|
||||
if (!IsDataField(field, out _, out _))
|
||||
return false;
|
||||
|
||||
return IsReadOnlyMember(type, field);
|
||||
}
|
||||
|
||||
@@ -313,38 +238,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetAttributeLocation(MemberDeclarationSyntax syntax, string attributeName, out Location location)
|
||||
{
|
||||
foreach (var attributeList in syntax.AttributeLists)
|
||||
{
|
||||
foreach (var attribute in attributeList.Attributes)
|
||||
{
|
||||
if (attribute.Name.ToString() != attributeName)
|
||||
continue;
|
||||
|
||||
location = attribute.GetLocation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Default to the declaration syntax's location
|
||||
location = syntax.GetLocation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
|
||||
{
|
||||
foreach (var modifier in syntax.Modifiers)
|
||||
{
|
||||
if (modifier.IsKind(modifierKind))
|
||||
{
|
||||
location = modifier.GetLocation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
location = syntax.GetLocation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
|
||||
{
|
||||
if (member is IFieldSymbol field)
|
||||
@@ -376,14 +269,17 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
|
||||
private static bool HasRedundantTag(ISymbol symbol)
|
||||
{
|
||||
if (!IsDataField(symbol, out var _, out var attribute))
|
||||
return false;
|
||||
|
||||
// No args, no problem
|
||||
if (datafieldAttribute.ConstructorArguments.Length == 0)
|
||||
if (attribute.ConstructorArguments.Length == 0)
|
||||
return false;
|
||||
|
||||
// If a tag is explicitly specified, it will be the first argument...
|
||||
var tagArgument = datafieldAttribute.ConstructorArguments[0];
|
||||
var tagArgument = attribute.ConstructorArguments[0];
|
||||
// ...but the first arg could also something else, since tag is optional
|
||||
// so we make sure that it's a string
|
||||
if (tagArgument.Value is not string explicitName)
|
||||
@@ -396,36 +292,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return explicitName == automaticName;
|
||||
}
|
||||
|
||||
private static bool HasVVReadWrite(ISymbol symbol)
|
||||
{
|
||||
// Make sure it has ViewVariablesAttribute
|
||||
AttributeData? viewVariablesAttribute = null;
|
||||
foreach (var attr in symbol.GetAttributes())
|
||||
{
|
||||
if (attr.AttributeClass?.ToDisplayString() == ViewVariablesNamespace)
|
||||
{
|
||||
viewVariablesAttribute = attr;
|
||||
}
|
||||
}
|
||||
if (viewVariablesAttribute == null)
|
||||
return false;
|
||||
|
||||
// Default is ReadOnly, which is fine
|
||||
if (viewVariablesAttribute.ConstructorArguments.Length == 0)
|
||||
return false;
|
||||
|
||||
var accessArgument = viewVariablesAttribute.ConstructorArguments[0];
|
||||
if (accessArgument.Value is not byte accessByte)
|
||||
return false;
|
||||
|
||||
return (VVAccess)accessByte == VVAccess.ReadWrite;
|
||||
}
|
||||
|
||||
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
|
||||
{
|
||||
return HasAttribute(type, NotYamlSerializableName);
|
||||
}
|
||||
|
||||
private static bool IsImplicitDataDefinition(ITypeSymbol type)
|
||||
{
|
||||
if (HasAttribute(type, ImplicitDataDefinitionNamespace))
|
||||
|
||||
@@ -15,11 +15,9 @@ public sealed class DefinitionFixer : CodeFixProvider
|
||||
{
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
|
||||
private const string ViewVariablesAttributeName = "ViewVariables";
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
|
||||
IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable,
|
||||
IdDataFieldRedundantTag, IdDataFieldNoVVReadWrite
|
||||
IdDataFieldRedundantTag
|
||||
);
|
||||
|
||||
public override Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
@@ -38,8 +36,6 @@ public sealed class DefinitionFixer : CodeFixProvider
|
||||
return RegisterDataFieldPropertyFix(context, diagnostic);
|
||||
case IdDataFieldRedundantTag:
|
||||
return RegisterRedundantTagFix(context, diagnostic);
|
||||
case IdDataFieldNoVVReadWrite:
|
||||
return RegisterVVReadWriteFix(context, diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,48 +136,6 @@ public sealed class DefinitionFixer : CodeFixProvider
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
private static async Task RegisterVVReadWriteFix(CodeFixContext context, Diagnostic diagnostic)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
var span = diagnostic.Location.SourceSpan;
|
||||
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().First();
|
||||
|
||||
if (token == null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(CodeAction.Create(
|
||||
"Remove ViewVariables attribute",
|
||||
c => RemoveVVAttribute(context.Document, token, c),
|
||||
"Remove ViewVariables attribute"
|
||||
), diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<Document> RemoveVVAttribute(Document document, MemberDeclarationSyntax syntax, CancellationToken cancellation)
|
||||
{
|
||||
var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation);
|
||||
|
||||
var newLists = new SyntaxList<AttributeListSyntax>();
|
||||
foreach (var attributeList in syntax.AttributeLists)
|
||||
{
|
||||
var attributes = new SeparatedSyntaxList<AttributeSyntax>();
|
||||
foreach (var attribute in attributeList.Attributes)
|
||||
{
|
||||
if (attribute.Name.ToString() != ViewVariablesAttributeName)
|
||||
{
|
||||
attributes = attributes.Add(attribute);
|
||||
}
|
||||
}
|
||||
// Don't add empty lists []
|
||||
if (attributes.Count > 0)
|
||||
newLists = newLists.Add(attributeList.WithAttributes(attributes));
|
||||
}
|
||||
var newSyntax = syntax.WithAttributeLists(newLists);
|
||||
|
||||
root = root!.ReplaceNode(syntax, newSyntax);
|
||||
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
private static async Task RegisterDataFieldFix(CodeFixContext context, Diagnostic diagnostic)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer that detects duplicate <c>[Dependency]</c> fields inside a single type.
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class DuplicateDependencyAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DependencyAttributeType = "Robust.Shared.IoC.DependencyAttribute";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
Diagnostics.IdDuplicateDependency,
|
||||
"Duplicate dependency field",
|
||||
"Another [Dependency] field of type '{0}' already exists in this type with field '{1}'",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterCompilationStartAction(compilationContext =>
|
||||
{
|
||||
var dependencyAttributeType = compilationContext.Compilation.GetTypeByMetadataName(DependencyAttributeType);
|
||||
if (dependencyAttributeType == null)
|
||||
return;
|
||||
|
||||
compilationContext.RegisterSymbolStartAction(symbolContext =>
|
||||
{
|
||||
var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
|
||||
// Only deal with non-static classes, doesn't make sense to have dependencies in anything else.
|
||||
if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsStatic)
|
||||
return;
|
||||
|
||||
var state = new AnalyzerState(dependencyAttributeType);
|
||||
symbolContext.RegisterSyntaxNodeAction(state.AnalyzeField, SyntaxKind.FieldDeclaration);
|
||||
symbolContext.RegisterSymbolEndAction(state.End);
|
||||
},
|
||||
SymbolKind.NamedType);
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class AnalyzerState(INamedTypeSymbol dependencyAttributeType)
|
||||
{
|
||||
private readonly Dictionary<ITypeSymbol, List<IFieldSymbol>> _dependencyFields = new(SymbolEqualityComparer.Default);
|
||||
|
||||
public void AnalyzeField(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var field = (FieldDeclarationSyntax)context.Node;
|
||||
if (field.AttributeLists.Count == 0)
|
||||
return;
|
||||
|
||||
if (context.ContainingSymbol is not IFieldSymbol fieldSymbol)
|
||||
return;
|
||||
|
||||
// Can't have [Dependency]s for non-reference types.
|
||||
if (!fieldSymbol.Type.IsReferenceType)
|
||||
return;
|
||||
|
||||
if (!IsDependency(context.ContainingSymbol))
|
||||
return;
|
||||
|
||||
lock (_dependencyFields)
|
||||
{
|
||||
if (!_dependencyFields.TryGetValue(fieldSymbol.Type, out var dependencyFields))
|
||||
{
|
||||
dependencyFields = [];
|
||||
_dependencyFields.Add(fieldSymbol.Type, dependencyFields);
|
||||
}
|
||||
|
||||
dependencyFields.Add(fieldSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsDependency(ISymbol symbol)
|
||||
{
|
||||
foreach (var attributeData in symbol.GetAttributes())
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, dependencyAttributeType))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void End(SymbolAnalysisContext context)
|
||||
{
|
||||
lock (_dependencyFields)
|
||||
{
|
||||
foreach (var pair in _dependencyFields)
|
||||
{
|
||||
var fieldType = pair.Key;
|
||||
var fields = pair.Value;
|
||||
if (fields.Count <= 1)
|
||||
continue;
|
||||
|
||||
// Sort so we can have deterministic order to skip reporting for a single field.
|
||||
// Whichever sorts first doesn't get reported.
|
||||
fields.Sort(static (a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
// Start at index 1 to skip first field.
|
||||
var firstField = fields[0];
|
||||
for (var i = 1; i < fields.Count; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
|
||||
context.ReportDiagnostic(
|
||||
Diagnostic.Create(Rule, field.Locations[0], fieldType.ToDisplayString(), firstField.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
|
||||
|
||||
public static DiagnosticDescriptor ForbidLiteralRule = new(
|
||||
Diagnostics.IdForbidLiteral,
|
||||
"Parameter forbids literal values",
|
||||
"The {0} parameter of {1} forbids literal values",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true,
|
||||
"Pass in a validated wrapper type like ProtoId, or a const or static value."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
|
||||
}
|
||||
|
||||
private void AnalyzeOperation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation invocationOperation)
|
||||
return;
|
||||
|
||||
// Check each parameter of the method invocation
|
||||
foreach (var argumentOperation in invocationOperation.Arguments)
|
||||
{
|
||||
// Check for our attribute on the parameter
|
||||
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
|
||||
continue;
|
||||
|
||||
// Handle parameters using the params keyword
|
||||
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
|
||||
{
|
||||
// Check each param value
|
||||
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
|
||||
{
|
||||
CheckArgumentSyntax(context, argumentOperation, subArgument);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not params, so just check the single parameter
|
||||
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
|
||||
continue;
|
||||
|
||||
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
|
||||
{
|
||||
// Handle collection types
|
||||
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
|
||||
{
|
||||
// Check each value of the collection
|
||||
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
|
||||
{
|
||||
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
|
||||
continue;
|
||||
|
||||
// Check if a literal was passed in
|
||||
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
|
||||
continue;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
|
||||
expressionSyntax.GetLocation(),
|
||||
operation.Parameter.Name,
|
||||
(context.Operation as IInvocationOperation).TargetMethod.Name
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not a collection, just a single value to check
|
||||
// Check if it's a literal
|
||||
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
|
||||
return;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
|
||||
argumentSyntax.GetLocation(),
|
||||
operation.Parameter.Name,
|
||||
(context.Operation as IInvocationOperation).TargetMethod.Name
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Enforces <c>MustCallBaseAttribute</c>.
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class MustCallBaseAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string Attribute = "Robust.Shared.Analyzers.MustCallBaseAttribute";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
Diagnostics.IdMustCallBase,
|
||||
"No base call in overriden function",
|
||||
"Overriders of this function must always call the base function",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
|
||||
}
|
||||
|
||||
private static void AnalyzeSymbol(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not IMethodSymbol { IsOverride: true } method)
|
||||
return;
|
||||
|
||||
var attrSymbol = context.Compilation.GetTypeByMetadataName(Attribute);
|
||||
if (attrSymbol == null)
|
||||
return;
|
||||
|
||||
if (DoesMethodOverriderHaveAttribute(method, attrSymbol) is not { } data)
|
||||
return;
|
||||
|
||||
if (data is { onlyOverrides: true, depth: < 2 })
|
||||
return;
|
||||
|
||||
var syntax = (MethodDeclarationSyntax) method.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
if (HasBaseCall(syntax))
|
||||
return;
|
||||
|
||||
var diag = Diagnostic.Create(Rule, syntax.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diag);
|
||||
}
|
||||
|
||||
private static (int depth, bool onlyOverrides)? DoesMethodOverriderHaveAttribute(
|
||||
IMethodSymbol method,
|
||||
INamedTypeSymbol attributeSymbol)
|
||||
{
|
||||
var depth = 0;
|
||||
while (method.OverriddenMethod != null)
|
||||
{
|
||||
depth += 1;
|
||||
method = method.OverriddenMethod;
|
||||
if (GetAttribute(method, attributeSymbol) is not { } attribute)
|
||||
continue;
|
||||
|
||||
var onlyOverrides = attribute.ConstructorArguments is [{Kind: TypedConstantKind.Primitive, Value: true}];
|
||||
return (depth, onlyOverrides);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool HasBaseCall(MethodDeclarationSyntax syntax)
|
||||
{
|
||||
return syntax.Accept(new BaseCallLocator());
|
||||
}
|
||||
|
||||
private static AttributeData? GetAttribute(ISymbol namedTypeSymbol, INamedTypeSymbol attrSymbol)
|
||||
{
|
||||
return namedTypeSymbol.GetAttributes()
|
||||
.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));
|
||||
}
|
||||
|
||||
private sealed class BaseCallLocator : CSharpSyntaxVisitor<bool>
|
||||
{
|
||||
public override bool VisitBaseExpression(BaseExpressionSyntax node)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool DefaultVisit(SyntaxNode node)
|
||||
{
|
||||
foreach (var childNode in node.ChildNodes())
|
||||
{
|
||||
if (childNode is not CSharpSyntaxNode cSharpSyntax)
|
||||
continue;
|
||||
|
||||
if (cSharpSyntax.Accept(this))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class ObsoleteInheritanceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string Attribute = "Robust.Shared.Analyzers.ObsoleteInheritanceAttribute";
|
||||
|
||||
public static readonly DiagnosticDescriptor Rule = new(
|
||||
Diagnostics.IdObsoleteInheritance,
|
||||
"Parent type has obsoleted inheritance",
|
||||
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public static readonly DiagnosticDescriptor RuleWithMessage = new(
|
||||
Diagnostics.IdObsoleteInheritanceWithMessage,
|
||||
"Parent type has obsoleted inheritance",
|
||||
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself: \"{2}\"",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, RuleWithMessage];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSymbolAction(CheckClass, SymbolKind.NamedType);
|
||||
}
|
||||
|
||||
private static void CheckClass(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
if (typeSymbol.IsValueType || typeSymbol.BaseType is not { } baseType)
|
||||
return;
|
||||
|
||||
if (!AttributeHelper.HasAttribute(baseType, Attribute, out var data))
|
||||
return;
|
||||
|
||||
var location = context.Symbol.Locations[0];
|
||||
|
||||
if (GetMessageFromAttributeData(data) is { } message)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
RuleWithMessage,
|
||||
location,
|
||||
[typeSymbol.Name, baseType.Name, message]));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
Rule,
|
||||
location,
|
||||
[typeSymbol.Name, baseType.Name]));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetMessageFromAttributeData(AttributeData data)
|
||||
{
|
||||
if (data.ConstructorArguments is not [var message, ..])
|
||||
return null;
|
||||
|
||||
return message.Value as string;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class PreferNonGenericVariantForAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string AttributeType = "Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute";
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
UseNonGenericVariantDescriptor
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UseNonGenericVariantDescriptor = new(
|
||||
Diagnostics.IdUseNonGenericVariant,
|
||||
"Consider using the non-generic variant of this method",
|
||||
"Use the non-generic variant of this method for type {0}",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true,
|
||||
"Use the generic variant of this method.");
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics | GeneratedCodeAnalysisFlags.Analyze);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterOperationAction(CheckForNonGenericVariant, OperationKind.Invocation);
|
||||
}
|
||||
|
||||
private void CheckForNonGenericVariant(OperationAnalysisContext obj)
|
||||
{
|
||||
if (obj.Operation is not IInvocationOperation invocationOperation) return;
|
||||
|
||||
var preferNonGenericAttribute = obj.Compilation.GetTypeByMetadataName(AttributeType);
|
||||
|
||||
HashSet<ITypeSymbol> forTypes = [];
|
||||
foreach (var attribute in invocationOperation.TargetMethod.GetAttributes())
|
||||
{
|
||||
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, preferNonGenericAttribute))
|
||||
continue;
|
||||
|
||||
foreach (var type in attribute.ConstructorArguments[0].Values)
|
||||
forTypes.Add((ITypeSymbol)type.Value);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (forTypes == null)
|
||||
return;
|
||||
|
||||
foreach (var typeArg in invocationOperation.TargetMethod.TypeArguments)
|
||||
{
|
||||
if (forTypes.Contains(typeArg))
|
||||
{
|
||||
obj.ReportDiagnostic(
|
||||
Diagnostic.Create(UseNonGenericVariantDescriptor,
|
||||
invocationOperation.Syntax.GetLocation(), typeArg.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class PreferOtherTypeAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string AttributeType = "Robust.Shared.Analyzers.PreferOtherTypeAttribute";
|
||||
|
||||
private static readonly DiagnosticDescriptor PreferOtherTypeDescriptor = new(
|
||||
Diagnostics.IdPreferOtherType,
|
||||
"Use the specific type",
|
||||
"Use the specific type {0} instead of {1} when the type argument is {2}",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
true,
|
||||
"Use the specific type.");
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
PreferOtherTypeDescriptor
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics | GeneratedCodeAnalysisFlags.Analyze);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeField, SyntaxKind.VariableDeclaration);
|
||||
}
|
||||
|
||||
private void AnalyzeField(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (context.Node is not VariableDeclarationSyntax node)
|
||||
return;
|
||||
|
||||
// Get the type of the generic being used
|
||||
if (node.Type is not GenericNameSyntax genericName)
|
||||
return;
|
||||
var genericSyntax = genericName.TypeArgumentList.Arguments[0];
|
||||
if (context.SemanticModel.GetSymbolInfo(genericSyntax).Symbol is not { } genericType)
|
||||
return;
|
||||
|
||||
// Look for the PreferOtherTypeAttribute
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(node.Type);
|
||||
if (symbolInfo.Symbol?.GetAttributes() is not { } attributes)
|
||||
return;
|
||||
|
||||
var preferOtherTypeAttribute = context.Compilation.GetTypeByMetadataName(AttributeType);
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, preferOtherTypeAttribute))
|
||||
continue;
|
||||
|
||||
// See if the generic type argument matches the type the attribute specifies
|
||||
if (attribute.ConstructorArguments[0].Value is not ITypeSymbol checkedType)
|
||||
return;
|
||||
if (!SymbolEqualityComparer.Default.Equals(checkedType, genericType))
|
||||
continue;
|
||||
|
||||
if (attribute.ConstructorArguments[1].Value is not ITypeSymbol replacementType)
|
||||
continue;
|
||||
context.ReportDiagnostic(Diagnostic.Create(PreferOtherTypeDescriptor,
|
||||
context.Node.GetLocation(),
|
||||
replacementType.Name,
|
||||
symbolInfo.Symbol.Name,
|
||||
genericType.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Robust.Roslyn.Shared.Diagnostics;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp)]
|
||||
public sealed class PreferOtherTypeFixer : CodeFixProvider
|
||||
{
|
||||
private const string PreferOtherTypeAttributeName = "PreferOtherTypeAttribute";
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
|
||||
IdPreferOtherType
|
||||
);
|
||||
|
||||
public override FixAllProvider GetFixAllProvider()
|
||||
{
|
||||
return WellKnownFixAllProviders.BatchFixer;
|
||||
}
|
||||
|
||||
public override Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
foreach (var diagnostic in context.Diagnostics)
|
||||
{
|
||||
switch (diagnostic.Id)
|
||||
{
|
||||
case IdPreferOtherType:
|
||||
return RegisterReplaceType(context, diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static async Task RegisterReplaceType(CodeFixContext context, Diagnostic diagnostic)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
var span = diagnostic.Location.SourceSpan;
|
||||
var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType<VariableDeclarationSyntax>().First();
|
||||
|
||||
if (token == null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(CodeAction.Create(
|
||||
"Replace type",
|
||||
c => ReplaceType(context.Document, token, c),
|
||||
"Replace type"
|
||||
), diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<Document> ReplaceType(Document document, VariableDeclarationSyntax syntax, CancellationToken cancellation)
|
||||
{
|
||||
var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation);
|
||||
var model = await document.GetSemanticModelAsync(cancellation);
|
||||
|
||||
if (model == null)
|
||||
return document;
|
||||
|
||||
if (syntax.Type is not GenericNameSyntax genericNameSyntax)
|
||||
return document;
|
||||
var genericTypeSyntax = genericNameSyntax.TypeArgumentList.Arguments[0];
|
||||
if (model.GetSymbolInfo(genericTypeSyntax).Symbol is not {} genericTypeSymbol)
|
||||
return document;
|
||||
|
||||
var symbolInfo = model.GetSymbolInfo(syntax.Type);
|
||||
if (symbolInfo.Symbol?.GetAttributes() is not { } attributes)
|
||||
return document;
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (attribute.AttributeClass?.Name != PreferOtherTypeAttributeName)
|
||||
continue;
|
||||
|
||||
if (attribute.ConstructorArguments[0].Value is not ITypeSymbol checkedTypeSymbol)
|
||||
continue;
|
||||
|
||||
if (!SymbolEqualityComparer.Default.Equals(checkedTypeSymbol, genericTypeSymbol))
|
||||
continue;
|
||||
|
||||
if (attribute.ConstructorArguments[1].Value is not ITypeSymbol replacementTypeSymbol)
|
||||
continue;
|
||||
|
||||
var replacementIdentifier = SyntaxFactory.IdentifierName(replacementTypeSymbol.Name);
|
||||
var replacementSyntax = syntax.WithType(replacementIdentifier);
|
||||
|
||||
root = root!.ReplaceNode(syntax, replacementSyntax);
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class PrototypeInstantiationAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
|
||||
|
||||
public static readonly DiagnosticDescriptor Rule = new(
|
||||
Diagnostics.IdPrototypeInstantiation,
|
||||
"Do not instantiate prototypes directly",
|
||||
"Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.RegisterCompilationStartAction(static ctx =>
|
||||
{
|
||||
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
|
||||
if (prototypeInterface == null)
|
||||
return;
|
||||
|
||||
ctx.RegisterOperationAction(symContext => Check(prototypeInterface, symContext), OperationKind.ObjectCreation);
|
||||
});
|
||||
}
|
||||
|
||||
private static void Check(INamedTypeSymbol prototypeInterface, OperationAnalysisContext ctx)
|
||||
{
|
||||
if (ctx.Operation is not IObjectCreationOperation { Type: { } resultType } creationOp)
|
||||
return;
|
||||
|
||||
if (!TypeSymbolHelper.ImplementsInterface(resultType, prototypeInterface))
|
||||
return;
|
||||
|
||||
ctx.ReportDiagnostic(Diagnostic.Create(Rule, creationOp.Syntax.GetLocation()));
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class PrototypeNetSerializableAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
|
||||
private const string NetSerializableAttributeType = "Robust.Shared.Serialization.NetSerializableAttribute";
|
||||
|
||||
public static readonly DiagnosticDescriptor RuleNetSerializable = new(
|
||||
Diagnostics.IdPrototypeNetSerializable,
|
||||
"Prototypes should not be [NetSerializable]",
|
||||
"Type {0} is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
|
||||
public static readonly DiagnosticDescriptor RuleSerializable = new(
|
||||
Diagnostics.IdPrototypeSerializable,
|
||||
"Prototypes should not be [Serializable]",
|
||||
"Type {0} is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [
|
||||
RuleNetSerializable,
|
||||
RuleSerializable
|
||||
];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
|
||||
context.RegisterCompilationStartAction(static ctx =>
|
||||
{
|
||||
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
|
||||
var netSerializableAttribute = ctx.Compilation.GetTypeByMetadataName(NetSerializableAttributeType);
|
||||
|
||||
if (prototypeInterface == null || netSerializableAttribute == null)
|
||||
return;
|
||||
|
||||
ctx.RegisterSymbolAction(symbolContext => CheckClass(prototypeInterface, netSerializableAttribute, symbolContext), SymbolKind.NamedType);
|
||||
});
|
||||
}
|
||||
|
||||
private static void CheckClass(
|
||||
INamedTypeSymbol prototypeInterface,
|
||||
INamedTypeSymbol netSerializableAttribute,
|
||||
SymbolAnalysisContext symbolContext)
|
||||
{
|
||||
if (symbolContext.Symbol is not INamedTypeSymbol symbol)
|
||||
return;
|
||||
|
||||
if (!TypeSymbolHelper.ImplementsInterface(symbol, prototypeInterface))
|
||||
return;
|
||||
|
||||
if (AttributeHelper.HasAttribute(symbol, netSerializableAttribute, out _))
|
||||
{
|
||||
symbolContext.ReportDiagnostic(
|
||||
Diagnostic.Create(RuleNetSerializable, symbol.Locations[0], symbol.ToDisplayString()));
|
||||
}
|
||||
|
||||
if (symbol.IsSerializable)
|
||||
{
|
||||
symbolContext.ReportDiagnostic(
|
||||
Diagnostic.Create(RuleSerializable, symbol.Locations[0], symbol.ToDisplayString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,9 @@
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for PreferNonGenericVariantAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for PreferOtherTypeAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Needed for DataDefinitionAnalyzer. -->
|
||||
<Compile Include="..\Robust.Shared\Serialization\Manager\Definition\DataDefinitionUtility.cs" LinkBase="Implementations" />
|
||||
<Compile Include="..\Robust.Shared\ViewVariables\ViewVariablesAttribute.cs" LinkBase="Implementations" />
|
||||
<Compile Include="..\Robust.Shared\Serialization\NetSerializableAttribute.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.Benchmarks.EntityManager;
|
||||
|
||||
[Virtual]
|
||||
public partial class HasComponentBenchmark
|
||||
{
|
||||
private static readonly Consumer Consumer = new();
|
||||
|
||||
private ISimulation _simulation = default!;
|
||||
private IEntityManager _entityManager = default!;
|
||||
|
||||
private ComponentRegistration _compReg = default!;
|
||||
|
||||
private A _dummyA = new();
|
||||
|
||||
[UsedImplicitly]
|
||||
[Params(1, 10, 100, 1000)]
|
||||
public int N;
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
_simulation = RobustServerSimulation
|
||||
.NewSimulation()
|
||||
.RegisterComponents(f => f.RegisterClass<A>())
|
||||
.InitializeInstance();
|
||||
|
||||
_entityManager = _simulation.Resolve<IEntityManager>();
|
||||
var map = _simulation.CreateMap().Uid;
|
||||
var coords = new EntityCoordinates(map, default);
|
||||
_compReg = _entityManager.ComponentFactory.GetRegistration(typeof(A));
|
||||
|
||||
for (var i = 0; i < N; i++)
|
||||
{
|
||||
var uid = _entityManager.SpawnEntity(null, coords);
|
||||
_entityManager.AddComponent<A>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void HasComponentGeneric()
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
var uid = new EntityUid(i);
|
||||
var result = _entityManager.HasComponent<A>(uid);
|
||||
Consumer.Consume(result);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void HasComponentCompReg()
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
var uid = new EntityUid(i);
|
||||
var result = _entityManager.HasComponent(uid, _compReg);
|
||||
Consumer.Consume(result);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void HasComponentType()
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
var uid = new EntityUid(i);
|
||||
var result = _entityManager.HasComponent(uid, typeof(A));
|
||||
Consumer.Consume(result);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void HasComponentGetType()
|
||||
{
|
||||
for (var i = 2; i <= N+1; i++)
|
||||
{
|
||||
var uid = new EntityUid(i);
|
||||
var type = _dummyA.GetType();
|
||||
var result = _entityManager.HasComponent(uid, type);
|
||||
Consumer.Consume(result);
|
||||
}
|
||||
}
|
||||
|
||||
[ComponentProtoName("A")]
|
||||
public sealed partial class A : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.Benchmarks.Physics;
|
||||
|
||||
[Virtual]
|
||||
[MediumRunJob]
|
||||
public class PhysicsBoxStackBenchmark
|
||||
{
|
||||
private ISimulation _sim = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void BoxStack()
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTumbler(IEntityManager entManager, MapId mapId)
|
||||
{
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
|
||||
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
|
||||
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
|
||||
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
|
||||
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
|
||||
|
||||
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
|
||||
fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground);
|
||||
|
||||
var xs = new[]
|
||||
{
|
||||
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
|
||||
};
|
||||
|
||||
var columnCount = 1;
|
||||
var rowCount = 15;
|
||||
PolygonShape shape;
|
||||
|
||||
for (var j = 0; j < columnCount; j++)
|
||||
{
|
||||
for (var i = 0; i < rowCount; i++)
|
||||
{
|
||||
var x = 0.0f;
|
||||
|
||||
var boxUid = entManager.SpawnEntity(null,
|
||||
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 1.1f * i), mapId));
|
||||
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
|
||||
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
|
||||
|
||||
shape = new PolygonShape();
|
||||
shape.SetAsBox(0.5f, 0.5f);
|
||||
physics.SetFixedRotation(boxUid, false, body: box);
|
||||
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true), body: box);
|
||||
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
}
|
||||
|
||||
physics.WakeBody(groundUid, body: ground);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.Benchmarks.Physics;
|
||||
|
||||
[Virtual]
|
||||
public class PhysicsCircleStackBenchmark
|
||||
{
|
||||
private ISimulation _sim = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CircleStack()
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 10000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTumbler(IEntityManager entManager, MapId mapId)
|
||||
{
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
|
||||
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
|
||||
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
|
||||
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
|
||||
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
|
||||
|
||||
var vertical = new EdgeShape(new Vector2(20, 0), new Vector2(20, 20));
|
||||
fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground);
|
||||
|
||||
var xs = new[]
|
||||
{
|
||||
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
|
||||
};
|
||||
|
||||
var columnCount = 1;
|
||||
var rowCount = 15;
|
||||
PhysShapeCircle shape;
|
||||
|
||||
for (var j = 0; j < columnCount; j++)
|
||||
{
|
||||
for (var i = 0; i < rowCount; i++)
|
||||
{
|
||||
var x = 0.0f;
|
||||
|
||||
var boxUid = entManager.SpawnEntity(null,
|
||||
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
|
||||
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
|
||||
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
|
||||
shape = new PhysShapeCircle(0.5f);
|
||||
physics.SetFixedRotation(boxUid, false, body: box);
|
||||
// TODO: Need to detect shape and work out if we need to use fixedrotation
|
||||
|
||||
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f));
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
}
|
||||
|
||||
physics.WakeBody(groundUid, body: ground);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.Benchmarks.Physics;
|
||||
|
||||
[Virtual]
|
||||
public class PhysicsPyramidBenchmark
|
||||
{
|
||||
private ISimulation _sim = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Pyramid()
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 5000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTumbler(IEntityManager entManager, MapId mapId)
|
||||
{
|
||||
const byte count = 20;
|
||||
|
||||
// Setup ground
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
|
||||
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
|
||||
var horizontal = new EdgeShape(new Vector2(40, 0), new Vector2(-40, 0));
|
||||
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
|
||||
physics.WakeBody(groundUid, body: ground);
|
||||
|
||||
// Setup boxes
|
||||
float a = 0.5f;
|
||||
PolygonShape shape = new();
|
||||
shape.SetAsBox(a, a);
|
||||
|
||||
var x = new Vector2(-7.0f, 0.75f);
|
||||
Vector2 y;
|
||||
Vector2 deltaX = new Vector2(0.5625f, 1.25f);
|
||||
Vector2 deltaY = new Vector2(1.125f, 0.0f);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
y = x;
|
||||
|
||||
for (var j = i; j < count; ++j)
|
||||
{
|
||||
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(y, mapId));
|
||||
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
|
||||
|
||||
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f), body: box);
|
||||
y += deltaY;
|
||||
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
|
||||
x += deltaX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.UnitTesting.Server;
|
||||
|
||||
namespace Robust.Benchmarks.Physics;
|
||||
|
||||
[Virtual]
|
||||
public class PhysicsTumblerBenchmark
|
||||
{
|
||||
private ISimulation _sim = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
|
||||
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
var mapUid = entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
|
||||
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
|
||||
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
|
||||
physics.SetFixedRotation(boxUid, false, body: box);
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(0.125f, 0.125f);
|
||||
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 0.0625f), body: box);
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
|
||||
if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase))
|
||||
entManager.System<SharedBroadphaseSystem>().RebuildBottomUp(mapBroadphase);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Tumbler()
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTumbler(IEntityManager entManager, MapId mapId)
|
||||
{
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
var joints = entManager.System<SharedJointSystem>();
|
||||
|
||||
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId));
|
||||
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
|
||||
// Due to lookup changes fixtureless bodies are invalid, so
|
||||
var cShape = new PhysShapeCircle(1f);
|
||||
fixtures.CreateFixture(groundUid, "fix1", new Fixture(cShape, 0, 0, false));
|
||||
|
||||
var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
|
||||
var body = entManager.AddComponent<PhysicsComponent>(bodyUid);
|
||||
|
||||
physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body);
|
||||
physics.SetSleepingAllowed(bodyUid, body, false);
|
||||
physics.SetFixedRotation(bodyUid, false, body: body);
|
||||
|
||||
|
||||
// TODO: Box2D just deref, bleh shape structs someday
|
||||
var shape1 = new PolygonShape();
|
||||
shape1.SetAsBox(0.5f, 10.0f, new Vector2(10.0f, 0.0f), 0.0f);
|
||||
fixtures.CreateFixture(bodyUid, "fix1", new Fixture(shape1, 2, 0, true, 20f));
|
||||
|
||||
var shape2 = new PolygonShape();
|
||||
shape2.SetAsBox(0.5f, 10.0f, new Vector2(-10.0f, 0.0f), 0f);
|
||||
fixtures.CreateFixture(bodyUid, "fix2", new Fixture(shape2, 2, 0, true, 20f));
|
||||
|
||||
var shape3 = new PolygonShape();
|
||||
shape3.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, 10.0f), 0f);
|
||||
fixtures.CreateFixture(bodyUid, "fix3", new Fixture(shape3, 2, 0, true, 20f));
|
||||
|
||||
var shape4 = new PolygonShape();
|
||||
shape4.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, -10.0f), 0f);
|
||||
fixtures.CreateFixture(bodyUid, "fix4", new Fixture(shape4, 2, 0, true, 20f));
|
||||
|
||||
physics.WakeBody(groundUid, body: ground);
|
||||
physics.WakeBody(bodyUid, body: body);
|
||||
var revolute = joints.CreateRevoluteJoint(groundUid, bodyUid);
|
||||
revolute.LocalAnchorA = new Vector2(0f, 10f);
|
||||
revolute.LocalAnchorB = new Vector2(0f, 0f);
|
||||
revolute.ReferenceAngle = 0f;
|
||||
revolute.MotorSpeed = 0.05f * MathF.PI;
|
||||
revolute.MaxMotorTorque = 100000000f;
|
||||
revolute.EnableMotor = true;
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,6 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
|
||||
<!-- Needed to pin transitive dependency versions. -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" />
|
||||
<PackageReference Include="System.Formats.Asn1" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
@@ -42,8 +42,8 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
|
||||
public void GlobalSetup()
|
||||
{
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
var server = StartServer(new() {Pool = false});
|
||||
var client = StartClient(new() {Pool = false});
|
||||
var server = StartServer();
|
||||
var client = StartClient();
|
||||
|
||||
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Robust.Xaml;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
@@ -37,12 +37,10 @@ namespace Robust.Build.Tasks
|
||||
var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
|
||||
BuildEngine.LogMessage(msg, MessageImportance.High);
|
||||
|
||||
var res = XamlAotCompiler.Compile(
|
||||
BuildEngine, input,
|
||||
var res = XamlCompiler.Compile(BuildEngine, input,
|
||||
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
|
||||
OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null
|
||||
);
|
||||
ProjectDirectory, OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null);
|
||||
if (!res.success)
|
||||
return false;
|
||||
if (!res.writtentofile)
|
||||
@@ -67,24 +65,22 @@ namespace Robust.Build.Tasks
|
||||
return true;
|
||||
}
|
||||
|
||||
// PYREX NOTE: This project was comically null-unsafe before I touched it. I'm just marking what it did accurately
|
||||
[Required]
|
||||
public string ReferencesFilePath { get; set; } = null!;
|
||||
public string ReferencesFilePath { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
public string ProjectDirectory { get; set; } = null!;
|
||||
public string ProjectDirectory { get; set; }
|
||||
|
||||
[Required]
|
||||
public string AssemblyFile { get; set; } = null!;
|
||||
public string AssemblyFile { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? OriginalCopyPath { get; set; } = null;
|
||||
public string OriginalCopyPath { get; set; }
|
||||
|
||||
public string? OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; } = null!;
|
||||
public string OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; }
|
||||
|
||||
public string AssemblyOriginatorKeyFile { get; set; } = null!;
|
||||
public string AssemblyOriginatorKeyFile { get; set; }
|
||||
public bool SignAssembly { get; set; }
|
||||
public bool DelaySign { get; set; }
|
||||
|
||||
@@ -99,7 +95,7 @@ namespace Robust.Build.Tasks
|
||||
return rv;
|
||||
}
|
||||
|
||||
public IBuildEngine BuildEngine { get; set; } = null!;
|
||||
public ITaskHost HostObject { get; set; } = null!;
|
||||
public IBuildEngine BuildEngine { get; set; }
|
||||
public ITaskHost HostObject { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.Build.Framework;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken from https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Extensions.cs
|
||||
/// </summary>
|
||||
internal static class Extensions
|
||||
public static class Extensions
|
||||
{
|
||||
//shamefully copied from avalonia
|
||||
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)
|
||||
37
Robust.Client.Injectors/MathParsing.cs
Normal file
37
Robust.Client.Injectors/MathParsing.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using Pidgin;
|
||||
using static Pidgin.Parser;
|
||||
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
public static class MathParsing
|
||||
{
|
||||
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
|
||||
|
||||
public static Parser<char, float> Single1 { get; }
|
||||
= Single.Between(SkipWhitespaces);
|
||||
|
||||
public static Parser<char, (float, float)> Single2 { get; }
|
||||
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
|
||||
{
|
||||
var arr = e.ToArray();
|
||||
return (arr[0], arr[1]);
|
||||
});
|
||||
|
||||
public static Parser<char, (float, float, float, float)> Single4 { get; }
|
||||
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
|
||||
{
|
||||
var arr = e.ToArray();
|
||||
return (arr[0], arr[1], arr[2], arr[3]);
|
||||
});
|
||||
|
||||
public static Parser<char, float[]> Thickness { get; }
|
||||
= SkipWhitespaces.Then(
|
||||
OneOf(
|
||||
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4})),
|
||||
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
|
||||
Try(Single1.Select(c => new[] {c}))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,9 @@ namespace Robust.Build.Tasks
|
||||
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
|
||||
IDictionary targetOutputs) => throw new NotSupportedException();
|
||||
|
||||
// PYREX NOTE: This project was extremely null-unsafe before I touched it. I'm just marking what it did already
|
||||
// Here's the broken interface of IBuildEngine that we started with
|
||||
public bool ContinueOnError => default;
|
||||
public int LineNumberOfTaskNode => default;
|
||||
public int ColumnNumberOfTaskNode => default;
|
||||
public string ProjectFileOfTaskNode => null!;
|
||||
public bool ContinueOnError { get; }
|
||||
public int LineNumberOfTaskNode { get; }
|
||||
public int ColumnNumberOfTaskNode { get; }
|
||||
public string ProjectFileOfTaskNode { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using XamlX.Ast;
|
||||
using System.Reflection.Emit;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
internal class RXamlColorAstNode
|
||||
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
|
||||
@@ -6,9 +6,9 @@ using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
internal abstract class RXamlVecLikeConstAstNode<T>
|
||||
public abstract class RXamlVecLikeConstAstNode<T>
|
||||
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
|
||||
where T : unmanaged
|
||||
{
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Xaml
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
|
||||
public sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
|
||||
{
|
||||
public RXamlSingleVecLikeConstAstNode(
|
||||
IXamlLineInfo lineInfo,
|
||||
@@ -69,7 +69,7 @@ namespace Robust.Xaml
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
|
||||
public sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
|
||||
{
|
||||
public RXamlInt32VecLikeConstAstNode(
|
||||
IXamlLineInfo lineInfo,
|
||||
@@ -2,9 +2,9 @@
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
internal class RXamlWellKnownTypes
|
||||
class RXamlWellKnownTypes
|
||||
{
|
||||
public XamlTypeWellKnownTypes XamlIlTypes { get; }
|
||||
public IXamlType Single { get; }
|
||||
@@ -1,30 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\MSBuild\Robust.Engine.props" />
|
||||
|
||||
<!--
|
||||
PJB3005 (2024-08-24)
|
||||
So the reason that Robust.Client.Injectors is NS2.0 is that Visual Studio
|
||||
still ships a .NET FX based MSBuild for some godforsaken reason. This means
|
||||
that when having Robust.Client.Injectors loaded directly by the main MSBuild
|
||||
process... that would break.
|
||||
|
||||
Except we don't do that anyways right now due to file locking issues, so maybe
|
||||
it's fine to give up on that. Whatever.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
|
||||
<PackageReference Include="Pidgin" Version="2.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
|
||||
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
@@ -6,7 +7,7 @@ using XamlX.IL;
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitters & Transformers based on:
|
||||
@@ -14,7 +15,7 @@ namespace Robust.Xaml
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/afb8ae6f3c517dae912729511483995b16cb31af/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs
|
||||
/// </summary>
|
||||
internal class RobustXamlILCompiler : XamlILCompiler
|
||||
public class RobustXamlILCompiler : XamlILCompiler
|
||||
{
|
||||
public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings, bool fillWithDefaults) : base(configuration, emitMappings, fillWithDefaults)
|
||||
{
|
||||
@@ -40,9 +41,8 @@ namespace Robust.Xaml
|
||||
&& mg.Children.OfType<RobustNameScopeRegistrationXamlIlNode>().Any())
|
||||
return node;
|
||||
|
||||
IXamlAstValueNode? value = null;
|
||||
IXamlAstValueNode value = null;
|
||||
for (var c = 0; c < pa.Values.Count; c++)
|
||||
{
|
||||
if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String))
|
||||
{
|
||||
value = pa.Values[c];
|
||||
@@ -57,7 +57,6 @@ namespace Robust.Xaml
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
@@ -85,9 +84,9 @@ namespace Robust.Xaml
|
||||
class RobustNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode
|
||||
{
|
||||
public IXamlAstValueNode Name { get; set; }
|
||||
public IXamlType? TargetType { get; }
|
||||
public IXamlType TargetType { get; }
|
||||
|
||||
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType? targetType) : base(name)
|
||||
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name)
|
||||
{
|
||||
TargetType = targetType;
|
||||
Name = name;
|
||||
@@ -105,7 +104,7 @@ namespace Robust.Xaml
|
||||
{
|
||||
|
||||
var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
|
||||
f.Name == XamlCustomizations.ContextNameScopeFieldName);
|
||||
f.Name == XamlCompiler.ContextNameScopeFieldName);
|
||||
var namescopeRegisterFunction = context.Configuration.TypeSystem
|
||||
.FindType("Robust.Client.UserInterface.XAML.NameScope").Methods
|
||||
.First(m => m.Name == "Register");
|
||||
@@ -129,7 +128,7 @@ namespace Robust.Xaml
|
||||
|
||||
return XamlILNodeEmitResult.Void(1);
|
||||
}
|
||||
return default!; // PYREX NOTE: This doesn't seem safe! But it's what we were doing before Nullable
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +161,7 @@ namespace Robust.Xaml
|
||||
{
|
||||
if (!(node is HandleRootObjectScopeNode))
|
||||
{
|
||||
return null!; // PYREX NOTE: This doesn't seem safe, but it predates Nullable on this file
|
||||
return null;
|
||||
}
|
||||
|
||||
var controlType = context.Configuration.TypeSystem.FindType("Robust.Client.UserInterface.Control");
|
||||
@@ -171,7 +170,7 @@ namespace Robust.Xaml
|
||||
var dontAbsorb = codeGen.DefineLabel();
|
||||
var end = codeGen.DefineLabel();
|
||||
var contextScopeField = context.RuntimeContext.ContextType.Fields.First(f =>
|
||||
f.Name == XamlCustomizations.ContextNameScopeFieldName);
|
||||
f.Name == XamlCompiler.ContextNameScopeFieldName);
|
||||
var controlNameScopeField = controlType.Fields.First(f => f.Name == "NameScope");
|
||||
var nameScopeType = context.Configuration.TypeSystem
|
||||
.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
||||
@@ -1,23 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Collections.Generic;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Xaml
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers taken from AvaloniaUI on GitHub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Helpers taken from:
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
||||
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
|
||||
/// </remarks>
|
||||
internal partial class XamlAotCompiler
|
||||
/// </summary>
|
||||
public partial class XamlCompiler
|
||||
{
|
||||
private static readonly string[] NameSuffixes = {".xaml", ".paml", ".axaml"};
|
||||
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
|
||||
|| r.Name.ToLowerInvariant().EndsWith(".paml")
|
||||
|| r.Name.ToLowerInvariant().EndsWith(".axaml");
|
||||
|
||||
static bool CheckXamlName(IResource r) =>
|
||||
NameSuffixes.Any(suffix => r.Name.ToLowerInvariant().EndsWith(suffix));
|
||||
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
|
||||
{
|
||||
var i = instructions[idx];
|
||||
// A "normal" way of passing `this` to a static method:
|
||||
|
||||
// ldarg.0
|
||||
// call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
|
||||
|
||||
return i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true);
|
||||
}
|
||||
|
||||
interface IResource : IFileSource
|
||||
{
|
||||
389
Robust.Client.Injectors/XamlCompiler.cs
Normal file
389
Robust.Client.Injectors/XamlCompiler.cs
Normal file
@@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Cecil.Rocks;
|
||||
using Pidgin;
|
||||
using XamlX;
|
||||
using XamlX.Ast;
|
||||
using XamlX.Emit;
|
||||
using XamlX.IL;
|
||||
using XamlX.Parsers;
|
||||
using XamlX.Transform;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
namespace Robust.Build.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
|
||||
/// Adjusted for our UI-Framework
|
||||
/// </summary>
|
||||
public partial class XamlCompiler
|
||||
{
|
||||
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
|
||||
string projectDirectory, string output, string strongNameKey)
|
||||
{
|
||||
var typeSystem = new CecilTypeSystem(references
|
||||
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
|
||||
.Concat(new[] { input }), input);
|
||||
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
|
||||
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
|
||||
{
|
||||
// If this type exists, the assembly has already been processed by us.
|
||||
// Do not run again, it would corrupt the file.
|
||||
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
|
||||
// but better safe than sorry eh?
|
||||
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var compileRes = CompileCore(engine, typeSystem);
|
||||
if (compileRes == null)
|
||||
return (true, false);
|
||||
if (compileRes == false)
|
||||
return (false, false);
|
||||
|
||||
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
|
||||
if (!string.IsNullOrWhiteSpace(strongNameKey))
|
||||
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
|
||||
|
||||
asm.Write(output, writerParameters);
|
||||
|
||||
return (true, true);
|
||||
|
||||
}
|
||||
|
||||
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
|
||||
{
|
||||
var asm = typeSystem.TargetAssemblyDefinition;
|
||||
var embrsc = new EmbeddedResources(asm);
|
||||
|
||||
if (embrsc.Resources.Count(CheckXamlName) == 0)
|
||||
// Nothing to do
|
||||
return null;
|
||||
|
||||
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
|
||||
{
|
||||
XmlnsAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
|
||||
|
||||
},
|
||||
ContentAttributes =
|
||||
{
|
||||
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
|
||||
},
|
||||
UsableDuringInitializationAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
|
||||
},
|
||||
DeferredContentPropertyAttributes =
|
||||
{
|
||||
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
|
||||
},
|
||||
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
|
||||
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
|
||||
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
|
||||
};
|
||||
var emitConfig = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
|
||||
{
|
||||
ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c)
|
||||
};
|
||||
|
||||
var transformerconfig = new TransformerConfiguration(
|
||||
typeSystem,
|
||||
typeSystem.TargetAssembly,
|
||||
xamlLanguage,
|
||||
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
|
||||
|
||||
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
|
||||
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
|
||||
asm.MainModule.Types.Add(contextDef);
|
||||
var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
|
||||
xamlLanguage, emitConfig);
|
||||
|
||||
var compiler =
|
||||
new RobustXamlILCompiler(transformerconfig, emitConfig, true);
|
||||
|
||||
bool CompileGroup(IResourceGroup group)
|
||||
{
|
||||
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
|
||||
asm.MainModule.TypeSystem.Object);
|
||||
|
||||
//typeDef.CustomAttributes.Add(new CustomAttribute(ed));
|
||||
asm.MainModule.Types.Add(typeDef);
|
||||
var builder = typeSystem.CreateTypeBuilder(typeDef);
|
||||
|
||||
foreach (var res in group.Resources.Where(CheckXamlName))
|
||||
{
|
||||
try
|
||||
{
|
||||
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
|
||||
|
||||
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
|
||||
var parsed = XDocumentXamlParser.Parse(xaml);
|
||||
|
||||
var initialRoot = (XamlAstObjectNode) parsed.Root;
|
||||
|
||||
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
|
||||
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
|
||||
string classname;
|
||||
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
|
||||
{
|
||||
classname = tn.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
classname = res.Name.Replace(".xaml","");
|
||||
}
|
||||
|
||||
var classType = typeSystem.TargetAssembly.FindType(classname);
|
||||
if (classType == null)
|
||||
throw new Exception($"Unable to find type '{classname}'");
|
||||
|
||||
compiler.Transform(parsed);
|
||||
|
||||
var populateName = $"Populate:{res.Name}";
|
||||
var buildName = $"Build:{res.Name}";
|
||||
|
||||
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve();
|
||||
|
||||
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
|
||||
|
||||
compiler.Compile(parsed, contextClass,
|
||||
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
|
||||
classTypeDefinition == null),
|
||||
compiler.DefineBuildMethod(builder, parsed, buildName, true),
|
||||
null,
|
||||
(closureName, closureBaseType) =>
|
||||
populateBuilder.DefineSubType(closureBaseType, closureName, false),
|
||||
res.Uri, res
|
||||
);
|
||||
|
||||
//add compiled populate method
|
||||
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
|
||||
.First(m => m.Name == populateName);
|
||||
|
||||
const string TrampolineName = "!XamlIlPopulateTrampoline";
|
||||
var trampoline = new MethodDefinition(TrampolineName,
|
||||
MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void);
|
||||
trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
|
||||
classTypeDefinition.Methods.Add(trampoline);
|
||||
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod));
|
||||
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
|
||||
|
||||
var foundXamlLoader = false;
|
||||
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
|
||||
foreach (var method in classTypeDefinition.Methods
|
||||
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
|
||||
{
|
||||
var i = method.Body.Instructions;
|
||||
for (var c = 1; c < i.Count; c++)
|
||||
{
|
||||
if (i[c].OpCode == OpCodes.Call)
|
||||
{
|
||||
var op = i[c].Operand as MethodReference;
|
||||
|
||||
if (op != null
|
||||
&& op.Name == TrampolineName)
|
||||
{
|
||||
foundXamlLoader = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (op != null
|
||||
&& op.Name == "Load"
|
||||
&& op.Parameters.Count == 1
|
||||
&& op.Parameters[0].ParameterType.FullName == "System.Object"
|
||||
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
|
||||
{
|
||||
if (MatchThisCall(i, c - 1))
|
||||
{
|
||||
i[c].Operand = trampoline;
|
||||
foundXamlLoader = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundXamlLoader)
|
||||
{
|
||||
var ctors = classTypeDefinition.GetConstructors()
|
||||
.Where(c => !c.IsStatic).ToList();
|
||||
// We can inject xaml loader into default constructor
|
||||
if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode != OpCodes.Nop) == 3)
|
||||
{
|
||||
var i = ctors[0].Body.Instructions;
|
||||
var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
|
||||
i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidProgramException(
|
||||
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
|
||||
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
|
||||
}
|
||||
res.Remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (embrsc.Resources.Count(CheckXamlName) != 0)
|
||||
{
|
||||
if (!CompileGroup(embrsc))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CustomValueConverter(
|
||||
AstTransformationContext context,
|
||||
IXamlAstValueNode node,
|
||||
IXamlType type,
|
||||
out IXamlAstValueNode result)
|
||||
{
|
||||
if (!(node is XamlAstTextNode textNode))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = textNode.Text;
|
||||
var types = context.GetRobustTypes();
|
||||
|
||||
if (type.Equals(types.Vector2))
|
||||
{
|
||||
var foo = MathParsing.Single2.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
|
||||
|
||||
var (x, y) = foo.Value;
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Vector2, types.Vector2ConstructorFull,
|
||||
types.Single, new[] {x, y});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Thickness))
|
||||
{
|
||||
var foo = MathParsing.Thickness.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
||||
|
||||
var val = foo.Value;
|
||||
float[] full;
|
||||
if (val.Length == 1)
|
||||
{
|
||||
var u = val[0];
|
||||
full = new[] {u, u, u, u};
|
||||
}
|
||||
else if (val.Length == 2)
|
||||
{
|
||||
var h = val[0];
|
||||
var v = val[1];
|
||||
full = new[] {h, v, h, v};
|
||||
}
|
||||
else // 4
|
||||
{
|
||||
full = val;
|
||||
}
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Thickness, types.ThicknessConstructorFull,
|
||||
types.Single, full);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Thickness))
|
||||
{
|
||||
var foo = MathParsing.Thickness.Parse(text);
|
||||
|
||||
if (!foo.Success)
|
||||
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
|
||||
|
||||
var val = foo.Value;
|
||||
float[] full;
|
||||
if (val.Length == 1)
|
||||
{
|
||||
var u = val[0];
|
||||
full = new[] {u, u, u, u};
|
||||
}
|
||||
else if (val.Length == 2)
|
||||
{
|
||||
var h = val[0];
|
||||
var v = val[1];
|
||||
full = new[] {h, v, h, v};
|
||||
}
|
||||
else // 4
|
||||
{
|
||||
full = val;
|
||||
}
|
||||
|
||||
result = new RXamlSingleVecLikeConstAstNode(
|
||||
node,
|
||||
types.Thickness, types.ThicknessConstructorFull,
|
||||
types.Single, full);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.Equals(types.Color))
|
||||
{
|
||||
// TODO: Interpret these colors at XAML compile time instead of at runtime.
|
||||
result = new RXamlColorAstNode(node, types, text);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public const string ContextNameScopeFieldName = "RobustNameScope";
|
||||
|
||||
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)
|
||||
{
|
||||
var nameScopeType = typeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
|
||||
var field = typeBuilder.DefineField(nameScopeType,
|
||||
ContextNameScopeFieldName, true, false);
|
||||
constructor
|
||||
.Ldarg_0()
|
||||
.Newobj(nameScopeType.GetConstructor())
|
||||
.Stfld(field);
|
||||
}
|
||||
}
|
||||
|
||||
interface IResource : IFileSource
|
||||
{
|
||||
string Uri { get; }
|
||||
string Name { get; }
|
||||
void Remove();
|
||||
|
||||
}
|
||||
|
||||
interface IResourceGroup
|
||||
{
|
||||
string Name { get; }
|
||||
IEnumerable<IResource> Resources { get; }
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,6 @@ using XamlX.Transform;
|
||||
using XamlX.Transform.Transformers;
|
||||
using XamlX.TypeSystem;
|
||||
|
||||
// Yes dude I know this source generator isn't incremental, I'll fix it eventually.
|
||||
#pragma warning disable RS1035
|
||||
|
||||
namespace Robust.Client.NameGenerator
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -64,8 +64,6 @@ internal abstract class BaseRobustCefClient : CefClient
|
||||
string title,
|
||||
string defaultFilePath,
|
||||
string[] acceptFilters,
|
||||
string[] acceptExtensions,
|
||||
string[] acceptDescriptions,
|
||||
CefFileDialogCallback callback)
|
||||
{
|
||||
callback.Cancel();
|
||||
|
||||
@@ -6,7 +6,7 @@ using Xilium.CefGlue;
|
||||
|
||||
namespace Robust.Client.WebView.Cef
|
||||
{
|
||||
internal static class Program
|
||||
public static class Program
|
||||
{
|
||||
// This was supposed to be the main entry for the subprocess program... It doesn't work.
|
||||
public static int Main(string[] args)
|
||||
|
||||
@@ -44,8 +44,6 @@ namespace Robust.Client.WebView.Cef
|
||||
//commandLine.AppendSwitch("--disable-gpu-compositing");
|
||||
//commandLine.AppendSwitch("--in-process-gpu");
|
||||
|
||||
commandLine.AppendSwitch("--off-screen-rendering-enabled");
|
||||
|
||||
commandLine.AppendSwitch("disable-threaded-scrolling", "1");
|
||||
commandLine.AppendSwitch("disable-features", "TouchpadAndWheelScrollLatching,AsyncWheelEvents");
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Robust.Client.WebView.Cef
|
||||
var info = CefWindowInfo.Create();
|
||||
info.Bounds = new CefRectangle(0, 0, createParams.Width, createParams.Height);
|
||||
info.SetAsPopup(mainHWnd, "ss14cef");
|
||||
info.RuntimeStyle = CefRuntimeStyle.Alloy;
|
||||
|
||||
var impl = new WebViewWindowImpl(this);
|
||||
|
||||
|
||||
@@ -162,10 +162,9 @@ namespace Robust.Client.WebView.Cef
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOpen => _data != null;
|
||||
public bool IsLoading => _data?.Browser.IsLoading ?? false;
|
||||
|
||||
public void StartBrowser()
|
||||
public void EnteredTree()
|
||||
{
|
||||
DebugTools.AssertNull(_data);
|
||||
|
||||
@@ -196,7 +195,7 @@ namespace Robust.Client.WebView.Cef
|
||||
_data = new LiveData(texture, client, browser, renderer);
|
||||
}
|
||||
|
||||
public void CloseBrowser()
|
||||
public void ExitedTree()
|
||||
{
|
||||
DebugTools.AssertNotNull(_data);
|
||||
|
||||
@@ -485,27 +484,27 @@ namespace Robust.Client.WebView.Cef
|
||||
public void FocusEntered()
|
||||
{
|
||||
if (_textInputActive)
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
_clyde.TextInputStart();
|
||||
}
|
||||
|
||||
public void FocusExited()
|
||||
{
|
||||
if (_textInputActive)
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
_clyde.TextInputStop();
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
_textInputActive = true;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
_clyde.TextInputStart();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
_textInputActive = false;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
_clyde.TextInputStop();
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
@@ -588,11 +587,8 @@ namespace Robust.Client.WebView.Cef
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAcceleratedPaint(
|
||||
CefBrowser browser,
|
||||
CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects,
|
||||
in CefAcceleratedPaintInfo info)
|
||||
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects, IntPtr sharedHandle)
|
||||
{
|
||||
// Unused, but we're forced to implement it so.. NOOP.
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -25,7 +24,6 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameControllerInternal _gameController = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
@@ -63,10 +61,7 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
|
||||
@@ -81,13 +81,11 @@ namespace Robust.Client.WebView.Headless
|
||||
|
||||
private sealed class WebViewControlImplDummy : DummyBase, IWebViewControlImpl
|
||||
{
|
||||
public bool IsOpen => false;
|
||||
|
||||
public void StartBrowser()
|
||||
public void EnteredTree()
|
||||
{
|
||||
}
|
||||
|
||||
public void CloseBrowser()
|
||||
public void ExitedTree()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ namespace Robust.Client.WebView
|
||||
/// </summary>
|
||||
internal interface IWebViewControlImpl : IWebViewControl
|
||||
{
|
||||
public bool IsOpen { get; }
|
||||
|
||||
void StartBrowser();
|
||||
void CloseBrowser();
|
||||
void EnteredTree();
|
||||
void ExitedTree();
|
||||
void MouseMove(GUIMouseMoveEventArgs args);
|
||||
void MouseExited();
|
||||
void MouseWheel(GUIMouseWheelEventArgs args);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
|
||||
<CETCompat>false</CETCompat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Robust.Client.WebView
|
||||
[Dependency] private readonly IWebViewManagerInternal _webViewManager = default!;
|
||||
|
||||
private readonly IWebViewControlImpl _controlImpl;
|
||||
private bool _alwaysActive;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Url
|
||||
@@ -23,21 +22,6 @@ namespace Robust.Client.WebView
|
||||
set => _controlImpl.Url = value;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool AlwaysActive
|
||||
{
|
||||
get => _alwaysActive;
|
||||
set
|
||||
{
|
||||
_alwaysActive = value;
|
||||
|
||||
if (_alwaysActive && !_controlImpl.IsOpen)
|
||||
_controlImpl.StartBrowser();
|
||||
else if (!_alwaysActive && _controlImpl.IsOpen && !IsInsideTree)
|
||||
_controlImpl.CloseBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public bool IsLoading => _controlImpl.IsLoading;
|
||||
|
||||
public WebViewControl()
|
||||
@@ -55,16 +39,14 @@ namespace Robust.Client.WebView
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
if (!_controlImpl.IsOpen)
|
||||
_controlImpl.StartBrowser();
|
||||
_controlImpl.EnteredTree();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
if (!_alwaysActive)
|
||||
_controlImpl.CloseBrowser();
|
||||
_controlImpl.ExitedTree();
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
|
||||
@@ -40,7 +40,11 @@ namespace Robust.Client.Animations
|
||||
var keyFrame = KeyFrames[keyFrameIndex];
|
||||
|
||||
var audioParams = keyFrame.AudioParamsFunc.Invoke();
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
|
||||
var audio = new SoundPathSpecifier(keyFrame.Resource)
|
||||
{
|
||||
Params = audioParams
|
||||
};
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
|
||||
}
|
||||
|
||||
return (keyFrameIndex, playingTime);
|
||||
@@ -51,7 +55,7 @@ namespace Robust.Client.Animations
|
||||
/// <summary>
|
||||
/// The RSI state to play when this keyframe gets triggered.
|
||||
/// </summary>
|
||||
public readonly ResolvedSoundSpecifier Specifier;
|
||||
public readonly string Resource;
|
||||
|
||||
/// <summary>
|
||||
/// A function that returns the audio parameter to be used.
|
||||
@@ -65,9 +69,9 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
|
||||
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
|
||||
{
|
||||
Specifier = specifier;
|
||||
Resource = resource;
|
||||
KeyTime = keyTime;
|
||||
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Maths;
|
||||
using Vector3 = Robust.Shared.Maths.Vector3;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Robust.Client.Animations
|
||||
{
|
||||
@@ -11,7 +13,7 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public abstract class AnimationTrackProperty : AnimationTrack
|
||||
{
|
||||
public List<KeyFrame> KeyFrames { get; set; } = new();
|
||||
public List<KeyFrame> KeyFrames { get; protected set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// How to interpolate values when between two keyframes.
|
||||
@@ -120,9 +122,9 @@ namespace Robust.Client.Animations
|
||||
case Vector2 vector2:
|
||||
return Vector2Helpers.InterpolateCubic((Vector2) preA, vector2, (Vector2) b, (Vector2) postB, t);
|
||||
case Vector3 vector3:
|
||||
return VectorHelpers.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
|
||||
return Vector3.InterpolateCubic((Vector3) preA, vector3, (Vector3) b, (Vector3) postB, t);
|
||||
case Vector4 vector4:
|
||||
return VectorHelpers.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
|
||||
return Vector4.InterpolateCubic((Vector4) preA, vector4, (Vector4) b, (Vector4) postB, t);
|
||||
case float f:
|
||||
return MathHelper.InterpolateCubic((float) preA, f, (float) b, (float) postB, t);
|
||||
case double d:
|
||||
|
||||
@@ -84,19 +84,6 @@ internal partial class AudioManager
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
}
|
||||
|
||||
void IAudioInternal.Remove(AudioStream stream)
|
||||
{
|
||||
if (stream.ClydeHandle == null)
|
||||
return;
|
||||
|
||||
if (!_audioSampleBuffers.Remove(stream.BufferId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.DeleteBuffer(stream.BufferId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
@@ -133,9 +120,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(this, buffer, handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -192,9 +179,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(this, buffer, handle, length, wav.NumChannels, name);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -223,8 +210,8 @@ internal partial class AudioManager
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
return new AudioStream(this, buffer, handle, length, channels, name);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
}
|
||||
|
||||
public void SetMasterGain(float newGain)
|
||||
@@ -306,7 +293,7 @@ internal partial class AudioManager
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[stream.BufferId].BufferHandle);
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
|
||||
@@ -315,7 +302,7 @@ internal partial class AudioManager
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IBufferedAudioSource? IAudioInternal.CreateBufferedAudioSource(int buffers, bool floatAudio)
|
||||
IBufferedAudioSource? IAudioInternal.CreateBufferedAudioSource(int buffers, bool floatAudio=false)
|
||||
{
|
||||
var source = AL.GenSource();
|
||||
|
||||
@@ -383,12 +370,5 @@ internal partial class AudioManager
|
||||
}
|
||||
|
||||
_bufferedAudioSources.Clear();
|
||||
|
||||
foreach (var buffer in _audioSampleBuffers.Values)
|
||||
{
|
||||
DeleteAudioBufferOnMainThread(buffer.BufferHandle);
|
||||
}
|
||||
|
||||
_audioSampleBuffers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -18,15 +17,13 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Shared.IoC.Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Shared.IoC.Dependency] private readonly IResourceCache _cache = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly Dictionary<int, LoadedAudioSample> _audioSampleBuffers = new();
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
|
||||
new();
|
||||
@@ -57,8 +54,8 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
_checkAlError();
|
||||
|
||||
// Load up AL context extensions.
|
||||
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
|
||||
foreach (var extension in s.Split(' '))
|
||||
{
|
||||
_alContextExtensions.Add(extension);
|
||||
}
|
||||
@@ -119,22 +116,6 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
|
||||
|
||||
_reload.Register("/Audio", "*.ogg");
|
||||
_reload.Register("/Audio", "*.wav");
|
||||
|
||||
_reload.OnChanged += OnReload;
|
||||
}
|
||||
|
||||
private void OnReload(ResPath args)
|
||||
{
|
||||
if (args.Extension != "ogg" &&
|
||||
args.Extension != "wav")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.ReloadResource<AudioResource>(args);
|
||||
}
|
||||
|
||||
internal bool IsMainThread()
|
||||
@@ -159,19 +140,15 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
}
|
||||
}
|
||||
|
||||
internal void LogError(string message)
|
||||
{
|
||||
OpenALSawmill.Error(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like _checkAlError but allows custom data to be passed in as relevant.
|
||||
/// </summary>
|
||||
internal void LogALError(ALErrorInterpolatedStringHandler message, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
|
||||
internal void LogALError(string message, [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = -1)
|
||||
{
|
||||
if (message.Error != ALError.NoError)
|
||||
var error = AL.GetError();
|
||||
if (error != ALError.NoError)
|
||||
{
|
||||
OpenALSawmill.Error("[{0}:{1}] AL error: {2}, {3}. Stacktrace is {4}", callerMember, callerLineNumber, message.Error, message.ToStringAndClear(), Environment.StackTrace);
|
||||
OpenALSawmill.Error("[{0}:{1}] AL error: {2}, {3}. Stacktrace is {4}", callerMember, callerLineNumber, error, message, Environment.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,32 +170,4 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
BufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
|
||||
[InterpolatedStringHandler]
|
||||
internal ref struct ALErrorInterpolatedStringHandler
|
||||
{
|
||||
private DefaultInterpolatedStringHandler _handler;
|
||||
public ALError Error;
|
||||
|
||||
public ALErrorInterpolatedStringHandler(int literalLength, int formattedCount, out bool shouldAppend)
|
||||
{
|
||||
Error = AL.GetError();
|
||||
if (Error == ALError.NoError)
|
||||
{
|
||||
shouldAppend = false;
|
||||
_handler = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldAppend = true;
|
||||
_handler = new DefaultInterpolatedStringHandler(literalLength, formattedCount);
|
||||
}
|
||||
}
|
||||
|
||||
public string ToStringAndClear() => _handler.ToStringAndClear();
|
||||
public override string ToString() => _handler.ToString();
|
||||
public void AppendLiteral(string value) => _handler.AppendLiteral(value);
|
||||
public void AppendFormatted<T>(T value) => _handler.AppendFormatted(value);
|
||||
public void AppendFormatted<T>(T value, string? format) => _handler.AppendFormatted(value, format);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class AudioOverlay : Overlay
|
||||
|
||||
var screenHandle = args.ScreenHandle;
|
||||
var output = new StringBuilder();
|
||||
var listenerPos = _transform.GetMapCoordinates(_entManager.GetComponent<TransformComponent>(localPlayer.Value));
|
||||
var listenerPos = _entManager.GetComponent<TransformComponent>(localPlayer.Value).MapPosition;
|
||||
|
||||
if (listenerPos.MapId != args.MapId)
|
||||
return;
|
||||
|
||||
@@ -6,15 +6,8 @@ namespace Robust.Client.Audio;
|
||||
/// <summary>
|
||||
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
|
||||
/// </summary>
|
||||
public sealed class AudioStream : IDisposable
|
||||
public sealed class AudioStream
|
||||
{
|
||||
private IAudioInternal _audio;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer ID for this audio in AL.
|
||||
/// </summary>
|
||||
internal int BufferId { get; }
|
||||
|
||||
public TimeSpan Length { get; }
|
||||
internal IClydeHandle? ClydeHandle { get; }
|
||||
public string? Name { get; }
|
||||
@@ -22,10 +15,8 @@ public sealed class AudioStream : IDisposable
|
||||
public string? Artist { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
internal AudioStream(IAudioInternal internalAudio, int bufferId, IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
{
|
||||
_audio = internalAudio;
|
||||
BufferId = bufferId;
|
||||
ClydeHandle = handle;
|
||||
Length = length;
|
||||
ChannelCount = channelCount;
|
||||
@@ -33,9 +24,4 @@ public sealed class AudioStream : IDisposable
|
||||
Title = title;
|
||||
Artist = artist;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_audio.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
@@ -38,6 +37,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly IAudioInternal _audio = default!;
|
||||
@@ -49,16 +49,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// Per-tick cache of relevant streams.
|
||||
/// </summary>
|
||||
private readonly List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> _streams = new();
|
||||
private EntityUid? _listenerGrid;
|
||||
private UpdateAudioJob _updateAudioJob;
|
||||
|
||||
private float _audioFrameTime;
|
||||
private float _audioFrameTimeRemaining;
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
private float _zOffset;
|
||||
private float _audioEndBuffer;
|
||||
|
||||
public override float ZOffset
|
||||
{
|
||||
@@ -82,6 +79,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
}
|
||||
|
||||
private float _zOffset;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -109,31 +108,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
|
||||
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
|
||||
|
||||
Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioTickRate, OnAudioTickRate, true);
|
||||
InitializeLimit();
|
||||
}
|
||||
|
||||
private void OnAudioBuffer(float value)
|
||||
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
_audioEndBuffer = value;
|
||||
}
|
||||
|
||||
private void OnAudioTickRate(int obj)
|
||||
{
|
||||
_audioFrameTime = 1f / obj;
|
||||
_audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime);
|
||||
}
|
||||
|
||||
private void OnAudioState(Entity<AudioComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
|
||||
if (component.LifeStage < ComponentLifeStage.Initialized)
|
||||
return;
|
||||
|
||||
ApplyAudioParams(component.Params, component);
|
||||
component.Source.Global = component.Global;
|
||||
|
||||
@@ -157,29 +138,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
case AudioState.Stopped:
|
||||
component.StopPlaying();
|
||||
component.PlaybackPosition = 0f;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
// If playback position changed then update it.
|
||||
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
|
||||
var currentPosition = entity.Comp.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
|
||||
// Don't try to set the audio too far ahead.
|
||||
if (!string.IsNullOrEmpty(entity.Comp.FileName))
|
||||
if (!string.IsNullOrEmpty(component.FileName))
|
||||
{
|
||||
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
|
||||
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
var currentPosition = component.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
|
||||
if (diff > 0.1f)
|
||||
{
|
||||
entity.Comp.StopPlaying();
|
||||
return;
|
||||
component.PlaybackPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
// If the difference is minor then we'll just keep playing it.
|
||||
if (diff > 0.1f)
|
||||
{
|
||||
entity.Comp.PlaybackPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -227,10 +200,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
|
||||
if (TryAudioLimit(component.FileName))
|
||||
{
|
||||
@@ -254,17 +223,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
// Don't play until first frame so occlusion etc. are correct.
|
||||
component.Gain = 0f;
|
||||
|
||||
// If the offset < buffer than just play it from the start.
|
||||
if (offset < AudioDespawnBuffer)
|
||||
{
|
||||
offset = 0;
|
||||
}
|
||||
// Not enough audio to play
|
||||
else if (offset > length.Value.TotalSeconds - _audioEndBuffer)
|
||||
{
|
||||
component.StopPlaying();
|
||||
return;
|
||||
}
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
|
||||
|
||||
if (offset > 0)
|
||||
{
|
||||
@@ -292,13 +254,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
_audioFrameTimeRemaining -= frameTime;
|
||||
|
||||
if (_audioFrameTimeRemaining > 0f)
|
||||
return;
|
||||
|
||||
// Clamp to 0 in case we have a really long frame.
|
||||
_audioFrameTimeRemaining = MathF.Max(0f, _audioFrameTime + _audioFrameTimeRemaining);
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var localEntity = _playerManager.LocalEntity;
|
||||
Vector2 listenerVelocity;
|
||||
@@ -322,6 +277,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
_streams.Add((uid, comp, xform));
|
||||
}
|
||||
|
||||
_mapManager.TryFindGridAt(ourPos, out var gridUid, out _);
|
||||
_listenerGrid = gridUid == EntityUid.Invalid ? null : gridUid;
|
||||
|
||||
try
|
||||
{
|
||||
_updateAudioJob.OurPosition = ourPos;
|
||||
@@ -374,6 +332,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
Vector2 worldPos;
|
||||
component.Volume = component.Params.Volume;
|
||||
Vector2 delta;
|
||||
|
||||
// Handle grid audio differently by using grid position.
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
@@ -387,7 +346,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
// Max distance check
|
||||
var delta = worldPos - listener.Position;
|
||||
delta = worldPos - listener.Position;
|
||||
var distance = delta.Length();
|
||||
|
||||
// Out of range so just clip it for us.
|
||||
@@ -446,16 +405,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return occlusion;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
var filename = GetAudioPath(specifier);
|
||||
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
|
||||
return true;
|
||||
|
||||
Log.Error($"Server tried to play audio file {filename} which does not exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
|
||||
{
|
||||
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
|
||||
@@ -474,15 +423,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
|
||||
AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(specifier, Filter.Local(), coordinates, true, audioParams);
|
||||
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
|
||||
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -494,17 +443,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayLocal(
|
||||
SoundSpecifier? sound,
|
||||
EntityUid source,
|
||||
EntityUid? soundInitiator,
|
||||
AudioParams? audioParams = null
|
||||
)
|
||||
{
|
||||
return PlayPredicted(sound, source, soundInitiator, audioParams);
|
||||
}
|
||||
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
|
||||
{
|
||||
if (Timing.IsFirstTimePredicted && sound != null)
|
||||
@@ -518,21 +456,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (specifier is null)
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
|
||||
{
|
||||
Specifier = specifier,
|
||||
FileName = filename,
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
|
||||
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -540,12 +478,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
|
||||
{
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
|
||||
component.Global = true;
|
||||
component.Source.Global = true;
|
||||
DirtyField(entity, component, nameof(AudioComponent.Global));
|
||||
Dirty(entity, component);
|
||||
return (entity, component);
|
||||
}
|
||||
|
||||
@@ -554,22 +492,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// </summary>
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (specifier is null)
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
|
||||
{
|
||||
Specifier = specifier,
|
||||
FileName = filename,
|
||||
NetEntity = GetNetEntity(entity),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
|
||||
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -578,15 +516,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="entity">The entity "emitting" the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(entity))
|
||||
{
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, entity);
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
|
||||
|
||||
return playing;
|
||||
@@ -598,22 +536,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
|
||||
{
|
||||
if (specifier is null)
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return null;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
|
||||
{
|
||||
Specifier = specifier,
|
||||
FileName = filename,
|
||||
Coordinates = GetNetCoordinates(coordinates),
|
||||
AudioParams = audioParams ?? AudioParams.Default
|
||||
});
|
||||
}
|
||||
|
||||
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
|
||||
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -622,41 +560,41 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
/// <param name="stream">The audio stream to play.</param>
|
||||
/// <param name="coordinates">The coordinates at which to play the audio.</param>
|
||||
/// <param name="audioParams"></param>
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
|
||||
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
if (TerminatingOrDeleted(coordinates.EntityId))
|
||||
{
|
||||
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
var playing = CreateAndStartPlayingStream(audioParams, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, coordinates);
|
||||
return playing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(specifier, audioParams);
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(specifier, entity, audioParams);
|
||||
return PlayEntity(filename, entity, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(specifier, coordinates, audioParams);
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(specifier, audioParams);
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
|
||||
@@ -670,39 +608,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayGlobal(specifier, audioParams);
|
||||
return PlayGlobal(filename, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(specifier, uid, audioParams);
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayEntity(specifier, uid, audioParams);
|
||||
return PlayEntity(filename, uid, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(specifier, coordinates, audioParams);
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
|
||||
{
|
||||
return PlayStatic(specifier, coordinates, audioParams);
|
||||
return PlayStatic(filename, coordinates, audioParams);
|
||||
}
|
||||
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
|
||||
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
|
||||
{
|
||||
var audioP = audioParams ?? AudioParams.Default;
|
||||
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
|
||||
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
|
||||
LoadStream(entity, stream);
|
||||
EntityManager.InitializeAndStartEntity(entity);
|
||||
var comp = entity.Comp;
|
||||
@@ -710,8 +648,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
// TODO clamp the offset inside of SetPlaybackPosition() itself.
|
||||
var offset = audioP.PlayOffsetSeconds;
|
||||
var maxOffset = Math.Max((float) stream.Length.TotalSeconds - 0.01f, 0f);
|
||||
offset = Math.Clamp(offset, 0f, maxOffset);
|
||||
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
|
||||
source.PlaybackPosition = offset;
|
||||
|
||||
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
|
||||
@@ -735,17 +672,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
|
||||
{
|
||||
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
|
||||
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnEntityAudio(PlayAudioEntityMessage ev)
|
||||
{
|
||||
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
|
||||
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
|
||||
}
|
||||
|
||||
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
|
||||
{
|
||||
PlayGlobal(ev.Specifier, ev.AudioParams, false);
|
||||
PlayGlobal(ev.FileName, ev.AudioParams, false);
|
||||
}
|
||||
|
||||
protected override TimeSpan GetAudioLengthImpl(string filename)
|
||||
@@ -753,12 +690,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
|
||||
}
|
||||
|
||||
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
|
||||
{
|
||||
var soundInfo = specifier?.ToString() ?? "unknown sound";
|
||||
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct UpdateAudioJob : IParallelRobustJob
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Effects;
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace Robust.Client.Audio;
|
||||
/// </summary>
|
||||
internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
private int _audioBuffer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InitializePostWindowing()
|
||||
{
|
||||
@@ -67,11 +65,6 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove(AudioStream stream)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopAllAudio()
|
||||
{
|
||||
@@ -108,11 +101,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
return new AudioStream(this, _audioBuffer++, null, length, channels, name);
|
||||
return new AudioStream(null, length, channels, name);
|
||||
}
|
||||
|
||||
private AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
{
|
||||
return new AudioStream(this, _audioBuffer++, null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ internal interface IAudioInternal : IAudioManager
|
||||
|
||||
void SetAttenuation(Attenuation attenuation);
|
||||
|
||||
void Remove(AudioStream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all audio from playing.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
@@ -10,7 +11,7 @@ namespace Robust.Client.Audio;
|
||||
public interface IAudioManager
|
||||
{
|
||||
IAudioSource? CreateAudioSource(AudioStream stream);
|
||||
|
||||
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
|
||||
@@ -6,7 +6,6 @@ using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
@@ -34,6 +33,12 @@ public interface IMidiRenderer : IDisposable
|
||||
/// </summary>
|
||||
bool LoopMidi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This increases all note on velocities to 127.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
|
||||
bool VolumeBoost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The midi program (instrument) the renderer is using.
|
||||
/// </summary>
|
||||
@@ -157,13 +162,8 @@ public interface IMidiRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// Loads a new soundfont into the renderer.
|
||||
/// </summary>
|
||||
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
|
||||
void LoadSoundfont(string filename, bool resetPresets = false);
|
||||
|
||||
void LoadSoundfontResource(ResPath path, bool resetPresets = false);
|
||||
|
||||
void LoadSoundfontUser(ResPath path, bool resetPresets = false);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked whenever a new midi event is registered.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using NFluidsynth;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiManager
|
||||
{
|
||||
// For loading sound fonts, we have to use a callback model where we can only parse a string.
|
||||
// This API, frankly, fucking sucks.
|
||||
//
|
||||
// These prefixes are used to separate the various places a file *can* be loaded from.
|
||||
//
|
||||
// We cannot prevent Fluidsynth from trying to load prefixed paths itself if they are invalid
|
||||
// So if content specifies "/foobar.sf2" to be loaded and it doesn't exist,
|
||||
// Fluidsynth *will* try to fopen("RES:/foobar.sf2"). For this reason I'm putting in some nonsense characters
|
||||
// that will pass through Fluidsynth fine, but make sure the filename is *never* a practically valid OS path.
|
||||
//
|
||||
// NOTE: Raw disk paths *cannot* be prefixed as Fluidsynth needs to load those itself.
|
||||
// Specifically, their .dls loader doesn't respect file callbacks.
|
||||
// If you're curious why this is: it's two-fold:
|
||||
// * The Fluidsynth C code for the .dls loader just doesn't use the file callbacks, period.
|
||||
// * Even if it did, we're not specifying those file callbacks, as they're per loader,
|
||||
// and we're only adding a *new* sound font loader with file callbacks, not modifying the existing ones.
|
||||
// The loader for .sfX format and .dls format are different loader objects in Fluidsynth.
|
||||
internal const string PrefixCommon = "!/ -?\x0001";
|
||||
internal const string PrefixLegacy = PrefixCommon + "LEGACY";
|
||||
internal const string PrefixUser = PrefixCommon + "USER";
|
||||
internal const string PrefixResources = PrefixCommon + "RES";
|
||||
|
||||
private void LoadSoundFontSetup(MidiRenderer renderer)
|
||||
{
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfontResource(FallbackSoundfont);
|
||||
|
||||
// Load system-specific soundfonts.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
|
||||
renderer.LoadSoundfontDisk(filepath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
|
||||
renderer.LoadSoundfontDisk(OsxSoundfont);
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
|
||||
renderer.LoadSoundfontDisk(WindowsSoundfont);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe load soundfont specified in environment variable.
|
||||
// Load it here so it can override system soundfonts but not content or user data soundfonts.
|
||||
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is { } soundfontOverride)
|
||||
{
|
||||
// Just to avoid funny shit: avoid people smuggling a prefix in here.
|
||||
// I wish I could separate this properly...
|
||||
var (prefix, _) = SplitPrefix(soundfontOverride);
|
||||
if (IsValidPrefix(prefix))
|
||||
{
|
||||
_midiSawmill.Error($"Not respecting {SoundfontEnvironmentVariable} env variable: invalid file path");
|
||||
}
|
||||
else if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
|
||||
renderer.LoadSoundfontDisk(soundfontOverride);
|
||||
}
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading content soundfont {file}");
|
||||
renderer.LoadSoundfontResource(file);
|
||||
}
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from user data directory {CustomSoundfontDirectory}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
|
||||
foreach (var file in enumerator)
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading user soundfont {file}");
|
||||
renderer.LoadSoundfontUser(file);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string PrefixPath(string prefix, string value)
|
||||
{
|
||||
return $"{prefix}:{value}";
|
||||
}
|
||||
|
||||
internal static (string prefix, string? value) SplitPrefix(string filename)
|
||||
{
|
||||
var filenameSplit = filename.Split(':', 2);
|
||||
if (filenameSplit.Length == 1)
|
||||
return (filenameSplit[0], null);
|
||||
|
||||
return (filenameSplit[0], filenameSplit[1]);
|
||||
}
|
||||
|
||||
internal static bool IsValidPrefix(string prefix)
|
||||
{
|
||||
return prefix is PrefixLegacy or PrefixUser or PrefixResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
{
|
||||
private readonly MidiManager _parent;
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public ResourceLoaderCallbacks(MidiManager parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream stream;
|
||||
try
|
||||
{
|
||||
stream = OpenCore(filename);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_parent._midiSawmill.Error($"Error while opening sound font: {e}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
private Stream OpenCore(string filename)
|
||||
{
|
||||
var (prefix, value) = SplitPrefix(filename);
|
||||
|
||||
if (!IsValidPrefix(prefix) || value == null)
|
||||
return File.OpenRead(filename);
|
||||
|
||||
var resourceCache = _parent._resourceManager;
|
||||
var resourcePath = new ResPath(value);
|
||||
|
||||
switch (prefix)
|
||||
{
|
||||
case PrefixUser:
|
||||
return resourceCache.UserData.OpenRead(resourcePath);
|
||||
case PrefixResources:
|
||||
return resourceCache.ContentFileRead(resourcePath);
|
||||
case PrefixLegacy:
|
||||
// Try resources first, then try user data.
|
||||
if (resourceCache.TryContentFileRead(resourcePath, out var stream))
|
||||
return stream;
|
||||
|
||||
return resourceCache.UserData.OpenRead(resourcePath);
|
||||
default:
|
||||
throw new UnreachableException("Invalid prefix specified!");
|
||||
}
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
if (count < 1024)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override long Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return (long) stream.Position;
|
||||
}
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
private const string OsxSoundfont =
|
||||
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
|
||||
|
||||
private static readonly ResPath FallbackSoundfont = new ResPath("/Midi/fallback.sf2");
|
||||
private const string FallbackSoundfont = "/Midi/fallback.sf2";
|
||||
|
||||
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
|
||||
|
||||
@@ -265,7 +265,81 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
|
||||
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
|
||||
|
||||
LoadSoundFontSetup(renderer);
|
||||
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
|
||||
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
|
||||
renderer.LoadSoundfont(FallbackSoundfont);
|
||||
|
||||
// Load system-specific soundfonts.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
foreach (var filepath in LinuxSoundfonts)
|
||||
{
|
||||
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
|
||||
renderer.LoadSoundfont(filepath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
|
||||
renderer.LoadSoundfont(OsxSoundfont);
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
|
||||
renderer.LoadSoundfont(WindowsSoundfont);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe load soundfont specified in environment variable.
|
||||
// Load it here so it can override system soundfonts but not content or user data soundfonts.
|
||||
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is {} soundfontOverride)
|
||||
{
|
||||
if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
|
||||
{
|
||||
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
|
||||
renderer.LoadSoundfont(soundfontOverride);
|
||||
}
|
||||
}
|
||||
|
||||
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
|
||||
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading content soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
var userDataPath = _resourceManager.UserData.RootDir == null
|
||||
? CustomSoundfontDirectory
|
||||
: new ResPath(_resourceManager.UserData.RootDir) / CustomSoundfontDirectory.ToRelativePath();
|
||||
|
||||
// Load every soundfont from the user data directory last, since those may override any other soundfont.
|
||||
_midiSawmill.Debug($"Loading soundfonts from user data directory {userDataPath}");
|
||||
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
|
||||
foreach (var file in enumerator)
|
||||
{
|
||||
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
|
||||
_midiSawmill.Debug($"Loading user soundfont {file}");
|
||||
renderer.LoadSoundfont(file.ToString());
|
||||
}
|
||||
|
||||
renderer.Source.Gain = _gain;
|
||||
|
||||
@@ -498,6 +572,130 @@ internal sealed partial class MidiManager : IMidiManager
|
||||
midiEvent.Velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to load soundfonts.
|
||||
/// </summary>
|
||||
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
|
||||
{
|
||||
private readonly MidiManager _parent;
|
||||
private readonly Dictionary<int, Stream> _openStreams = new();
|
||||
private int _nextStreamId = 1;
|
||||
|
||||
public ResourceLoaderCallbacks(MidiManager parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public override IntPtr Open(string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
Stream? stream;
|
||||
var resourceCache = _parent._resourceManager;
|
||||
var resourcePath = new ResPath(filename);
|
||||
|
||||
if (resourcePath.IsRooted)
|
||||
{
|
||||
// is it in content?
|
||||
if (resourceCache.ContentFileExists(filename))
|
||||
{
|
||||
if (!resourceCache.TryContentFileRead(filename, out stream))
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
// is it in userdata?
|
||||
else if (resourceCache.UserData.Exists(resourcePath))
|
||||
{
|
||||
stream = resourceCache.UserData.OpenRead(resourcePath);
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
else if (File.Exists(filename))
|
||||
{
|
||||
stream = File.OpenRead(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var id = _nextStreamId++;
|
||||
|
||||
_openStreams.Add(id, stream);
|
||||
|
||||
return (IntPtr) id;
|
||||
}
|
||||
|
||||
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
|
||||
{
|
||||
var length = (int) count;
|
||||
var span = new Span<byte>(buf.ToPointer(), length);
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
|
||||
try
|
||||
{
|
||||
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
|
||||
if (count < 1024)
|
||||
{
|
||||
// ReSharper disable once SuggestVarOrType_Elsewhere
|
||||
Span<byte> buffer = stackalloc byte[(int)count];
|
||||
|
||||
stream.ReadExact(buffer);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = stream.ReadExact(length);
|
||||
|
||||
buffer.CopyTo(span);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
stream.Seek(offset, origin);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override long Tell(IntPtr sfHandle)
|
||||
{
|
||||
var stream = _openStreams[(int) sfHandle];
|
||||
|
||||
return (long) stream.Position;
|
||||
}
|
||||
|
||||
public override int Close(IntPtr sfHandle)
|
||||
{
|
||||
if (!_openStreams.Remove((int) sfHandle, out var stream))
|
||||
return -1;
|
||||
|
||||
stream.Dispose();
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region Jobs
|
||||
|
||||
private record struct MidiUpdateJob : IParallelRobustJob
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiRenderer
|
||||
{
|
||||
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
|
||||
public void LoadSoundfont(string filename, bool resetPresets = true)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixLegacy, filename),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
public void LoadSoundfontResource(ResPath path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixResources, path.ToString()),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
public void LoadSoundfontUser(ResPath path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
MidiManager.PrefixPath(MidiManager.PrefixUser, path.ToString()),
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
internal void LoadSoundfontDisk(string path, bool resetPresets = false)
|
||||
{
|
||||
LoadSoundfontCore(
|
||||
path,
|
||||
resetPresets);
|
||||
}
|
||||
|
||||
private void LoadSoundfontCore(string filenameString, bool resetPresets)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_synth.LoadSoundFont(filenameString, resetPresets);
|
||||
MidiSoundfont = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Audio.Midi;
|
||||
|
||||
internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
internal sealed class MidiRenderer : IMidiRenderer
|
||||
{
|
||||
private readonly IMidiManager _midiManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
@@ -205,6 +205,14 @@ internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
|
||||
public bool VolumeBoost
|
||||
{
|
||||
get => VelocityOverride == 127;
|
||||
set => VelocityOverride = value ? 127 : null;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityUid? TrackingEntity { get; set; } = null;
|
||||
|
||||
@@ -226,9 +234,6 @@ internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
if (value == _master)
|
||||
return;
|
||||
|
||||
if (CheckMasterCycle(value))
|
||||
throw new InvalidOperationException("Tried to set master to a child of this renderer!");
|
||||
|
||||
if (_master is { Disposed: false })
|
||||
{
|
||||
try
|
||||
@@ -435,6 +440,15 @@ internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
|
||||
}
|
||||
|
||||
public void LoadSoundfont(string filename, bool resetPresets = true)
|
||||
{
|
||||
lock (_playerStateLock)
|
||||
{
|
||||
_synth.LoadSoundFont(filename, resetPresets);
|
||||
MidiSoundfont = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void IMidiRenderer.Render()
|
||||
{
|
||||
Render();
|
||||
@@ -575,28 +589,18 @@ internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
case RobustMidiCommand.NoteOff:
|
||||
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
|
||||
_synth.NoteOff(midiEvent.Channel, midiEvent.Key);
|
||||
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.NoteOn:
|
||||
// Velocity 0 *can* represent a NoteOff event.
|
||||
var velocity = midiEvent.Velocity;
|
||||
if (velocity == 0)
|
||||
{
|
||||
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = 0;
|
||||
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (FilteredChannels[midiEvent.Channel])
|
||||
break;
|
||||
|
||||
velocity = VelocityOverride ?? midiEvent.Velocity;
|
||||
var velocity = VelocityOverride ?? midiEvent.Velocity;
|
||||
|
||||
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = velocity;
|
||||
_synth.NoteOn(midiEvent.Channel, midiEvent.Key, velocity);
|
||||
|
||||
break;
|
||||
|
||||
case RobustMidiCommand.AfterTouch:
|
||||
_rendererState.NoteVelocities.AsSpan[midiEvent.Channel].AsSpan[midiEvent.Key] = midiEvent.Value;
|
||||
_synth.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
|
||||
@@ -733,22 +737,4 @@ internal sealed partial class MidiRenderer : IMidiRenderer
|
||||
_synth?.Dispose();
|
||||
_player?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that a given renderer is not already a child of this renderer, i.e. it would introduce a cycle if set as master of this renderer.
|
||||
/// </summary>
|
||||
private bool CheckMasterCycle(IMidiRenderer? otherRenderer)
|
||||
{
|
||||
// Doesn't inside drift, cringe.
|
||||
|
||||
while (otherRenderer != null)
|
||||
{
|
||||
if (otherRenderer == this)
|
||||
return true;
|
||||
|
||||
otherRenderer = otherRenderer.Master;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user