mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
222 Commits
v230.1.0-p
...
v239.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e70f68aa73 | ||
|
|
f2aa5e8bb5 | ||
|
|
bcc4cd77cf | ||
|
|
941cb4c1d6 | ||
|
|
7b58760331 | ||
|
|
f0306b593a | ||
|
|
5e1935c310 | ||
|
|
67c44a5fc5 | ||
|
|
3ea0a0244b | ||
|
|
325a39ee4b | ||
|
|
e4190f4f29 | ||
|
|
4d163ed818 | ||
|
|
09c6a816e0 | ||
|
|
dfc4894c8b | ||
|
|
f2b096f145 | ||
|
|
1984e97d2f | ||
|
|
57291b88c0 | ||
|
|
f40dd51648 | ||
|
|
d08fdd3a18 | ||
|
|
80dbf02af4 | ||
|
|
a2983a5ee0 | ||
|
|
7810cd0c2e | ||
|
|
6c8b863731 | ||
|
|
c2ca7c7811 | ||
|
|
347d240fae | ||
|
|
87a5745519 | ||
|
|
e47ba0faea | ||
|
|
fb705702fb | ||
|
|
a2aec44ebb | ||
|
|
5e97db435c | ||
|
|
9af119f57a | ||
|
|
6247be2c84 | ||
|
|
acb1d37b99 | ||
|
|
82c94fc8b0 | ||
|
|
9837c33de7 | ||
|
|
ac30ad1820 | ||
|
|
9c30fdf5fd | ||
|
|
0b7e8c2560 | ||
|
|
7982aa236c | ||
|
|
0559339143 | ||
|
|
89fcd1dd2b | ||
|
|
649378e59a | ||
|
|
0c7ace16d1 | ||
|
|
27f7f5ee36 | ||
|
|
fe0fcbd851 | ||
|
|
aca7847933 | ||
|
|
1621d25a92 | ||
|
|
b7e0a9bc03 | ||
|
|
9909416006 | ||
|
|
c3e487b61c | ||
|
|
89ad8b6c9f | ||
|
|
efbc9ef2bf | ||
|
|
ce240773e8 | ||
|
|
8563466011 | ||
|
|
af4d53fb54 | ||
|
|
3086fc446c | ||
|
|
5f3a54376d | ||
|
|
9bb7af364e | ||
|
|
92b0e7f1a8 | ||
|
|
47d1c372b2 | ||
|
|
453f763128 | ||
|
|
65a7942d63 | ||
|
|
f1f3c60d1f | ||
|
|
d4e8a27c23 | ||
|
|
18a17da8fa | ||
|
|
90a8c66e96 | ||
|
|
45c14b2bc3 | ||
|
|
d227613997 | ||
|
|
7557cc703c | ||
|
|
7b81d0d881 | ||
|
|
b59f7801ac | ||
|
|
d724c5b3eb | ||
|
|
f812dc4dac | ||
|
|
2a1bcb6f1e | ||
|
|
fa9030e59c | ||
|
|
8dcae8631b | ||
|
|
21c3535486 | ||
|
|
4e100d96bc | ||
|
|
14d3699ae2 | ||
|
|
350fa8736d | ||
|
|
5a82df216d | ||
|
|
32bca7cfd4 | ||
|
|
008babebc6 | ||
|
|
c65c4ba57e | ||
|
|
eb5b838e61 | ||
|
|
6b43036c9d | ||
|
|
f23a55793d | ||
|
|
46143d2589 | ||
|
|
ba7d1452c1 | ||
|
|
1c1343466e | ||
|
|
0d534e8bcd | ||
|
|
c83c6f9592 | ||
|
|
c794bd84bf | ||
|
|
d1d43f834b | ||
|
|
9505cb68df | ||
|
|
9763f5fdf4 | ||
|
|
80a963ec05 | ||
|
|
3a670ec25e | ||
|
|
e45950a557 | ||
|
|
6f1427ef3c | ||
|
|
9a7d1a39c1 | ||
|
|
9be903ee56 | ||
|
|
72f9f9c343 | ||
|
|
3ad760a99e | ||
|
|
e2f3722ce9 | ||
|
|
b4beca6562 | ||
|
|
ea02260230 | ||
|
|
3b243e487d | ||
|
|
f40ccb7558 | ||
|
|
f467a7027b | ||
|
|
c9d7d442d9 | ||
|
|
342626ad9b | ||
|
|
1c3ea968e4 | ||
|
|
f0ed3537ee | ||
|
|
74e7e61a98 | ||
|
|
fb9b0ae89b | ||
|
|
dbe297b1fc | ||
|
|
b84917e8e4 | ||
|
|
abb3f65fe4 | ||
|
|
41ec2dc131 | ||
|
|
e714dcc83c | ||
|
|
46291af1be | ||
|
|
ad929c9955 | ||
|
|
c86cb0b795 | ||
|
|
8d03feb84f | ||
|
|
0fa21ee2d2 | ||
|
|
9be0f032e8 | ||
|
|
afffb33446 | ||
|
|
19a87fb67a | ||
|
|
2fda62a274 | ||
|
|
5218bf70b0 | ||
|
|
4f95c07ab3 | ||
|
|
786acae47a | ||
|
|
f81e30a031 | ||
|
|
f5c1d870f9 | ||
|
|
4949b34c88 | ||
|
|
0f60ad9018 | ||
|
|
f7287b181d | ||
|
|
45b7500d93 | ||
|
|
dbe6f65880 | ||
|
|
4faef1bfd3 | ||
|
|
48d70a09c6 | ||
|
|
f682fb9cc7 | ||
|
|
814e5bcf17 | ||
|
|
dbc4e80e61 | ||
|
|
5eb5ddd96e | ||
|
|
405ed378c0 | ||
|
|
be9db264dd | ||
|
|
2f73f6190d | ||
|
|
f3dfa1f666 | ||
|
|
b0d17e9527 | ||
|
|
4c81e68bf1 | ||
|
|
4490751001 | ||
|
|
bc8d2c154c | ||
|
|
3c83f8e62a | ||
|
|
c36919d76a | ||
|
|
70a853cdd5 | ||
|
|
fd3eb092cc | ||
|
|
c740026014 | ||
|
|
44f9262d1a | ||
|
|
df2160b151 | ||
|
|
5c7b1e6823 | ||
|
|
eaf7a6ba0f | ||
|
|
9ab4286592 | ||
|
|
3f02ef3730 | ||
|
|
2f17cbb1dc | ||
|
|
c2657812f5 | ||
|
|
f17f077849 | ||
|
|
306deddbd2 | ||
|
|
cdd8df743a | ||
|
|
e7ac5ad047 | ||
|
|
0e621a26be | ||
|
|
bbcc7cfe1f | ||
|
|
1208c25dcd | ||
|
|
38c227b692 | ||
|
|
4e73d72753 | ||
|
|
b1e1a0cd88 | ||
|
|
6e25ead588 | ||
|
|
cfae6e1f95 | ||
|
|
da56851846 | ||
|
|
69ed2c3c33 | ||
|
|
c558a0327b | ||
|
|
3bb7df3254 | ||
|
|
ab6bd19817 | ||
|
|
73ef69aa94 | ||
|
|
656835e7fa | ||
|
|
26c87b5858 | ||
|
|
be36001ab8 | ||
|
|
2002402af8 | ||
|
|
f659b2b58c | ||
|
|
b1e13f5b13 | ||
|
|
e5995d4edc | ||
|
|
6eb080a277 | ||
|
|
b0cb41e94a | ||
|
|
23a23f7c22 | ||
|
|
ec3a74d268 | ||
|
|
12b0bc4e0a | ||
|
|
903041dfd1 | ||
|
|
b96419f0b2 | ||
|
|
fe33ad2652 | ||
|
|
057a68b366 | ||
|
|
1a2c9008fe | ||
|
|
cd95929ebe | ||
|
|
6396ec472d | ||
|
|
d7aa5daf6a | ||
|
|
e3819f8245 | ||
|
|
57f133b742 | ||
|
|
04344ffe19 | ||
|
|
f2ee9a43f9 | ||
|
|
8d5ebd830a | ||
|
|
4d265b2210 | ||
|
|
e04caf7eb4 | ||
|
|
a4c54d3602 | ||
|
|
43fd6bc764 | ||
|
|
ff056552fe | ||
|
|
3014d9880e | ||
|
|
679c31199d | ||
|
|
0bc3c51707 | ||
|
|
140767c262 | ||
|
|
36a5b672e5 | ||
|
|
1eb63cb616 | ||
|
|
c14689f233 |
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@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.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,33 +2,32 @@ 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@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
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- 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,14 +11,8 @@
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
#on:
|
||||
# push:
|
||||
# branches: [ master ]
|
||||
# pull_request:
|
||||
# # The branches below must be a subset of the branches above
|
||||
# branches: [ master ]
|
||||
# schedule:
|
||||
# - cron: '30 18 * * 6'
|
||||
on:
|
||||
workflow_dispatch
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -28,50 +22,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@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.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,51 +3,50 @@
|
||||
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@v3.6.0
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.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: 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: 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 }}
|
||||
|
||||
55
.github/workflows/test-content.yml
vendored
55
.github/workflows/test-content.yml
vendored
@@ -2,40 +2,39 @@ 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@v3.6.0
|
||||
with:
|
||||
repository: space-wizards/space-station-14
|
||||
submodules: recursive
|
||||
- name: Check out content
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
repository: space-wizards/space-station-14
|
||||
submodules: recursive
|
||||
|
||||
- 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: 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: 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,66 +10,69 @@
|
||||
<ManagePackageVersionsCentrally />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<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="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.8" />
|
||||
<PackageVersion Include="Linguini.Bundle" Version="0.8.1" />
|
||||
<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.CodeFix.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.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.DotNet.RemoteExecutor" Version="8.0.0-beta.24059.4" />
|
||||
<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.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.Win32.Registry" Version="5.0.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="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="Nett" Version="0.15.0" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
|
||||
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
|
||||
<PackageVersion Include="Pidgin" Version="3.2.2" />
|
||||
<PackageVersion Include="Pidgin" Version="3.3.0" />
|
||||
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
|
||||
<PackageVersion Include="Robust.Natives.Cef" Version="120.1.9" />
|
||||
<PackageVersion Include="Robust.Natives.Cef" Version="131.3.5" />
|
||||
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
|
||||
<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="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="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.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.Windows" Version="10.0.26100.1" />
|
||||
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
|
||||
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
|
||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageVersion Include="PolySharp" Version="1.14.1" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<LangVersion>13.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>net8.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13</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>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
Submodule NetSerializer updated: 7f51deaeca...4882400f2c
416
RELEASE-NOTES.md
416
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,6 +54,420 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 239.0.2
|
||||
|
||||
|
||||
## 239.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix logging of received packets with `net.packet` logging level.
|
||||
* Downgrade `VorbisPizza` to fix audio playback for systems without AVX2 support.
|
||||
|
||||
### Other
|
||||
|
||||
* Improved performance of some Roslyn analyzers and source generators, which should significantly improve compile times and IDE performance.
|
||||
|
||||
|
||||
## 239.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Robust now uses **.NET 9**.
|
||||
* `ISerializationManager` will now log errors if it encounters `Entity<T>` data-fields.
|
||||
* To be clear, this has never been supported and is not really a breaking change, but this will likely require manual intervention to prevent tests from failing.
|
||||
* `IClyde.TextInputSetRect`, `TextInputStart` and `TextInputStop` have been moved to be on `IClydeWindow`.
|
||||
* Updated various NuGet dependencies and removed some other ones, of note:
|
||||
* `FastAccessors`, which is a transitive dep we never used, is now gone. It might have snuck into some `using` statement thanks to your IDE, and those will now fail to compile. Remove them.
|
||||
* NUnit `Is.EqualTo(default)` seems to have ambiguous overload resolution in some cases now, this can be fixed by using an explicit `default(type)` syntax.
|
||||
* This also fixed various false-positive warnings reported by NuGet.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `MockInterfaces.MakeConfigurationManager` for creating functional configuration managers for unit test mocking.
|
||||
* Added `ISawmill.IsLogLevelEnabled()` to avoid doing expensive verbose logging operations when not necessary.
|
||||
* ``string[] Split(System.ReadOnlySpan`1<char>)`` is now available in sandbox.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed auto-generated component delta-states not raising `AfterAutoHandleStateEvent`
|
||||
* Fixed auto-generated component delta-states improperly implementing `IComponentDeltaState` methods. May have caused bugs in replays.
|
||||
* Fixed `Robust.Client.WebView` on the launcher via a new release.
|
||||
* Fixed an exception that could occur when saving a map that had tiles migrated by alias.
|
||||
|
||||
### Other
|
||||
|
||||
* The `loglevel` command now properly shows the "`null`" log level that resets the level to inheriting from parent. This was already supported by it, but the completions didn't list it.
|
||||
|
||||
### Internal
|
||||
|
||||
* Experimental SDL2 windowing backend has been replaced with SDL3. SDL3 backend is also more feature-complete, though it is still not in use.
|
||||
* Updated CEF used by Robust.Client.WebView to 131.3.5.
|
||||
|
||||
## 238.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed source generation for auto-networked EntityUid Dictionaries missing a semicolon
|
||||
* Fixed PlacementManager using the wrong coordinates when deleting entities in an area.
|
||||
|
||||
|
||||
## 238.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Some toolshed command syntax/parsing has changed slightly, and several toolshed related classes and interfaces have changed significantly, including ToolshedManager, type parsers, invocation contexts, and parser contexts. For more detail see the the description of PR #5455
|
||||
|
||||
|
||||
## 237.4.0
|
||||
|
||||
### New features
|
||||
|
||||
* Implement automatic field-level delta states via AutoGenerateComponentState via opt-in.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Remove redundant TransformComponentState bool.
|
||||
|
||||
|
||||
## 237.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added stack-like functions to `ValueList<T>` and added an `AddRange(ReadOnlySpan<T>)` overload.
|
||||
* Added new `AssetPassFilterDrop`.
|
||||
* Added a new RayCastSystem with the latest Box2D raycast + shapecasts implemented.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `IPrototypeManager.TryGetKindFrom()` not working for prototypes with automatically inferred kind names.
|
||||
|
||||
### Other
|
||||
|
||||
* Sandbox error reference locator now works with generic method calls.
|
||||
|
||||
|
||||
## 237.2.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `SharedEyeSystem..SetTarget()` will now also automatically remove the old target from the session's ViewSubscriptions
|
||||
|
||||
### New features
|
||||
|
||||
* `ImmutableArray<T>` can now be serialized by `RobustSerializer`.
|
||||
* `RequiresLocationAttribute`, used by `ref readonly`, is now allowed by the sandbox.
|
||||
* Added `DAT-OBJ()` localization function, for the dative case in certain languages.
|
||||
* Client builds for FreeBSD are now made.
|
||||
* Added `FormattedMessage.TrimEnd()`.
|
||||
* Added Toolshed `with` for `ProtoId<T>`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `UniqueIndex<,>.RemoveRange()` and`UniqueIndexHkm<,>.RemoveRange()` clearing the whole set instead of just removing the specified values.
|
||||
* Avoid server crashes on some weird console setups (notably Pterodactyl).
|
||||
* Avoid unhandled exceptions during server shutdown getting swallowed due logging into a disposed logger.
|
||||
* Fix sandbox definitions for `Regex` functions returning `MatchCollection`.
|
||||
* Fix minor layout bugs with `SplitContainer` and `BoxContainer`.
|
||||
|
||||
### Other
|
||||
|
||||
* Changed how multi-window rendering presents to the screen with a new CVar `display.thread_unlock_before_swap`. This is an experiment to see if it solves some synchronization issues.
|
||||
* View Variables no longer clears the window on refresh while waiting on response from server.
|
||||
* `SpinBox` buttons now have a `+` prefix for the positive ones.
|
||||
* Improve Toolshed type intersection mechanism
|
||||
|
||||
### Internal
|
||||
|
||||
* Warning cleanup.
|
||||
|
||||
## 237.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* csi's auto import-system can now handle generic types.
|
||||
* csi's reflection helpers (like `fld()`) handle private members up the inheritance chain.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `UniqueIndexHkm<,>` and, by extension, entity data storage memory leaking.
|
||||
* Fix bugs related to UIScale on `OSWindow`s.
|
||||
|
||||
|
||||
## 237.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `IClydeWindow.Size` is now settable, allowing window sizes to be changed after creation.
|
||||
|
||||
### New features
|
||||
|
||||
* The game server's `/update` endpoint now supports passing more information on why an update is available.
|
||||
* This information is accessible via `IWatchdogApi.RestartRequested`.
|
||||
* Information can be specified by passing a JSON object with a `Reason` code and `Message` field.
|
||||
* Added an "Erase" button to the tile spawn menu.
|
||||
* Added `OSWindow.Create()`, which allows OS windows to be created & initialised without immediately opening/showing them.
|
||||
|
||||
### Other
|
||||
|
||||
* Made `WatchdogApi` and some members of `IWatchdogApi` private. These symbols should never have been accessed by content.
|
||||
|
||||
|
||||
## 236.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* `RequiredMemberAttribute` and `SetsRequiredMembersAttribute` have been added to the sandbox whitelist. I.e., you can now use the `required` keyword in client/shared code.
|
||||
* Added `SwitchExpressionException` to sandbox. This type gets used if you have a `switch` expression with no default case.
|
||||
* Added `LineEdit.SelectAllOnFocus`.
|
||||
* `GameTitle`, `WindowIconSet` and `SplashLogo` are exposed in `IGameController`. These will return said information set in game options or whatever is set in `manifest.yml`.
|
||||
* `BoundUserInterface` inheritors now have access to `PlayerManager`.
|
||||
* Added `MuteSounds` bool to `BaseButton`.
|
||||
* The engine has a new future-proof HWID system.
|
||||
* The auth server now manages HWIDs. This avoids HWID impersonation attacks.
|
||||
* The auth server can return multiple HWIDs. They are accessible in `NetUserData.ModernHWIds`.
|
||||
* The auth server also returns a trust score factor, accessible as `NetUserData.Trust`.
|
||||
* HWID can be disabled client side (`ROBUST_AUTH_ALLOW_HWID` env var) or server side (`net.hwid` cvar).
|
||||
* The old HWID system is still in place. It is intended that content switches to placing new bans against the new HWIDs.
|
||||
* Old HWIDs no longer work if the connection is not authenticated.
|
||||
* `launchauth` command now recognizes `SS14_LAUNCHER_APPDATA_NAME`.
|
||||
* Added new overload to `EntityLookupSystem.GetEntitiesIntersecting`.
|
||||
* Added `Control.RemoveChild(int childIndex)`.
|
||||
* `build.entities_category_filter` allows filtering the entity spawn panel to a specific category.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `SpriteView` offset calculations when scaled.
|
||||
|
||||
### Other
|
||||
|
||||
* Sprite flicks are applied immediately when started.
|
||||
* More warning fixes.
|
||||
* If the server gets shut down before finishing startup, the reason is now logged properly.
|
||||
|
||||
|
||||
## 236.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Revert IsTouching only being set to true if the contact were laready touching in clientside physics prediction.
|
||||
* Don't touch IsTouching if both bodies are asleep for clientside physics contacts. This change and the one above should fix a lot of clientside contact issues, particularly around repeated incorrect clientside contact events.
|
||||
|
||||
### New features
|
||||
|
||||
* Added an analyzer to detect duplicate Dependency fields.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Auto-networked dictionaries now use `TryAdd()` to avoid duplicate key errors when a dictionary contains multiple unknown networked entities.
|
||||
* Fixed `ICommonSession.Ping` always returning zero instead of the ping. Note that this will still return zero for client-side code when trying to get the ping of other players.
|
||||
* Hot reload XAML files on rename to fix them potentially not being reloaded with Visual Studio.
|
||||
* Fix TabContainer click detection for non-1.0 UI scales.
|
||||
|
||||
### Other
|
||||
|
||||
* Obsolete some static localization methods.
|
||||
* Tried to improve PVS tolerance to exceptions occurring.
|
||||
|
||||
|
||||
## 235.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Several different `AudioSystem` methods were incorrectly given a `[return: NotNullIfNotNull]` attribute. Content code that uses these methods needs to be updated to perform null checks.
|
||||
* noSpawn is no longer obsolete and is now removed in lieu of the EntityCategory HideSpawnMenu.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* physics.maxlinvelocity is now a replicated cvar.
|
||||
* Fix DistanceJoint debug drawing in physics not using the local anchors.
|
||||
* Fixed filtered AudioSystem methods playing a sound for all players when given an empty filter.
|
||||
* Fixed equality checks for `MarkupNode` not properly handling attributes.
|
||||
* Fixed `MarkupNode` not having a `GetHashCode()` implementation.
|
||||
* Fixed a PVS error that could occur when trying to delete the first entity that gets created in a round.
|
||||
* Fixed the "to" and "take" toolshed commands not working as intended.
|
||||
* Rich text controls within an `OutputPanel` control will now become invisible when they are out of view.
|
||||
|
||||
### Other
|
||||
|
||||
* Improve precision for Quaternion2D constructor from angles.
|
||||
|
||||
|
||||
## 234.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* SharedAudioSystem now has PlayLocal which only runs audio locally on the client.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix AudioParams not being passed through on PlayGlobal methods.
|
||||
|
||||
|
||||
## 234.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Remove a lot of obsoleted code that has been obsoleted for a while.
|
||||
|
||||
### New features
|
||||
|
||||
* Add another GetLocalEntitiesIntersecting override.
|
||||
|
||||
### Other
|
||||
|
||||
* Mark large replays as requiring Server GC.
|
||||
* Obsolete some IResourceCache proxies.
|
||||
|
||||
|
||||
## 233.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add GetGridEntities and another GetEntitiesIntersecting overload to EntityLookupSystem.
|
||||
* `MarkupNode` is now `IEquatable<MarkupNode>`. It already supported equality checks, now it implements the interface.
|
||||
* Added `Entity<T>` overloads to the following `SharedMapSystem` methods: `GetTileRef`, `GetAnchoredEntities`, `TileIndicesFor`.
|
||||
* Added `EntityUid`-only overloads to the following `SharedTransformSystem` methods: `AnchorEntity`, `Unanchor`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed equality checks for `MarkupNode` not properly handling attributes.
|
||||
* Fixed toolshed commands failing to generate error messages when working with array types
|
||||
* Fixed `MarkupNode` not having a `GetHashCode()` implementation.
|
||||
|
||||
### Other
|
||||
|
||||
* If `EntityManager.FlushEntities()` fails to delete all entities, it will now attempt to do so a second time before throwing an exception.
|
||||
|
||||
|
||||
## 233.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix exceptions in client game state handling for grids. Now they will rely upon the networked fixture data and not try to rebuild in the grid state handler.
|
||||
|
||||
|
||||
## 233.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix IsHardCollidable component to EntityUid references.
|
||||
|
||||
|
||||
## 233.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Made EntityRenamed a broadcast event & added additional args.
|
||||
* Made test runs parallelizable.
|
||||
* Added a debug assert that other threads aren't touching entities.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix some entitylookup method transformations and add more tests.
|
||||
* Fix mousehover not updating if new controls showed up under the mouse.
|
||||
|
||||
### Internal
|
||||
|
||||
* `ClientGameStateManager` now only initialises or starts entities after their parents have already been initialized. There are also some new debug asserts to try ensure that this rule isn't broken elsewhere.
|
||||
* Engine version script now supports dashes.
|
||||
|
||||
|
||||
## 232.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Obsolete method `AppearanceComponent.TryGetData` is now access-restricted to `SharedAppearanceSystem`; use `SharedAppearanceSystem.TryGetData` instead.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `SharedAppearanceSystem.AppendData`, which appends non-existing `AppearanceData` from one `AppearanceComponent` to another.
|
||||
* Added `AppearanceComponent.AppearanceDataInit`, which can be used to set initial `AppearanceData` entries in .yaml.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix BUI interfaces not deep-copying in state handling.
|
||||
* Add Robust.Xaml.csproj to the solution to fix some XAML issues.
|
||||
|
||||
### Other
|
||||
|
||||
* Serialization will now add type tags (`!type:<T>`) for necessary `NodeData` when writing (currently only for `object` nodes).
|
||||
|
||||
### Internal
|
||||
|
||||
* Added `ObjectSerializer`, which handles serialization of the generic `object` type.
|
||||
|
||||
|
||||
## 231.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a bug where the client might not add entities to the broadphase/lookup components.
|
||||
* Fixed various toolshed commands not working, including `sort`, `sortdown` `join` (for strings), and `emplace`
|
||||
|
||||
### Other
|
||||
|
||||
* Toolshed command blocks now stop executing if previous errors were not handled / cleared.
|
||||
|
||||
|
||||
## 231.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Network `InterfaceData` on `UserInterfaceComponent`.
|
||||
* Added `System.Decimal` to sandbox.
|
||||
* Added XAML hot reloading.
|
||||
* Added API for content to write custom files into replay through `IReplayFileWriter`.
|
||||
|
||||
### Other
|
||||
|
||||
* Optimized `EntityLookup` and other physics systems.
|
||||
|
||||
### Internal
|
||||
|
||||
* Added more tests related to physics.
|
||||
|
||||
|
||||
## 231.0.1
|
||||
|
||||
### Other
|
||||
|
||||
* Add better logging to failed PVS sends.
|
||||
|
||||
|
||||
## 231.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* ViewSubscriber has been moved to shared; it doesn't actually do anything on the client but makes shared code easier.
|
||||
|
||||
### New features
|
||||
|
||||
* ContactEnumreator exists to iterate the contacts of a particular entity.
|
||||
* Add FixturesChangeComponent as a generic way to add and remove fixtures easily.
|
||||
* PointLightComponent enabling / disabling now has an attempt event if you wish to block it on content side.
|
||||
* There's an OpenScreenAt overload for screen-relative coordinates.
|
||||
* SpriteSystem has methods to get an entity's position in sprite terms.
|
||||
* EntityManager and ComponentFactory now have additional methods that interact with ComponentRegistry and ComponentRegistryEntry.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix PrototypeFlags Add not actually working.
|
||||
* Fix BUIs going BRRT opening and closing repeatedly upon prediction. The closing now gets deferred to the update loop if it's still closed at the end of prediction.
|
||||
|
||||
|
||||
## 230.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add ProcessNow for IRobustJob as a convenience method where you may not want to run a job in the background sometimes.
|
||||
* Add Vector2i helpers to all 8 neighbouring directions.
|
||||
|
||||
### Other
|
||||
|
||||
* Remove IThreadPoolWorkItem interface from IRobustJob.
|
||||
|
||||
|
||||
## 230.1.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -18,3 +18,9 @@
|
||||
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,6 +20,15 @@ zzzz-object-pronoun = { GENDER($ent) ->
|
||||
*[neuter] it
|
||||
}
|
||||
|
||||
# Used internally by the DAT-OBJ() function.
|
||||
# Not used in en-US. Created for supporting other languages.
|
||||
zzzz-dat-object = { GENDER($ent) ->
|
||||
[male] him
|
||||
[female] her
|
||||
[epicene] them
|
||||
*[neuter] it
|
||||
}
|
||||
|
||||
# Used internally by the POSS-PRONOUN() function.
|
||||
zzzz-possessive-pronoun = { GENDER($ent) ->
|
||||
[male] his
|
||||
|
||||
@@ -4,9 +4,16 @@ 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
|
||||
|
||||
## Console
|
||||
|
||||
console-line-edit-placeholder = Command Here
|
||||
|
||||
## Common Used
|
||||
|
||||
window-erase-button-text = Erase Mode
|
||||
|
||||
@@ -7,3 +7,6 @@ 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,4 +1,8 @@
|
||||
command-description-tpto =
|
||||
command-help-usage =
|
||||
Usage:
|
||||
command-help-invertible =
|
||||
The behaviour of this command can be inverted using the "not" prefix.
|
||||
command-description-tpto =
|
||||
Teleport the given entities to some target entity.
|
||||
command-description-player-list =
|
||||
Returns a list of all player sessions.
|
||||
@@ -19,7 +23,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.
|
||||
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.
|
||||
command-description-search =
|
||||
Searches through the input for the provided value.
|
||||
command-description-stopwatch =
|
||||
@@ -53,10 +57,8 @@ 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 =
|
||||
@@ -95,6 +97,8 @@ 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 =
|
||||
@@ -119,6 +123,8 @@ 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 =
|
||||
@@ -219,9 +225,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 from its input to its argument (i.e. n..m inclusive)
|
||||
Returns a number between the input (inclusive) and the argument (exclusive).
|
||||
command-description-rng-from =
|
||||
Returns a number to its input from its argument (i.e. m..n inclusive)
|
||||
Returns a number between the argument (inclusive) and the input (exclusive))
|
||||
command-description-rng-prob =
|
||||
Returns a boolean based on the input probability/chance (from 0 to 1)
|
||||
command-description-sum =
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing.Verifiers;
|
||||
using NUnit.Framework;
|
||||
using Robust.Analyzers;
|
||||
|
||||
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.AccessAnalyzer>;
|
||||
using static Microsoft.CodeAnalysis.Testing.DiagnosticResult;
|
||||
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.AccessAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -16,7 +12,7 @@ public sealed class AccessAnalyzer_Test
|
||||
{
|
||||
public Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<AccessAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<AccessAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
114
Robust.Analyzers.Tests/ByRefEventAnalyzerTest.cs
Normal file
114
Robust.Analyzers.Tests/ByRefEventAnalyzerTest.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.DataDefinitionAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DataDefinitionAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DataDefinitionAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<DataDefinitionAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class DependencyAssignAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
62
Robust.Analyzers.Tests/DuplicateDependencyAnalyzerTest.cs
Normal file
62
Robust.Analyzers.Tests/DuplicateDependencyAnalyzerTest.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
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,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class MustCallBaseAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class NoUncachedRegexAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferNonGenericVariantForAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferNonGenericVariantForAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class PreferNonGenericVariantForTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<PreferNonGenericVariantForAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<PreferNonGenericVariantForAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -14,7 +13,7 @@ public sealed class PreferOtherTypeAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<PreferOtherTypeAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<PreferOtherTypeAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -12,7 +11,7 @@ public sealed class PreferOtherTypeFixerTest
|
||||
{
|
||||
private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpCodeFixTest<PreferOtherTypeAnalyzer, PreferOtherTypeFixer, NUnitVerifier>()
|
||||
var test = new CSharpCodeFixTest<PreferOtherTypeAnalyzer, PreferOtherTypeFixer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<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\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" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -27,13 +28,17 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.NUnit"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing"/>
|
||||
<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."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
|
||||
public 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."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
|
||||
public static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
|
||||
Diagnostics.IdValueEventRaisedByRef,
|
||||
"Value event raised by-ref",
|
||||
"Tried to raise a value event '{0}' by-ref",
|
||||
@@ -54,32 +54,44 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckEventRaise(OperationAnalysisContext context)
|
||||
private static void CheckEventRaise(
|
||||
OperationAnalysisContext context,
|
||||
IReadOnlyCollection<IMethodSymbol> raiseMethods)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation operation)
|
||||
return;
|
||||
|
||||
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)
|
||||
if (!operation.TargetMethod.Name.Contains("RaiseLocalEvent"))
|
||||
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
|
||||
|
||||
126
Robust.Analyzers/DuplicateDependencyAnalyzer.cs
Normal file
126
Robust.Analyzers/DuplicateDependencyAnalyzer.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Robust.Benchmarks/Physics/BoxStackBenchmark.cs
Normal file
96
Robust.Benchmarks/Physics/BoxStackBenchmark.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
92
Robust.Benchmarks/Physics/CircleStackBenchmark.cs
Normal file
92
Robust.Benchmarks/Physics/CircleStackBenchmark.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
91
Robust.Benchmarks/Physics/PyramidBenchmark.cs
Normal file
91
Robust.Benchmarks/Physics/PyramidBenchmark.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
Robust.Benchmarks/Physics/TumblerBenchmark.cs
Normal file
105
Robust.Benchmarks/Physics/TumblerBenchmark.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
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>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 800; 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);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Tumbler()
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 5000; 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,6 +19,10 @@
|
||||
<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" />
|
||||
|
||||
@@ -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,10 +37,12 @@ namespace Robust.Build.Tasks
|
||||
var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
|
||||
BuildEngine.LogMessage(msg, MessageImportance.High);
|
||||
|
||||
var res = XamlCompiler.Compile(BuildEngine, input,
|
||||
var res = XamlAotCompiler.Compile(
|
||||
BuildEngine, input,
|
||||
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
|
||||
ProjectDirectory, OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null);
|
||||
OutputPath,
|
||||
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null
|
||||
);
|
||||
if (!res.success)
|
||||
return false;
|
||||
if (!res.writtentofile)
|
||||
@@ -65,22 +67,24 @@ 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; }
|
||||
public string ReferencesFilePath { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string ProjectDirectory { get; set; }
|
||||
|
||||
public string ProjectDirectory { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string AssemblyFile { get; set; }
|
||||
public string AssemblyFile { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string OriginalCopyPath { get; set; }
|
||||
public string? OriginalCopyPath { get; set; } = null;
|
||||
|
||||
public string OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; }
|
||||
public string? OutputPath { get; set; }
|
||||
public string UpdateBuildIndicator { get; set; } = null!;
|
||||
|
||||
public string AssemblyOriginatorKeyFile { get; set; }
|
||||
public string AssemblyOriginatorKeyFile { get; set; } = null!;
|
||||
public bool SignAssembly { get; set; }
|
||||
public bool DelaySign { get; set; }
|
||||
|
||||
@@ -95,7 +99,7 @@ namespace Robust.Build.Tasks
|
||||
return rv;
|
||||
}
|
||||
|
||||
public IBuildEngine BuildEngine { get; set; }
|
||||
public ITaskHost HostObject { get; set; }
|
||||
public IBuildEngine BuildEngine { get; set; } = null!;
|
||||
public ITaskHost HostObject { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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,9 +55,11 @@ namespace Robust.Build.Tasks
|
||||
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
|
||||
IDictionary targetOutputs) => throw new NotSupportedException();
|
||||
|
||||
public bool ContinueOnError { get; }
|
||||
public int LineNumberOfTaskNode { get; }
|
||||
public int ColumnNumberOfTaskNode { get; }
|
||||
public string ProjectFileOfTaskNode { get; }
|
||||
// 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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<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>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</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,389 +0,0 @@
|
||||
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,6 +15,9 @@ 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,6 +64,8 @@ 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
|
||||
{
|
||||
public static class Program
|
||||
internal 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,6 +44,8 @@ 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,6 +23,7 @@ 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);
|
||||
|
||||
|
||||
@@ -484,27 +484,27 @@ namespace Robust.Client.WebView.Cef
|
||||
public void FocusEntered()
|
||||
{
|
||||
if (_textInputActive)
|
||||
_clyde.TextInputStart();
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
public void FocusExited()
|
||||
{
|
||||
if (_textInputActive)
|
||||
_clyde.TextInputStop();
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
_textInputActive = true;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
_clyde.TextInputStart();
|
||||
Owner.Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
_textInputActive = false;
|
||||
if (Owner.HasKeyboardFocus())
|
||||
_clyde.TextInputStop();
|
||||
Owner.Root?.Window?.TextInputStop();
|
||||
}
|
||||
|
||||
private sealed class LiveData
|
||||
@@ -587,8 +587,11 @@ namespace Robust.Client.WebView.Cef
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects, IntPtr sharedHandle)
|
||||
protected override void OnAcceleratedPaint(
|
||||
CefBrowser browser,
|
||||
CefPaintElementType type,
|
||||
CefRectangle[] dirtyRects,
|
||||
in CefAcceleratedPaintInfo info)
|
||||
{
|
||||
// Unused, but we're forced to implement it so.. NOOP.
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -24,6 +25,7 @@ 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!;
|
||||
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
|
||||
<CETCompat>false</CETCompat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -453,6 +453,17 @@ 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)
|
||||
@@ -493,7 +504,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
|
||||
component.Global = true;
|
||||
component.Source.Global = true;
|
||||
Dirty(entity, component);
|
||||
DirtyField(entity, component, nameof(AudioComponent.Global));
|
||||
return (entity, component);
|
||||
}
|
||||
|
||||
@@ -658,7 +669,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
// TODO clamp the offset inside of SetPlaybackPosition() itself.
|
||||
var offset = audioP.PlayOffsetSeconds;
|
||||
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
|
||||
var maxOffset = Math.Max((float) stream.Length.TotalSeconds - 0.01f, 0f);
|
||||
offset = Math.Clamp(offset, 0f, maxOffset);
|
||||
source.PlaybackPosition = offset;
|
||||
|
||||
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
|
||||
|
||||
@@ -33,12 +33,6 @@ 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>
|
||||
|
||||
@@ -205,14 +205,6 @@ internal sealed 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;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.HWId;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Placement;
|
||||
@@ -26,6 +27,7 @@ using Robust.Client.Upload;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Shared;
|
||||
@@ -146,7 +148,18 @@ namespace Robust.Client
|
||||
deps.Register<IConfigurationManagerInternal, ClientNetConfigurationManager>();
|
||||
deps.Register<IClientNetConfigurationManager, ClientNetConfigurationManager>();
|
||||
deps.Register<INetConfigurationManagerInternal, ClientNetConfigurationManager>();
|
||||
|
||||
#if TOOLS
|
||||
deps.Register<IXamlProxyManager, XamlProxyManager>();
|
||||
deps.Register<IXamlHotReloadManager, XamlHotReloadManager>();
|
||||
#else
|
||||
deps.Register<IXamlProxyManager, XamlProxyManagerStub>();
|
||||
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>();
|
||||
#endif
|
||||
|
||||
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
|
||||
deps.Register<MarkupTagManager>();
|
||||
deps.Register<IHWId, BasicHWId>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ namespace Robust.Client.Console.Commands
|
||||
{
|
||||
var wantName = args.Length > 0 ? args[0] : null;
|
||||
|
||||
var basePath = Path.GetDirectoryName(UserDataDir.GetUserDataDir(_gameController))!;
|
||||
var dbPath = Path.Combine(basePath, "launcher", "settings.db");
|
||||
var basePath = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
var launcherDirName = Environment.GetEnvironmentVariable("SS14_LAUNCHER_APPDATA_NAME") ?? "launcher";
|
||||
var dbPath = Path.Combine(basePath, launcherDirName, "settings.db");
|
||||
|
||||
#if USE_SYSTEM_SQLITE
|
||||
SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
|
||||
|
||||
@@ -14,15 +14,6 @@ namespace Robust.Client.Credits
|
||||
/// </summary>
|
||||
public static class CreditsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list of open source software used in the engine and their license.
|
||||
/// </summary>
|
||||
[Obsolete("Use overload that takes in an explicit resource manager instead.")]
|
||||
public static IEnumerable<LicenseEntry> GetLicenses()
|
||||
{
|
||||
return GetLicenses(IoCManager.Resolve<IResourceManager>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of open source software used in the engine and their license.
|
||||
/// </summary>
|
||||
|
||||
@@ -544,7 +544,7 @@ namespace Robust.Client.Debugging
|
||||
switch (joint)
|
||||
{
|
||||
case DistanceJoint:
|
||||
worldHandle.DrawLine(xf1, xf2, JointColor);
|
||||
worldHandle.DrawLine(p1, p2, JointColor);
|
||||
break;
|
||||
case PrismaticJoint prisma:
|
||||
var pA = Transform.Mul(xfa, joint.LocalAnchorA);
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Robust.Client
|
||||
private Thread? _gameThread;
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Start(args, new GameControllerOptions());
|
||||
|
||||
@@ -19,6 +19,7 @@ using Robust.Client.State;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML.Proxy;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.ViewVariables;
|
||||
using Robust.Client.WebViewHook;
|
||||
@@ -53,6 +54,8 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = default!;
|
||||
[Dependency] private readonly IXamlHotReloadManager _xamlHotReloadManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
@@ -109,14 +112,28 @@ namespace Robust.Client
|
||||
_commandLineArgs = args;
|
||||
}
|
||||
|
||||
public string GameTitle()
|
||||
{
|
||||
return Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox";
|
||||
}
|
||||
|
||||
public string WindowIconSet()
|
||||
{
|
||||
return Options.WindowIconSet?.ToString() ?? _resourceManifest!.WindowIconSet ?? "";
|
||||
}
|
||||
|
||||
public string SplashLogo()
|
||||
{
|
||||
return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? "";
|
||||
}
|
||||
|
||||
internal bool StartupContinue(DisplayMode displayMode)
|
||||
{
|
||||
DebugTools.AssertNotNull(_resourceManifest);
|
||||
|
||||
_clyde.InitializePostWindowing();
|
||||
_audio.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(
|
||||
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
|
||||
_clyde.SetWindowTitle(GameTitle());
|
||||
|
||||
_taskManager.Initialize();
|
||||
_parallelMgr.Initialize();
|
||||
@@ -171,6 +188,8 @@ namespace Robust.Client
|
||||
_reflectionManager.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDefaultPrototypes();
|
||||
_xamlProxyManager.Initialize();
|
||||
_xamlHotReloadManager.Initialize();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
@@ -363,7 +382,7 @@ namespace Robust.Client
|
||||
|
||||
_prof.Initialize();
|
||||
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
@@ -394,10 +413,8 @@ namespace Robust.Client
|
||||
// Handle GameControllerOptions implicit CVar overrides.
|
||||
_configurationManager.OverrideConVars(new[]
|
||||
{
|
||||
(CVars.DisplayWindowIconSet.Name,
|
||||
options.WindowIconSet?.ToString() ?? _resourceManifest.WindowIconSet ?? ""),
|
||||
(CVars.DisplaySplashLogo.Name,
|
||||
options.SplashLogo?.ToString() ?? _resourceManifest.SplashLogo ?? "")
|
||||
(CVars.DisplayWindowIconSet.Name, WindowIconSet()),
|
||||
(CVars.DisplaySplashLogo.Name, SplashLogo())
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -99,15 +99,6 @@ namespace Robust.Client.GameObjects
|
||||
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start playing an animation.
|
||||
/// </summary>
|
||||
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
|
||||
public void Play(AnimationPlayerComponent component, Animation animation, string key)
|
||||
{
|
||||
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
|
||||
}
|
||||
|
||||
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
|
||||
{
|
||||
AddComponent(ent);
|
||||
@@ -152,6 +143,14 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
#endif
|
||||
|
||||
foreach (var track in animation.AnimationTracks)
|
||||
{
|
||||
if (track is not AnimationTrackSpriteFlick)
|
||||
continue;
|
||||
|
||||
track.AdvancePlayback(ent.Owner, 0, 0, 0f);
|
||||
}
|
||||
|
||||
ent.Comp.PlayingAnimations.Add(key, playback);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -17,7 +16,6 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
public sealed class ContainerSystem : SharedContainerSystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _netMan = default!;
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
|
||||
[Dependency] private readonly PointLightSystem _lightSys = default!;
|
||||
@@ -242,7 +240,7 @@ namespace Robust.Client.GameObjects
|
||||
#if DEBUG
|
||||
var uid = GetEntity(netEntity);
|
||||
|
||||
if (TryComp<MetaDataComponent>(uid, out var meta))
|
||||
if (TryComp(uid, out MetaDataComponent? meta))
|
||||
{
|
||||
DebugTools.Assert((meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
|
||||
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");
|
||||
|
||||
@@ -82,6 +82,7 @@ namespace Robust.Client.GameObjects
|
||||
var viewport = args.WorldBounds;
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
var fixturesQuery = _entityManager.GetEntityQuery<FixturesComponent>();
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
|
||||
foreach (var grid in _grids)
|
||||
@@ -89,13 +90,15 @@ namespace Robust.Client.GameObjects
|
||||
var worldMatrix = _transformSystem.GetWorldMatrix(grid);
|
||||
worldHandle.SetTransform(worldMatrix);
|
||||
var transform = new Transform(Vector2.Zero, Angle.Zero);
|
||||
var fixtures = fixturesQuery.Comp(grid.Owner);
|
||||
|
||||
var chunkEnumerator = _mapSystem.GetMapChunks(grid.Owner, grid.Comp, viewport);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
foreach (var fixture in chunk.Fixtures.Values)
|
||||
foreach (var id in chunk.Fixtures)
|
||||
{
|
||||
var fixture = fixtures.Fixtures[id];
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
var verts = new Vector2[poly.VertexCount];
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
public sealed class InputSystem : SharedInputSystem, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _stateManager = default!;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an entity's sprite position in world terms.
|
||||
/// </summary>
|
||||
public Vector2 GetSpriteWorldPosition(Entity<SpriteComponent?, TransformComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp2))
|
||||
return Vector2.Zero;
|
||||
|
||||
var (worldPos, worldRot) = _xforms.GetWorldPositionRotation(entity.Owner);
|
||||
|
||||
if (!Resolve(entity, ref entity.Comp1, false))
|
||||
{
|
||||
return worldPos;
|
||||
}
|
||||
|
||||
if (entity.Comp1.NoRotation)
|
||||
{
|
||||
return worldPos + entity.Comp1.Offset;
|
||||
}
|
||||
|
||||
return worldPos + worldRot.RotateVec(entity.Comp1.Rotation.RotateVec(entity.Comp1.Offset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an entity's sprite position in screen coordinates.
|
||||
/// </summary>
|
||||
public ScreenCoordinates GetSpriteScreenCoordinates(Entity<SpriteComponent?, TransformComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp2))
|
||||
return ScreenCoordinates.Invalid;
|
||||
|
||||
var spriteCoords = GetSpriteWorldPosition(entity);
|
||||
return _eye.MapToScreen(new MapCoordinates(spriteCoords, entity.Comp2.MapID));
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,12 @@ namespace Robust.Client.GameObjects
|
||||
public sealed partial class SpriteSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem;
|
||||
@@ -960,7 +960,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
// Initialize and start the newly created entities.
|
||||
if (_toCreate.Count > 0)
|
||||
InitializeAndStart(_toCreate);
|
||||
InitializeAndStart(_toCreate, metas, xforms);
|
||||
|
||||
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
|
||||
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
|
||||
@@ -1188,7 +1188,10 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAndStart(Dictionary<NetEntity, EntityState> toCreate)
|
||||
private void InitializeAndStart(
|
||||
Dictionary<NetEntity, EntityState> toCreate,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
_toStart.Clear();
|
||||
|
||||
@@ -1197,22 +1200,8 @@ namespace Robust.Client.GameStates
|
||||
EntityUid entity = default;
|
||||
foreach (var netEntity in toCreate.Keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
(entity, var meta) = _entityManager.GetEntityData(netEntity);
|
||||
_entities.InitializeEntity(entity, meta);
|
||||
_toStart.Add((entity, netEntity));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
_toCreate.Remove(netEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
(entity, var meta) = _entityManager.GetEntityData(netEntity);
|
||||
InitializeRecursive(entity, meta, metas, xforms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1233,44 @@ namespace Robust.Client.GameStates
|
||||
_brokenEnts.Clear();
|
||||
}
|
||||
|
||||
private void InitializeRecursive(
|
||||
EntityUid entity,
|
||||
MetaDataComponent meta,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
var xform = xforms.GetComponent(entity);
|
||||
if (xform.ParentUid is {Valid: true} parent)
|
||||
{
|
||||
var parentMeta = metas.GetComponent(parent);
|
||||
if (parentMeta.EntityLifeStage < EntityLifeStage.Initialized)
|
||||
InitializeRecursive(parent, parentMeta, metas, xforms);
|
||||
}
|
||||
|
||||
if (meta.EntityLifeStage >= EntityLifeStage.Initialized)
|
||||
{
|
||||
// Was probably already initialized because one of its children appeared earlier in the list.
|
||||
DebugTools.AssertEqual(_toStart.Count(x => x.Item1 == entity), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_entities.InitializeEntity(entity, meta);
|
||||
_toStart.Add((entity, meta.NetEntity));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Server entity threw in Init: nent={meta.NetEntity}, ent={_entities.ToPrettyString(entity)}");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
|
||||
_toCreate.Remove(meta.NetEntity);
|
||||
_brokenEnts.Add(entity);
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
|
||||
EntityState? nextState, GameTick lastApplied, GameTick toTick, bool enteringPvs)
|
||||
{
|
||||
|
||||
@@ -34,9 +34,6 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
int GetApplicableStateCount();
|
||||
|
||||
[Obsolete("use GetApplicableStateCount()")]
|
||||
int CurrentBufferSize => GetApplicableStateCount();
|
||||
|
||||
/// <summary>
|
||||
/// Total number of game states currently in the state buffer.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Robust.Client.GameStates
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IConsoleHost _host = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
|
||||
@@ -46,7 +47,6 @@ namespace Robust.Client.GameStates
|
||||
|
||||
// sum of all data point sizes in bytes
|
||||
private int _totalHistoryPayload;
|
||||
private int _totalUncompressed;
|
||||
|
||||
public EntityUid WatchEntId { get; set; }
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
string? entStateString = null;
|
||||
string? entDelString = null;
|
||||
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
|
||||
var conShell = _host.LocalShell;
|
||||
|
||||
var entStates = args.AppliedState.EntityStates;
|
||||
if (entStates.HasContents)
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
private readonly EntityLookupSystem _lookup;
|
||||
|
||||
|
||||
@@ -109,6 +109,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void SendWindowResized(WindowReg reg, Vector2i oldSize)
|
||||
{
|
||||
if (!reg.IsVisible) // Only send this for open windows
|
||||
return;
|
||||
|
||||
var loaded = RtToLoaded(reg.RenderTarget);
|
||||
loaded.Size = reg.FramebufferSize;
|
||||
|
||||
|
||||
@@ -362,6 +362,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
rect.BottomLeft, rect.BottomRight, color, subRegion);
|
||||
}
|
||||
|
||||
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
|
||||
{
|
||||
base.DrawTexture(texture, position, modulate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an entity.
|
||||
/// </summary>
|
||||
|
||||
@@ -109,8 +109,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
case "glfw":
|
||||
winImpl = new GlfwWindowingImpl(this, _deps);
|
||||
break;
|
||||
case "sdl2":
|
||||
winImpl = new Sdl2WindowingImpl(this, _deps);
|
||||
case "sdl3":
|
||||
winImpl = new Sdl3WindowingImpl(this, _deps);
|
||||
break;
|
||||
default:
|
||||
_logManager.GetSawmill("clyde.win").Log(
|
||||
@@ -343,6 +343,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (isMain)
|
||||
_mainWindow = reg;
|
||||
|
||||
reg.IsVisible = parameters.Visible;
|
||||
|
||||
_windows.Add(reg);
|
||||
_windowHandles.Add(reg.Handle);
|
||||
|
||||
@@ -444,6 +446,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.CursorSet(_mainWindow!, cursor);
|
||||
}
|
||||
|
||||
private void SetWindowSize(WindowReg reg, Vector2i size)
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.WindowSetSize(reg, size);
|
||||
}
|
||||
|
||||
private void SetWindowVisible(WindowReg reg, bool visible)
|
||||
{
|
||||
@@ -459,26 +467,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.RunOnWindowThread(a);
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputSetRect(rect);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputStart();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
DebugTools.AssertNotNull(_windowing);
|
||||
|
||||
_windowing!.TextInputStop();
|
||||
}
|
||||
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
|
||||
|
||||
private abstract class WindowReg
|
||||
{
|
||||
@@ -533,7 +522,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_clyde.DoDestroyWindow(Reg);
|
||||
}
|
||||
|
||||
public Vector2i Size => Reg.FramebufferSize;
|
||||
public Vector2i Size
|
||||
{
|
||||
get => Reg.FramebufferSize;
|
||||
set => _clyde.SetWindowSize(Reg, value);
|
||||
}
|
||||
|
||||
public IRenderTarget RenderTarget => Reg.RenderTarget;
|
||||
|
||||
@@ -578,6 +571,27 @@ namespace Robust.Client.Graphics.Clyde
|
||||
remove => Reg.Resized -= value;
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect, int cursor)
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputSetRect(Reg, rect, cursor);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputStart(Reg);
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
DebugTools.AssertNotNull(_clyde._windowing);
|
||||
|
||||
_clyde._windowing!.TextInputStop(Reg);
|
||||
}
|
||||
|
||||
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEventSubscriber
|
||||
{
|
||||
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly ILightManager _lightManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
@@ -7,6 +7,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
@@ -284,6 +285,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
action();
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => null;
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
public void Dispose()
|
||||
@@ -517,7 +520,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RenderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public Vector2i Size { get; } = default;
|
||||
public Vector2i Size { get; set; } = default;
|
||||
public bool IsDisposed { get; private set; }
|
||||
public WindowId Id { get; set; }
|
||||
public IRenderTarget RenderTarget { get; }
|
||||
@@ -531,6 +534,21 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public event Action<WindowDestroyedEventArgs>? Destroyed;
|
||||
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect, int cursor)
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
// Nop.
|
||||
}
|
||||
|
||||
public void MaximizeOnMonitor(IClydeMonitor monitor)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
@@ -176,6 +176,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
window.BlitDoneEvent!.Reset();
|
||||
window.BlitStartEvent!.Set();
|
||||
window.BlitDoneEvent.Wait();
|
||||
window.UnlockBeforeSwap = Clyde._cfg.GetCVar(CVars.DisplayThreadUnlockBeforeSwap);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -212,8 +213,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
|
||||
Clyde.CheckGlError();
|
||||
|
||||
window.BlitDoneEvent?.Set();
|
||||
if (window.UnlockBeforeSwap)
|
||||
{
|
||||
window.BlitDoneEvent?.Set();
|
||||
}
|
||||
Clyde._windowing!.WindowSwapBuffers(window.Reg);
|
||||
if (!window.UnlockBeforeSwap)
|
||||
{
|
||||
window.BlitDoneEvent?.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void BlitThreadInit(WindowData reg)
|
||||
@@ -336,6 +344,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public Thread? BlitThread;
|
||||
public ManualResetEventSlim? BlitStartEvent;
|
||||
public ManualResetEventSlim? BlitDoneEvent;
|
||||
public bool UnlockBeforeSwap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void ProcessEventChar(EventChar ev)
|
||||
{
|
||||
if (!_textInputActive)
|
||||
var windowReg = FindWindow(ev.Window);
|
||||
if (windowReg is not { TextInputActive: true })
|
||||
return;
|
||||
|
||||
_clyde.SendText(new TextEnteredEventArgs(new Rune(ev.CodePoint).ToString()));
|
||||
|
||||
@@ -85,6 +85,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
WinThreadWinSetMonitor(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetSize cmd:
|
||||
WinThreadWinSetSize(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetVisible cmd:
|
||||
WinThreadWinSetVisible(cmd);
|
||||
break;
|
||||
@@ -234,6 +238,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetSize(
|
||||
nint Window,
|
||||
int W, int H
|
||||
) : CmdBase;
|
||||
|
||||
private sealed record CmdWinSetVisible(
|
||||
nint Window,
|
||||
bool Visible
|
||||
|
||||
@@ -84,6 +84,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
);
|
||||
}
|
||||
|
||||
public void WindowSetSize(WindowReg window, Vector2i size)
|
||||
{
|
||||
var reg = (GlfwWindowReg) window;
|
||||
|
||||
SendCmd(new CmdWinSetSize((nint) reg.GlfwWindow, size.X, size.Y));
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
var reg = (GlfwWindowReg) window;
|
||||
@@ -92,6 +99,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
SendCmd(new CmdWinSetVisible((nint) reg.GlfwWindow, visible));
|
||||
}
|
||||
|
||||
private void WinThreadWinSetSize(CmdWinSetSize cmd)
|
||||
{
|
||||
var win = (Window*) cmd.Window;
|
||||
|
||||
GLFW.SetWindowSize(win, cmd.W, cmd.H);
|
||||
}
|
||||
|
||||
private void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
var win = (Window*) cmd.Window;
|
||||
@@ -454,6 +468,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.WindowHint(WindowHintInt.AlphaBits, 8);
|
||||
GLFW.WindowHint(WindowHintInt.StencilBits, 8);
|
||||
|
||||
GLFW.WindowHint(WindowHintBool.Decorated, (parameters.Styles & OSWindowStyles.NoTitleBar) == 0);
|
||||
|
||||
var window = GLFW.CreateWindow(
|
||||
parameters.Width, parameters.Height,
|
||||
parameters.Title,
|
||||
@@ -471,23 +487,12 @@ namespace Robust.Client.Graphics.Clyde
|
||||
GLFW.MaximizeWindow(window);
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
|
||||
{
|
||||
GLFW.WindowHint(WindowHintBool.Decorated, false);
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var hWnd = (HWND) GLFW.GetWin32Window(window);
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWL.GWL_STYLE,
|
||||
// Cast to long here to work around a bug in rider with nint bitwise operators.
|
||||
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
|
||||
WsiShared.SetWindowStyleNoTitleOptionsWindows(hWnd);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
@@ -495,23 +500,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var x11Window = (X11Window)GLFW.GetX11Window(window);
|
||||
var x11Display = (Display*) GLFW.GetX11Display(window);
|
||||
DebugTools.Assert(x11Window != X11Window.NULL);
|
||||
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
|
||||
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
|
||||
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
|
||||
DebugTools.Assert(newPropVal != Atom.NULL);
|
||||
|
||||
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
|
||||
#pragma warning disable CA1806
|
||||
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
|
||||
Xlib.XChangeProperty(x11Display, x11Window,
|
||||
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
|
||||
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
|
||||
(byte*)&newPropVal, 1);
|
||||
#pragma warning restore CA1806
|
||||
|
||||
Marshal.FreeCoTaskMem(newPropValString);
|
||||
Marshal.FreeCoTaskMem(propNameString);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
@@ -629,16 +618,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return reg;
|
||||
}
|
||||
|
||||
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
|
||||
private GlfwWindowReg? FindWindow(nint window) => FindWindow((Window*) window);
|
||||
|
||||
private WindowReg? FindWindow(Window* window)
|
||||
private GlfwWindowReg? FindWindow(Window* window)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (GlfwWindowReg) windowReg;
|
||||
if (glfwReg.GlfwWindow == window)
|
||||
{
|
||||
return windowReg;
|
||||
return glfwReg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,23 +714,23 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return (void*) GLFW.GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
|
||||
{
|
||||
// Not supported on GLFW.
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
public void TextInputStart(WindowReg reg)
|
||||
{
|
||||
// Not properly supported on GLFW.
|
||||
|
||||
_textInputActive = true;
|
||||
((GlfwWindowReg)reg).TextInputActive = true;
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
public void TextInputStop(WindowReg reg)
|
||||
{
|
||||
// Not properly supported on GLFW.
|
||||
|
||||
_textInputActive = false;
|
||||
((GlfwWindowReg)reg).TextInputActive = false;
|
||||
}
|
||||
|
||||
private void CheckWindowDisposed(WindowReg reg)
|
||||
@@ -756,6 +745,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
|
||||
// While GLFW does not provide proper IME APIs, we can at least emulate SDL3's StartTextInput() system.
|
||||
// This will ensure some level of consistency between the backends.
|
||||
public bool TextInputActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private bool _glfwInitialized;
|
||||
private bool _win32Experience;
|
||||
|
||||
// While GLFW does not provide proper IME APIs, we can at least emulate SDL2's StartTextInput() system.
|
||||
// This will ensure some level of consistency between the backends.
|
||||
private bool _textInputActive;
|
||||
|
||||
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
void WindowDestroy(WindowReg reg);
|
||||
void WindowSetTitle(WindowReg window, string title);
|
||||
void WindowSetMonitor(WindowReg window, IClydeMonitor monitor);
|
||||
void WindowSetSize(WindowReg window, Vector2i size);
|
||||
void WindowSetVisible(WindowReg window, bool visible);
|
||||
void WindowRequestAttention(WindowReg window);
|
||||
void WindowSwapBuffers(WindowReg window);
|
||||
@@ -65,9 +66,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
void RunOnWindowThread(Action a);
|
||||
|
||||
// IME
|
||||
void TextInputSetRect(UIBox2i rect);
|
||||
void TextInputStart();
|
||||
void TextInputStop();
|
||||
void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor);
|
||||
void TextInputStart(WindowReg reg);
|
||||
void TextInputStop(WindowReg reg);
|
||||
string GetDescription();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
25
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/LICENSE
Normal file
25
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/LICENSE
Normal file
@@ -0,0 +1,25 @@
|
||||
/* SDL3-CS - C# Bindings for SDL3
|
||||
*
|
||||
* Copyright (c) 2024 Colin Jackson
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied warranty.
|
||||
* In no event will the authors be held liable for any damages arising from
|
||||
* the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software in a
|
||||
* product, an acknowledgment in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
*
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
*
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*
|
||||
* Colin "cryy22" Jackson <c@cryy22.art>
|
||||
*
|
||||
*/
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SDL3;
|
||||
|
||||
public static partial class SDL
|
||||
{
|
||||
// Extensions to SDL3-CS that aren't part of the main library.
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
|
||||
|
||||
[LibraryImport(nativeLibName, EntryPoint = "SDL_WaitEvent")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static partial SDLBool SDL_WaitEventRef(ref SDL_Event @event);
|
||||
|
||||
public const byte SDL_BUTTON_LEFT = 1;
|
||||
public const byte SDL_BUTTON_MIDDLE = 2;
|
||||
public const byte SDL_BUTTON_RIGHT = 3;
|
||||
public const byte SDL_BUTTON_X1 = 4;
|
||||
public const byte SDL_BUTTON_X2 = 5;
|
||||
|
||||
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
|
||||
|
||||
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
|
||||
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
|
||||
|
||||
public const int SDL_FILEDIALOG_OPENFILE = 0;
|
||||
public const int SDL_FILEDIALOG_SAVEFILE = 1;
|
||||
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
|
||||
|
||||
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
|
||||
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
|
||||
|
||||
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
|
||||
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
|
||||
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
|
||||
}
|
||||
8043
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/SDL3.Core.cs
Normal file
8043
Robust.Client/Graphics/Clyde/Windowing/SDL3-CS/SDL3.Core.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_Scancode;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using Button = Robust.Client.Input.Mouse.Button;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
// Indices are values of SDL_Scancode
|
||||
private static readonly Key[] KeyMap;
|
||||
private static readonly FrozenDictionary<Key, SDL_Scancode> KeyMapReverse;
|
||||
private static readonly Button[] MouseButtonMap;
|
||||
|
||||
// TODO: to avoid having to ask the windowing thread, key names are cached.
|
||||
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
|
||||
|
||||
private void ReloadKeyMap()
|
||||
{
|
||||
// This may be ran concurrently from the windowing thread.
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
_printableKeyNameMap.Clear();
|
||||
|
||||
// List of mappable keys from SDL2's source appears to be:
|
||||
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
|
||||
// (the actual logic is more nuanced, but it appears to match the above)
|
||||
// Comes out to these two ranges:
|
||||
|
||||
for (var k = SDL_SCANCODE_A; k <= SDL_SCANCODE_0; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
for (var k = SDL_SCANCODE_MINUS; k <= SDL_SCANCODE_SLASH; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
void CacheKey(SDL_Scancode scancode)
|
||||
{
|
||||
var rKey = ConvertSdl2Scancode(scancode);
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
var name = SDL_GetKeyName(SDL_GetKeyFromScancode(scancode));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? KeyGetName(Key key)
|
||||
{
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
if (_printableKeyNameMap.TryGetValue(key, out var name))
|
||||
return name;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Key ConvertSdl2Scancode(SDL_Scancode scancode)
|
||||
{
|
||||
return KeyMap[(int) scancode];
|
||||
}
|
||||
|
||||
public static Button ConvertSdl2Button(int button)
|
||||
{
|
||||
return MouseButtonMap[button];
|
||||
}
|
||||
|
||||
static Sdl2WindowingImpl()
|
||||
{
|
||||
MouseButtonMap = new Button[6];
|
||||
MouseButtonMap[SDL_BUTTON_LEFT] = Button.Left;
|
||||
MouseButtonMap[SDL_BUTTON_RIGHT] = Button.Right;
|
||||
MouseButtonMap[SDL_BUTTON_MIDDLE] = Button.Middle;
|
||||
MouseButtonMap[SDL_BUTTON_X1] = Button.Button4;
|
||||
MouseButtonMap[SDL_BUTTON_X2] = Button.Button5;
|
||||
|
||||
KeyMap = new Key[(int) SDL_NUM_SCANCODES];
|
||||
MapKey(SDL_SCANCODE_A, Key.A);
|
||||
MapKey(SDL_SCANCODE_B, Key.B);
|
||||
MapKey(SDL_SCANCODE_C, Key.C);
|
||||
MapKey(SDL_SCANCODE_D, Key.D);
|
||||
MapKey(SDL_SCANCODE_E, Key.E);
|
||||
MapKey(SDL_SCANCODE_F, Key.F);
|
||||
MapKey(SDL_SCANCODE_G, Key.G);
|
||||
MapKey(SDL_SCANCODE_H, Key.H);
|
||||
MapKey(SDL_SCANCODE_I, Key.I);
|
||||
MapKey(SDL_SCANCODE_J, Key.J);
|
||||
MapKey(SDL_SCANCODE_K, Key.K);
|
||||
MapKey(SDL_SCANCODE_L, Key.L);
|
||||
MapKey(SDL_SCANCODE_M, Key.M);
|
||||
MapKey(SDL_SCANCODE_N, Key.N);
|
||||
MapKey(SDL_SCANCODE_O, Key.O);
|
||||
MapKey(SDL_SCANCODE_P, Key.P);
|
||||
MapKey(SDL_SCANCODE_Q, Key.Q);
|
||||
MapKey(SDL_SCANCODE_R, Key.R);
|
||||
MapKey(SDL_SCANCODE_S, Key.S);
|
||||
MapKey(SDL_SCANCODE_T, Key.T);
|
||||
MapKey(SDL_SCANCODE_U, Key.U);
|
||||
MapKey(SDL_SCANCODE_V, Key.V);
|
||||
MapKey(SDL_SCANCODE_W, Key.W);
|
||||
MapKey(SDL_SCANCODE_X, Key.X);
|
||||
MapKey(SDL_SCANCODE_Y, Key.Y);
|
||||
MapKey(SDL_SCANCODE_Z, Key.Z);
|
||||
MapKey(SDL_SCANCODE_0, Key.Num0);
|
||||
MapKey(SDL_SCANCODE_1, Key.Num1);
|
||||
MapKey(SDL_SCANCODE_2, Key.Num2);
|
||||
MapKey(SDL_SCANCODE_3, Key.Num3);
|
||||
MapKey(SDL_SCANCODE_4, Key.Num4);
|
||||
MapKey(SDL_SCANCODE_5, Key.Num5);
|
||||
MapKey(SDL_SCANCODE_6, Key.Num6);
|
||||
MapKey(SDL_SCANCODE_7, Key.Num7);
|
||||
MapKey(SDL_SCANCODE_8, Key.Num8);
|
||||
MapKey(SDL_SCANCODE_9, Key.Num9);
|
||||
MapKey(SDL_SCANCODE_KP_0, Key.NumpadNum0);
|
||||
MapKey(SDL_SCANCODE_KP_1, Key.NumpadNum1);
|
||||
MapKey(SDL_SCANCODE_KP_2, Key.NumpadNum2);
|
||||
MapKey(SDL_SCANCODE_KP_3, Key.NumpadNum3);
|
||||
MapKey(SDL_SCANCODE_KP_4, Key.NumpadNum4);
|
||||
MapKey(SDL_SCANCODE_KP_5, Key.NumpadNum5);
|
||||
MapKey(SDL_SCANCODE_KP_6, Key.NumpadNum6);
|
||||
MapKey(SDL_SCANCODE_KP_7, Key.NumpadNum7);
|
||||
MapKey(SDL_SCANCODE_KP_8, Key.NumpadNum8);
|
||||
MapKey(SDL_SCANCODE_KP_9, Key.NumpadNum9);
|
||||
MapKey(SDL_SCANCODE_ESCAPE, Key.Escape);
|
||||
MapKey(SDL_SCANCODE_LCTRL, Key.Control);
|
||||
MapKey(SDL_SCANCODE_RCTRL, Key.Control);
|
||||
MapKey(SDL_SCANCODE_RSHIFT, Key.Shift);
|
||||
MapKey(SDL_SCANCODE_LSHIFT, Key.Shift);
|
||||
MapKey(SDL_SCANCODE_LALT, Key.Alt);
|
||||
MapKey(SDL_SCANCODE_RALT, Key.Alt);
|
||||
MapKey(SDL_SCANCODE_LGUI, Key.LSystem);
|
||||
MapKey(SDL_SCANCODE_RGUI, Key.RSystem);
|
||||
MapKey(SDL_SCANCODE_MENU, Key.Menu);
|
||||
MapKey(SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
|
||||
MapKey(SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
|
||||
MapKey(SDL_SCANCODE_SEMICOLON, Key.SemiColon);
|
||||
MapKey(SDL_SCANCODE_COMMA, Key.Comma);
|
||||
MapKey(SDL_SCANCODE_PERIOD, Key.Period);
|
||||
MapKey(SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
|
||||
MapKey(SDL_SCANCODE_SLASH, Key.Slash);
|
||||
MapKey(SDL_SCANCODE_BACKSLASH, Key.BackSlash);
|
||||
MapKey(SDL_SCANCODE_GRAVE, Key.Tilde);
|
||||
MapKey(SDL_SCANCODE_EQUALS, Key.Equal);
|
||||
MapKey(SDL_SCANCODE_SPACE, Key.Space);
|
||||
MapKey(SDL_SCANCODE_RETURN, Key.Return);
|
||||
MapKey(SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
|
||||
MapKey(SDL_SCANCODE_BACKSPACE, Key.BackSpace);
|
||||
MapKey(SDL_SCANCODE_TAB, Key.Tab);
|
||||
MapKey(SDL_SCANCODE_PAGEUP, Key.PageUp);
|
||||
MapKey(SDL_SCANCODE_PAGEDOWN, Key.PageDown);
|
||||
MapKey(SDL_SCANCODE_END, Key.End);
|
||||
MapKey(SDL_SCANCODE_HOME, Key.Home);
|
||||
MapKey(SDL_SCANCODE_INSERT, Key.Insert);
|
||||
MapKey(SDL_SCANCODE_DELETE, Key.Delete);
|
||||
MapKey(SDL_SCANCODE_MINUS, Key.Minus);
|
||||
MapKey(SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
|
||||
MapKey(SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
|
||||
MapKey(SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
|
||||
MapKey(SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
|
||||
MapKey(SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
|
||||
MapKey(SDL_SCANCODE_LEFT, Key.Left);
|
||||
MapKey(SDL_SCANCODE_RIGHT, Key.Right);
|
||||
MapKey(SDL_SCANCODE_UP, Key.Up);
|
||||
MapKey(SDL_SCANCODE_DOWN, Key.Down);
|
||||
MapKey(SDL_SCANCODE_F1, Key.F1);
|
||||
MapKey(SDL_SCANCODE_F2, Key.F2);
|
||||
MapKey(SDL_SCANCODE_F3, Key.F3);
|
||||
MapKey(SDL_SCANCODE_F4, Key.F4);
|
||||
MapKey(SDL_SCANCODE_F5, Key.F5);
|
||||
MapKey(SDL_SCANCODE_F6, Key.F6);
|
||||
MapKey(SDL_SCANCODE_F7, Key.F7);
|
||||
MapKey(SDL_SCANCODE_F8, Key.F8);
|
||||
MapKey(SDL_SCANCODE_F9, Key.F9);
|
||||
MapKey(SDL_SCANCODE_F10, Key.F10);
|
||||
MapKey(SDL_SCANCODE_F11, Key.F11);
|
||||
MapKey(SDL_SCANCODE_F12, Key.F12);
|
||||
MapKey(SDL_SCANCODE_F13, Key.F13);
|
||||
MapKey(SDL_SCANCODE_F14, Key.F14);
|
||||
MapKey(SDL_SCANCODE_F15, Key.F15);
|
||||
MapKey(SDL_SCANCODE_F16, Key.F16);
|
||||
MapKey(SDL_SCANCODE_F17, Key.F17);
|
||||
MapKey(SDL_SCANCODE_F18, Key.F18);
|
||||
MapKey(SDL_SCANCODE_F19, Key.F19);
|
||||
MapKey(SDL_SCANCODE_F20, Key.F20);
|
||||
MapKey(SDL_SCANCODE_F21, Key.F21);
|
||||
MapKey(SDL_SCANCODE_F22, Key.F22);
|
||||
MapKey(SDL_SCANCODE_F23, Key.F23);
|
||||
MapKey(SDL_SCANCODE_F24, Key.F24);
|
||||
MapKey(SDL_SCANCODE_PAUSE, Key.Pause);
|
||||
|
||||
var keyMapReverse = new Dictionary<Key, SDL_Scancode>();
|
||||
|
||||
for (var code = 0; code < KeyMap.Length; code++)
|
||||
{
|
||||
var key = KeyMap[code];
|
||||
if (key != Key.Unknown)
|
||||
keyMapReverse[key] = (SDL_Scancode) code;
|
||||
}
|
||||
|
||||
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void MapKey(SDL_Scancode code, Key key)
|
||||
{
|
||||
KeyMap[(int)code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL2;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
// NOTE: SDL2 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
|
||||
|
||||
// Can't use ClydeHandle because it's not thread safe to allocate.
|
||||
private int _nextMonitorId = 1;
|
||||
|
||||
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
|
||||
private readonly Dictionary<int, Sdl2MonitorReg> _monitors = new();
|
||||
|
||||
private void InitMonitors()
|
||||
{
|
||||
var numDisplays = SDL.SDL_GetNumVideoDisplays();
|
||||
for (var i = 0; i < numDisplays; i++)
|
||||
{
|
||||
// SDL.SDL_GetDisplayDPI(i, out var ddpi, out var hdpi, out var vdpi);
|
||||
// _sawmill.Info($"[{i}] {ddpi} {hdpi} {vdpi}");
|
||||
WinThreadSetupMonitor(i);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WinThreadSetupMonitor(int displayIdx)
|
||||
{
|
||||
var id = _nextMonitorId++;
|
||||
|
||||
var name = SDL.SDL_GetDisplayName(displayIdx);
|
||||
var modeCount = SDL.SDL_GetNumDisplayModes(displayIdx);
|
||||
SDL.SDL_GetCurrentDisplayMode(displayIdx, out var curMode);
|
||||
var modes = new VideoMode[modeCount];
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
SDL.SDL_GetDisplayMode(displayIdx, i, out var mode);
|
||||
modes[i] = ConvertVideoMode(mode);
|
||||
}
|
||||
|
||||
_winThreadMonitors.Add(id, new WinThreadMonitorReg { Id = id, DisplayIdx = displayIdx });
|
||||
|
||||
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(curMode), modes));
|
||||
|
||||
if (displayIdx == 0)
|
||||
_clyde._primaryMonitorId = id;
|
||||
}
|
||||
|
||||
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = (ushort)mode.w,
|
||||
Height = (ushort)mode.h,
|
||||
RefreshRate = (ushort)mode.refresh_rate,
|
||||
// TODO: set bits count based on format (I'm lazy)
|
||||
RedBits = 8,
|
||||
GreenBits = 8,
|
||||
BlueBits = 8,
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessSetupMonitor(EventMonitorSetup ev)
|
||||
{
|
||||
var impl = new MonitorHandle(
|
||||
ev.Id,
|
||||
ev.Name,
|
||||
(ev.CurrentMode.Width, ev.CurrentMode.Height),
|
||||
ev.CurrentMode.RefreshRate,
|
||||
ev.AllModes);
|
||||
|
||||
_clyde._monitorHandles.Add(ev.Id, impl);
|
||||
_monitors[ev.Id] = new Sdl2MonitorReg
|
||||
{
|
||||
Id = ev.Id,
|
||||
Handle = impl
|
||||
};
|
||||
}
|
||||
|
||||
private void WinThreadDestroyMonitor(int displayIdx)
|
||||
{
|
||||
var monitorId = 0;
|
||||
|
||||
foreach (var (id, monitorReg) in _winThreadMonitors)
|
||||
{
|
||||
if (monitorReg.DisplayIdx == displayIdx)
|
||||
{
|
||||
monitorId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (monitorId == 0)
|
||||
return;
|
||||
|
||||
// So SDL2 doesn't have a very nice indexing system for monitors like GLFW does.
|
||||
// This means that, when a monitor is disconnected, all monitors *after* it get shifted down one slot.
|
||||
// Now, this happens *after* the event is fired, to make matters worse.
|
||||
// So we're basically trying to match unspecified SDL2 internals here. Great.
|
||||
|
||||
_winThreadMonitors.Remove(monitorId);
|
||||
|
||||
foreach (var (_, reg) in _winThreadMonitors)
|
||||
{
|
||||
if (reg.DisplayIdx > displayIdx)
|
||||
reg.DisplayIdx -= 1;
|
||||
}
|
||||
|
||||
SendEvent(new EventMonitorDestroy(monitorId));
|
||||
}
|
||||
|
||||
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
|
||||
{
|
||||
_monitors.Remove(ev.Id);
|
||||
_clyde._monitorHandles.Remove(ev.Id);
|
||||
}
|
||||
|
||||
private sealed class Sdl2MonitorReg : MonitorReg
|
||||
{
|
||||
public int Id;
|
||||
}
|
||||
|
||||
private sealed class WinThreadMonitorReg
|
||||
{
|
||||
public int Id;
|
||||
public int DisplayIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Maths;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_EventType;
|
||||
using static SDL2.SDL.SDL_SYSWM_TYPE;
|
||||
using static SDL2.SDL.SDL_WindowEventID;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
|
||||
private static unsafe int EventWatch(void* userdata, SDL_Event* sdlevent)
|
||||
{
|
||||
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
ref readonly var ev = ref *sdlevent;
|
||||
|
||||
obj.ProcessSdl2Event(in ev);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ProcessSdl2Event(in SDL_Event ev)
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case SDL_WINDOWEVENT:
|
||||
ProcessSdl2EventWindow(in ev.window);
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP:
|
||||
ProcessSdl2KeyEvent(in ev.key);
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
ProcessSdl2EventTextInput(in ev.text);
|
||||
break;
|
||||
case SDL_TEXTEDITING:
|
||||
ProcessSdl2EventTextEditing(in ev.edit);
|
||||
break;
|
||||
case SDL_KEYMAPCHANGED:
|
||||
ProcessSdl2EventKeyMapChanged();
|
||||
break;
|
||||
case SDL_TEXTEDITING_EXT:
|
||||
ProcessSdl2EventTextEditingExt(in ev.editExt);
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
ProcessSdl2EventMouseMotion(in ev.motion);
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
ProcessSdl2EventMouseButton(in ev.button);
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
ProcessSdl2EventMouseWheel(in ev.wheel);
|
||||
break;
|
||||
case SDL_DISPLAYEVENT:
|
||||
ProcessSdl2EventDisplay(in ev.display);
|
||||
break;
|
||||
case SDL_SYSWMEVENT:
|
||||
ProcessSdl2EventSysWM(in ev.syswm);
|
||||
break;
|
||||
case SDL_QUIT:
|
||||
ProcessSdl2EventQuit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventQuit()
|
||||
{
|
||||
SendEvent(new EventQuit());
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventDisplay(in SDL_DisplayEvent evDisplay)
|
||||
{
|
||||
switch (evDisplay.displayEvent)
|
||||
{
|
||||
case SDL_DisplayEventID.SDL_DISPLAYEVENT_CONNECTED:
|
||||
WinThreadSetupMonitor((int) evDisplay.display);
|
||||
break;
|
||||
case SDL_DisplayEventID.SDL_DISPLAYEVENT_DISCONNECTED:
|
||||
WinThreadDestroyMonitor((int) evDisplay.display);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseWheel(in SDL_MouseWheelEvent ev)
|
||||
{
|
||||
SendEvent(new EventWheel(ev.windowID, ev.preciseX, ev.preciseY));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseButton(in SDL_MouseButtonEvent ev)
|
||||
{
|
||||
SendEvent(new EventMouseButton(ev.windowID, ev.type, ev.button));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventMouseMotion(in SDL_MouseMotionEvent ev)
|
||||
{
|
||||
// _sawmill.Info($"{evMotion.x}, {evMotion.y}, {evMotion.xrel}, {evMotion.yrel}");
|
||||
SendEvent(new EventMouseMotion(ev.windowID, ev.x, ev.y, ev.xrel, ev.yrel));
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextInput(in SDL_TextInputEvent ev)
|
||||
{
|
||||
fixed (byte* text = ev.text)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)text) ?? "";
|
||||
// _logManager.GetSawmill("ime").Debug($"Input: {str}");
|
||||
SendEvent(new EventText(ev.windowID, str));
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextEditing(in SDL_TextEditingEvent ev)
|
||||
{
|
||||
fixed (byte* text = ev.text)
|
||||
{
|
||||
SendTextEditing(ev.windowID, text, ev.start, ev.length);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl2EventTextEditingExt(in SDL_TextEditingExtEvent ev)
|
||||
{
|
||||
SendTextEditing(ev.windowID, (byte*) ev.text, ev.start, ev.length);
|
||||
SDL_free(ev.text);
|
||||
}
|
||||
|
||||
private unsafe void SendTextEditing(uint window, byte* text, int start, int length)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((nint) text) ?? "";
|
||||
// _logManager.GetSawmill("ime").Debug($"Editing: '{str}', start: {start}, len: {length}");
|
||||
SendEvent(new EventTextEditing(window, str, start, length));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventKeyMapChanged()
|
||||
{
|
||||
ReloadKeyMap();
|
||||
SendEvent(new EventKeyMapChanged());
|
||||
}
|
||||
|
||||
private void ProcessSdl2KeyEvent(in SDL_KeyboardEvent ev)
|
||||
{
|
||||
SendEvent(new EventKey(
|
||||
ev.windowID,
|
||||
ev.keysym.scancode,
|
||||
ev.type,
|
||||
ev.repeat != 0,
|
||||
ev.keysym.mod));
|
||||
}
|
||||
|
||||
private void ProcessSdl2EventWindow(in SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL_GetWindowFromID(ev.windowID);
|
||||
|
||||
switch (ev.windowEvent)
|
||||
{
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
var width = ev.data1;
|
||||
var height = ev.data2;
|
||||
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
var (xScale, yScale) = GetWindowScale(window);
|
||||
|
||||
_sawmill.Debug($"{width}x{height}, {fbW}x{fbH}, {xScale}x{yScale}");
|
||||
|
||||
SendEvent(new EventWindowSize(ev.windowID, width, height, fbW, fbH, xScale, yScale));
|
||||
break;
|
||||
|
||||
default:
|
||||
SendEvent(new EventWindow(ev.windowID, ev.windowEvent));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private unsafe void ProcessSdl2EventSysWM(in SDL_SysWMEvent ev)
|
||||
{
|
||||
ref readonly var sysWmMessage = ref *(SDL_SysWMmsg*)ev.msg;
|
||||
if (sysWmMessage.subsystem != SDL_SYSWM_WINDOWS)
|
||||
return;
|
||||
|
||||
ref readonly var winMessage = ref *(SDL_SysWMmsgWin32*)ev.msg;
|
||||
if (winMessage.msg is WM.WM_KEYDOWN or WM.WM_KEYUP)
|
||||
{
|
||||
TryWin32VirtualVKey(in winMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryWin32VirtualVKey(in SDL_SysWMmsgWin32 msg)
|
||||
{
|
||||
// Workaround for https://github.com/ocornut/imgui/issues/2977
|
||||
// This is gonna bite me in the ass if SDL2 ever fixes this upstream, isn't it...
|
||||
// (I spent disproportionate amounts of effort on this).
|
||||
|
||||
// Code for V key.
|
||||
if ((int)msg.wParam is not (0x56 or VK.VK_CONTROL))
|
||||
return;
|
||||
|
||||
var scanCode = (msg.lParam >> 16) & 0xFF;
|
||||
if (scanCode != 0)
|
||||
return;
|
||||
|
||||
SendEvent(new EventWindowsFakeV(msg.hwnd, msg.msg, msg.wParam));
|
||||
}
|
||||
|
||||
private abstract record EventBase;
|
||||
|
||||
private record EventWindowCreate(
|
||||
Sdl2WindowCreateResult Result,
|
||||
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
|
||||
) : EventBase;
|
||||
|
||||
private record EventKey(
|
||||
uint WindowId,
|
||||
SDL_Scancode Scancode,
|
||||
SDL_EventType Type,
|
||||
bool Repeat,
|
||||
SDL_Keymod Mods
|
||||
) : EventBase;
|
||||
|
||||
private record EventMouseMotion(
|
||||
uint WindowId,
|
||||
int X, int Y,
|
||||
int XRel, int YRel
|
||||
) : EventBase;
|
||||
|
||||
private record EventMouseButton(
|
||||
uint WindowId,
|
||||
SDL_EventType Type,
|
||||
byte Button
|
||||
) : EventBase;
|
||||
|
||||
private record EventText(
|
||||
uint WindowId,
|
||||
string Text
|
||||
) : EventBase;
|
||||
|
||||
private record EventTextEditing(
|
||||
uint WindowId,
|
||||
string Text,
|
||||
int Start,
|
||||
int Length
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowSize(
|
||||
uint WindowId,
|
||||
int Width,
|
||||
int Height,
|
||||
int FramebufferWidth,
|
||||
int FramebufferHeight,
|
||||
float XScale,
|
||||
float YScale
|
||||
) : EventBase;
|
||||
|
||||
private record EventWheel(
|
||||
uint WindowId,
|
||||
float XOffset,
|
||||
float YOffset
|
||||
) : EventBase;
|
||||
|
||||
// SDL_WindowEvents that don't have special handling like size.
|
||||
private record EventWindow(
|
||||
uint WindowId,
|
||||
SDL_WindowEventID EventId
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorSetup
|
||||
(
|
||||
int Id,
|
||||
string Name,
|
||||
VideoMode CurrentMode,
|
||||
VideoMode[] AllModes
|
||||
) : EventBase;
|
||||
|
||||
private record EventMonitorDestroy
|
||||
(
|
||||
int Id
|
||||
) : EventBase;
|
||||
|
||||
private record EventWindowsFakeV(HWND Window,
|
||||
uint Message, WPARAM WParam) : EventBase;
|
||||
|
||||
private record EventKeyMapChanged : EventBase;
|
||||
private record EventQuit : EventBase;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
private struct SDL_SysWMmsg
|
||||
{
|
||||
public SDL_version version;
|
||||
public SDL_SYSWM_TYPE subsystem;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo")]
|
||||
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||
private struct SDL_SysWMmsgWin32
|
||||
{
|
||||
public SDL_version version;
|
||||
public SDL_SYSWM_TYPE subsystem;
|
||||
public HWND hwnd;
|
||||
public uint msg;
|
||||
public WPARAM wParam;
|
||||
public LPARAM lParam;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,575 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_bool;
|
||||
using static SDL2.SDL.SDL_FlashOperation;
|
||||
using static SDL2.SDL.SDL_GLattr;
|
||||
using static SDL2.SDL.SDL_GLcontext;
|
||||
using static SDL2.SDL.SDL_GLprofile;
|
||||
using static SDL2.SDL.SDL_SYSWM_TYPE;
|
||||
using static SDL2.SDL.SDL_WindowFlags;
|
||||
using BOOL = TerraFX.Interop.Windows.BOOL;
|
||||
using HWND = TerraFX.Interop.Windows.HWND;
|
||||
using GWLP = TerraFX.Interop.Windows.GWLP;
|
||||
using Windows = TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
{
|
||||
private int _nextWindowId = 1;
|
||||
|
||||
public (WindowReg?, string? error) WindowCreate(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
WindowReg? share,
|
||||
WindowReg? owner)
|
||||
{
|
||||
nint shareWindow = 0;
|
||||
nint shareContext = 0;
|
||||
if (share is Sdl2WindowReg shareReg)
|
||||
{
|
||||
shareWindow = shareReg.Sdl2Window;
|
||||
shareContext = shareReg.GlContext;
|
||||
}
|
||||
|
||||
nint ownerPtr = 0;
|
||||
if (owner is Sdl2WindowReg ownerReg)
|
||||
ownerPtr = ownerReg.Sdl2Window;
|
||||
|
||||
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
|
||||
|
||||
// Block the main thread (to avoid stuff like texture uploads being problematic).
|
||||
WaitWindowCreate(task);
|
||||
|
||||
#pragma warning disable RA0004
|
||||
// Block above ensured task is done, this is safe.
|
||||
var (reg, error) = task.Result;
|
||||
#pragma warning restore RA0004
|
||||
if (reg != null)
|
||||
{
|
||||
reg.Owner = reg.Handle;
|
||||
}
|
||||
|
||||
return (reg, error);
|
||||
}
|
||||
|
||||
private void WaitWindowCreate(Task<Sdl2WindowCreateResult> windowTask)
|
||||
{
|
||||
while (!windowTask.IsCompleted)
|
||||
{
|
||||
// Keep processing events until the window task gives either an error or success.
|
||||
WaitEvents();
|
||||
ProcessEvents(single: true);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Sdl2WindowCreateResult> SharedWindowCreate(
|
||||
GLContextSpec? glSpec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint owner)
|
||||
{
|
||||
//
|
||||
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
|
||||
// I originally wanted this to be async so we could avoid blocking the main thread
|
||||
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
|
||||
// This doesn't *work* because
|
||||
// we have to release the GL context while the shared context is being created.
|
||||
// (at least on WGL, I didn't test other platforms and I don't care to.)
|
||||
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
|
||||
// because rendering would be locked up *anyways*.
|
||||
//
|
||||
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
|
||||
// and I should get on either Veldrid or Vulkan some time.
|
||||
// Probably Veldrid tbh.
|
||||
//
|
||||
|
||||
// Yes we ping-pong this TCS through the window thread and back, deal with it.
|
||||
var tcs = new TaskCompletionSource<Sdl2WindowCreateResult>();
|
||||
SendCmd(new CmdWinCreate(glSpec, parameters, shareWindow, shareContext, owner, tcs));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void FinishWindowCreate(EventWindowCreate ev)
|
||||
{
|
||||
var (res, tcs) = ev;
|
||||
|
||||
tcs.TrySetResult(res);
|
||||
}
|
||||
|
||||
private void WinThreadWinCreate(CmdWinCreate cmd)
|
||||
{
|
||||
var (glSpec, parameters, shareWindow, shareContext, owner, tcs) = cmd;
|
||||
|
||||
var (window, context) = CreateSdl2WindowForRenderer(glSpec, parameters, shareWindow, shareContext, owner);
|
||||
|
||||
if (window == 0)
|
||||
{
|
||||
var err = SDL_GetError();
|
||||
|
||||
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(null, err), tcs));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't invoke the TCS directly from the windowing thread because:
|
||||
// * it'd hit the synchronization context,
|
||||
// which would make (blocking) main window init more annoying.
|
||||
// * it'd not be synchronized to other incoming window events correctly which might be icky.
|
||||
// So we send the TCS back to the game thread
|
||||
// which processes events in the correct order and has better control of stuff during init.
|
||||
var reg = WinThreadSetupWindow(window, context);
|
||||
|
||||
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(reg, null), tcs));
|
||||
}
|
||||
|
||||
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && cmd.HadOwner)
|
||||
{
|
||||
// On Windows, closing the child window causes the owner to be minimized, apparently.
|
||||
// Clear owner on close to avoid this.
|
||||
|
||||
SDL_SysWMinfo wmInfo = default;
|
||||
SDL_VERSION(out wmInfo.version);
|
||||
if (SDL_GetWindowWMInfo(cmd.Window, ref wmInfo) == SDL_TRUE && wmInfo.subsystem == SDL_SYSWM_WINDOWS)
|
||||
{
|
||||
var hWnd = (HWND)wmInfo.info.win.window;
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWLP.GWLP_HWNDPARENT,
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_DestroyWindow(cmd.Window);
|
||||
}
|
||||
|
||||
private (nint window, nint context) CreateSdl2WindowForRenderer(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint ownerWindow)
|
||||
{
|
||||
var windowFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
||||
|
||||
if (spec is { } s)
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_OPENGL;
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, s.Profile == GLContextProfile.Es ? 0 : 1);
|
||||
SDL_GLcontext ctxFlags = 0;
|
||||
#if DEBUG
|
||||
ctxFlags |= SDL_GL_CONTEXT_DEBUG_FLAG;
|
||||
#endif
|
||||
if (s.Profile == GLContextProfile.Core)
|
||||
ctxFlags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
|
||||
|
||||
if (shareContext != 0)
|
||||
{
|
||||
SDL_GL_MakeCurrent(shareWindow, shareContext);
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||||
}
|
||||
|
||||
var profile = s.Profile switch
|
||||
{
|
||||
GLContextProfile.Compatibility => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
GLContextProfile.Core => SDL_GL_CONTEXT_PROFILE_CORE,
|
||||
GLContextProfile.Es => SDL_GL_CONTEXT_PROFILE_ES,
|
||||
_ => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
};
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile);
|
||||
SDL_SetHint("SDL_OPENGL_ES_DRIVER", s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
|
||||
|
||||
if (s.CreationApi == GLContextCreationApi.Egl)
|
||||
WsiShared.EnsureEglAvailable();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
}
|
||||
|
||||
if (parameters.Fullscreen)
|
||||
{
|
||||
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
|
||||
nint window = SDL_CreateWindow(
|
||||
"",
|
||||
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
parameters.Width, parameters.Height,
|
||||
windowFlags);
|
||||
|
||||
if (window == 0)
|
||||
return default;
|
||||
|
||||
nint glContext = SDL_GL_CreateContext(window);
|
||||
if (glContext == 0)
|
||||
{
|
||||
SDL_DestroyWindow(window);
|
||||
return default;
|
||||
}
|
||||
|
||||
// TODO: Monitors, window maximize.
|
||||
// TODO: a bunch of win32 calls for funny window properties I still haven't ported to other platforms.
|
||||
|
||||
// Make sure window thread doesn't keep hold of the GL context.
|
||||
SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
SDL_SysWMinfo info = default;
|
||||
SDL_VERSION(out info.version);
|
||||
if (SDL_GetWindowWMInfo(window, ref info) == SDL_TRUE && info.subsystem == SDL_SYSWM_WINDOWS)
|
||||
WsiShared.WindowsSharedWindowCreate((HWND) info.info.win.window, _cfg);
|
||||
}
|
||||
|
||||
if (parameters.Visible)
|
||||
SDL_ShowWindow(window);
|
||||
|
||||
return (window, glContext);
|
||||
}
|
||||
|
||||
private unsafe Sdl2WindowReg WinThreadSetupWindow(nint window, nint context)
|
||||
{
|
||||
var reg = new Sdl2WindowReg
|
||||
{
|
||||
Sdl2Window = window,
|
||||
GlContext = context,
|
||||
WindowId = SDL_GetWindowID(window),
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
var handle = new WindowHandle(_clyde, reg);
|
||||
reg.Handle = handle;
|
||||
|
||||
SDL_VERSION(out reg.SysWMinfo.version);
|
||||
var res = SDL_GetWindowWMInfo(window, ref reg.SysWMinfo);
|
||||
if (res == SDL_FALSE)
|
||||
_sawmill.Error("Failed to get window WM info: {error}", SDL_GetError());
|
||||
|
||||
// LoadWindowIcon(window);
|
||||
|
||||
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
reg.FramebufferSize = (fbW, fbH);
|
||||
|
||||
reg.WindowScale = GetWindowScale(window);
|
||||
|
||||
SDL_GetWindowSize(window, out var w, out var h);
|
||||
reg.PrevWindowSize = reg.WindowSize = (w, h);
|
||||
|
||||
SDL_GetWindowPosition(window, out var x, out var y);
|
||||
reg.PrevWindowPos = (x, y);
|
||||
|
||||
reg.PixelRatio = reg.FramebufferSize / (Vector2) reg.WindowSize;
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
SendCmd(new CmdWinDestroy(reg.Sdl2Window, window.Owner != null));
|
||||
}
|
||||
|
||||
public void UpdateMainWindowMode()
|
||||
{
|
||||
if (_clyde._mainWindow == null)
|
||||
return;
|
||||
|
||||
var win = (Sdl2WindowReg) _clyde._mainWindow;
|
||||
|
||||
SendCmd(new CmdWinWinSetMode(win.Sdl2Window, _clyde._windowMode));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetMode(CmdWinWinSetMode cmd)
|
||||
{
|
||||
var flags = cmd.Mode switch
|
||||
{
|
||||
WindowMode.Fullscreen => (uint) SDL_WINDOW_FULLSCREEN_DESKTOP,
|
||||
_ => 0u
|
||||
};
|
||||
|
||||
SDL_SetWindowFullscreen(cmd.Window, flags);
|
||||
}
|
||||
|
||||
public void WindowSetTitle(WindowReg window, string title)
|
||||
{
|
||||
SendCmd(new CmdWinSetTitle(WinPtr(window), title));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
|
||||
{
|
||||
SDL_SetWindowTitle(cmd.Window, cmd.Title);
|
||||
}
|
||||
|
||||
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
|
||||
{
|
||||
// API isn't really used and kinda wack, don't feel like figuring it out for SDL2 yet.
|
||||
_sawmill.Warning("WindowSetMonitor not implemented on SDL2");
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
if (cmd.Visible)
|
||||
SDL_ShowWindow(cmd.Window);
|
||||
else
|
||||
SDL_HideWindow(cmd.Window);
|
||||
}
|
||||
|
||||
public void WindowRequestAttention(WindowReg window)
|
||||
{
|
||||
SendCmd(new CmdWinRequestAttention(WinPtr(window)));
|
||||
}
|
||||
|
||||
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
|
||||
{
|
||||
var res = SDL_FlashWindow(cmd.Window, SDL_FLASH_UNTIL_FOCUSED);
|
||||
if (res < 0)
|
||||
_sawmill.Error("Failed to flash window: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public unsafe void WindowSwapBuffers(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl2WindowReg)window;
|
||||
var windowPtr = WinPtr(reg);
|
||||
|
||||
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
|
||||
// This means OpenGL vsync is effectively broken by default on Windows.
|
||||
// We manually sync via DwmFlush(). GLFW does this automatically, SDL2 does not.
|
||||
//
|
||||
// Windows DwmFlush logic partly taken from:
|
||||
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
|
||||
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
|
||||
// See also: https://github.com/libsdl-org/SDL/issues/5797
|
||||
|
||||
var dwmFlush = false;
|
||||
var swapInterval = 0;
|
||||
|
||||
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
|
||||
{
|
||||
BOOL compositing;
|
||||
// 6.2 is Windows 8
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|
||||
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
|
||||
{
|
||||
var curCtx = SDL_GL_GetCurrentContext();
|
||||
var curWin = SDL_GL_GetCurrentWindow();
|
||||
|
||||
if (curCtx != reg.GlContext || curWin != reg.Sdl2Window)
|
||||
throw new InvalidOperationException("Window context must be current!");
|
||||
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
dwmFlush = true;
|
||||
swapInterval = reg.SwapInterval;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(windowPtr);
|
||||
|
||||
if (dwmFlush)
|
||||
{
|
||||
var i = swapInterval;
|
||||
while (i-- > 0)
|
||||
{
|
||||
Windows.DwmFlush();
|
||||
}
|
||||
|
||||
SDL_GL_SetSwapInterval(swapInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public uint? WindowGetX11Id(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
|
||||
return null;
|
||||
|
||||
return (uint?) reg.SysWMinfo.info.x11.window;
|
||||
}
|
||||
|
||||
public nint? WindowGetX11Display(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
|
||||
return null;
|
||||
|
||||
return reg.SysWMinfo.info.x11.display;
|
||||
}
|
||||
|
||||
public nint? WindowGetWin32Window(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
var reg = (Sdl2WindowReg) window;
|
||||
|
||||
if (reg.SysWMinfo.subsystem != SDL_SYSWM_WINDOWS)
|
||||
return null;
|
||||
|
||||
return reg.SysWMinfo.info.win.window;
|
||||
}
|
||||
|
||||
public void RunOnWindowThread(Action a)
|
||||
{
|
||||
SendCmd(new CmdRunAction(a));
|
||||
}
|
||||
|
||||
public void TextInputSetRect(UIBox2i rect)
|
||||
{
|
||||
SendCmd(new CmdTextInputSetRect(new SDL_Rect
|
||||
{
|
||||
x = rect.Left,
|
||||
y = rect.Top,
|
||||
w = rect.Width,
|
||||
h = rect.Height
|
||||
}));
|
||||
}
|
||||
|
||||
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
|
||||
{
|
||||
var rect = cmdTextInput.Rect;
|
||||
SDL_SetTextInputRect(ref rect);
|
||||
}
|
||||
|
||||
public void TextInputStart()
|
||||
{
|
||||
SendCmd(CmdTextInputStart.Instance);
|
||||
}
|
||||
|
||||
private static void WinThreadStartTextInput()
|
||||
{
|
||||
SDL_StartTextInput();
|
||||
}
|
||||
|
||||
public void TextInputStop()
|
||||
{
|
||||
SendCmd(CmdTextInputStop.Instance);
|
||||
}
|
||||
|
||||
private static void WinThreadStopTextInput()
|
||||
{
|
||||
SDL_StopTextInput();
|
||||
}
|
||||
|
||||
public void ClipboardSetText(WindowReg mainWindow, string text)
|
||||
{
|
||||
SendCmd(new CmdSetClipboard(text));
|
||||
}
|
||||
|
||||
private void WinThreadSetClipboard(CmdSetClipboard cmd)
|
||||
{
|
||||
var res = SDL_SetClipboardText(cmd.Text);
|
||||
if (res < 0)
|
||||
_sawmill.Error("Failed to set clipboard text: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public Task<string> ClipboardGetText(WindowReg mainWindow)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
SendCmd(new CmdGetClipboard(tcs));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
|
||||
{
|
||||
cmd.Tcs.TrySetResult(SDL_GetClipboardText());
|
||||
}
|
||||
|
||||
private static Vector2 GetWindowScale(nint window)
|
||||
{
|
||||
// Get scale by diving size in pixels with size in points.
|
||||
SDL_GetWindowSizeInPixels(window, out var pixW, out var pixH);
|
||||
SDL_GetWindowSize(window, out var pointW, out var pointH);
|
||||
|
||||
// Avoiding degenerate cases, not sure if these can actually happen.
|
||||
if (pixW == 0 || pixH == 0 || pointW == 0 || pointH == 0)
|
||||
return new Vector2(1, 1);
|
||||
|
||||
var scaleH = pixW / (float) pointW;
|
||||
var scaleV = pixH / (float) pointH;
|
||||
|
||||
// Round to 5% increments to avoid rounding errors causing constantly different scales.
|
||||
scaleH = MathF.Round(scaleH * 20) / 20;
|
||||
scaleV = MathF.Round(scaleV * 20) / 20;
|
||||
|
||||
return new Vector2(scaleH, scaleV);
|
||||
}
|
||||
|
||||
private static void CheckWindowDisposed(WindowReg reg)
|
||||
{
|
||||
if (reg.IsDisposed)
|
||||
throw new ObjectDisposedException("Window disposed");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nint WinPtr(WindowReg reg) => ((Sdl2WindowReg)reg).Sdl2Window;
|
||||
|
||||
private WindowReg? FindWindow(uint windowId)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (Sdl2WindowReg) windowReg;
|
||||
if (glfwReg.WindowId == windowId)
|
||||
return windowReg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sealed class Sdl2WindowReg : WindowReg
|
||||
{
|
||||
public nint Sdl2Window;
|
||||
public uint WindowId;
|
||||
public nint GlContext;
|
||||
public SDL_SysWMinfo SysWMinfo;
|
||||
#pragma warning disable CS0649
|
||||
public bool Fullscreen;
|
||||
#pragma warning restore CS0649
|
||||
public int SwapInterval;
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_LogCategory;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
private GCHandle _selfGCHandle;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly ISawmill _sawmillSdl2;
|
||||
|
||||
public Sdl2WindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
deps.InjectDependencies(this, true);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("clyde.win");
|
||||
_sawmillSdl2 = _logManager.GetSawmill("clyde.win.sdl2");
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
{
|
||||
InitChannels();
|
||||
|
||||
if (!InitSdl2())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool InitSdl2()
|
||||
{
|
||||
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SDL_LogSetAllPriority(SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
|
||||
SDL_LogSetOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
SDL_SetHint("SDL_WINDOWS_DPI_SCALING", "1");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1");
|
||||
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
|
||||
|
||||
var res = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
|
||||
if (res < 0)
|
||||
{
|
||||
_sawmill.Fatal("Failed to initialize SDL2: {error}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GetVersion(out var version);
|
||||
var videoDriver = SDL_GetCurrentVideoDriver();
|
||||
_sawmill.Debug(
|
||||
"SDL2 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}", version.major, version.minor, version.patch, videoDriver);
|
||||
|
||||
_sdlEventWakeup = SDL_RegisterEvents(1);
|
||||
|
||||
SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE);
|
||||
|
||||
InitCursors();
|
||||
InitMonitors();
|
||||
ReloadKeyMap();
|
||||
|
||||
SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
// SDL defaults to having text input enabled, so we have to manually turn it off in init for consistency.
|
||||
// If we don't, text input will remain enabled *until* the user first leaves a LineEdit/TextEdit.
|
||||
SDL_StopTextInput();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe void Shutdown()
|
||||
{
|
||||
if (_selfGCHandle != default)
|
||||
{
|
||||
SDL_DelEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
_selfGCHandle.Free();
|
||||
}
|
||||
|
||||
SDL_LogSetOutputFunction(null, null);
|
||||
|
||||
if (SDL_WasInit(0) != 0)
|
||||
{
|
||||
_sawmill.Debug("Terminating SDL2");
|
||||
SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDispose()
|
||||
{
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
public void GLMakeContextCurrent(WindowReg? reg)
|
||||
{
|
||||
int res;
|
||||
if (reg is Sdl2WindowReg sdlReg)
|
||||
res = SDL_GL_MakeCurrent(sdlReg.Sdl2Window, sdlReg.GlContext);
|
||||
else
|
||||
res = SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (res < 0)
|
||||
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL_GetError());
|
||||
}
|
||||
|
||||
public void GLSwapInterval(WindowReg reg, int interval)
|
||||
{
|
||||
((Sdl2WindowReg)reg).SwapInterval = interval;
|
||||
SDL_GL_SetSwapInterval(interval);
|
||||
}
|
||||
|
||||
public unsafe void* GLGetProcAddress(string procName)
|
||||
{
|
||||
return (void*) SDL_GL_GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
SDL_GetVersion(out var version);
|
||||
_sawmill.Debug(
|
||||
"SDL2 initialized, version: {major}.{minor}.{patch}", version.major, version.minor, version.patch);
|
||||
|
||||
var videoDriver = SDL_GetCurrentVideoDriver();
|
||||
|
||||
return $"SDL2 {version.major}.{version.minor}.{version.patch} ({videoDriver})";
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
|
||||
private static unsafe void LogOutputFunction(
|
||||
void* userdata,
|
||||
int category,
|
||||
SDL_LogPriority priority,
|
||||
byte* message)
|
||||
{
|
||||
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
var level = priority switch
|
||||
{
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
|
||||
SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
|
||||
_ => LogLevel.Error
|
||||
};
|
||||
|
||||
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
|
||||
if (msg == "That operation is not supported")
|
||||
{
|
||||
obj._sawmillSdl2.Info(Environment.StackTrace);
|
||||
}
|
||||
|
||||
var categoryName = SdlLogCategoryName(category);
|
||||
obj._sawmillSdl2.Log(level, $"[{categoryName}] {msg}");
|
||||
}
|
||||
|
||||
private static string SdlLogCategoryName(int category)
|
||||
{
|
||||
return (SDL_LogCategory) category switch {
|
||||
// @formatter:off
|
||||
SDL_LOG_CATEGORY_APPLICATION => "application",
|
||||
SDL_LOG_CATEGORY_ERROR => "error",
|
||||
SDL_LOG_CATEGORY_ASSERT => "assert",
|
||||
SDL_LOG_CATEGORY_SYSTEM => "system",
|
||||
SDL_LOG_CATEGORY_AUDIO => "audio",
|
||||
SDL_LOG_CATEGORY_VIDEO => "video",
|
||||
SDL_LOG_CATEGORY_RENDER => "render",
|
||||
SDL_LOG_CATEGORY_INPUT => "input",
|
||||
SDL_LOG_CATEGORY_TEST => "test",
|
||||
_ => "unknown"
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,15 @@ using System.Collections.Generic;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_SystemCursor;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
|
||||
private readonly CursorImpl[] _standardCursors = new CursorImpl[(int)StandardCursorShape.CountCursors];
|
||||
@@ -28,31 +27,32 @@ internal partial class Clyde
|
||||
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
|
||||
|
||||
var id = _clyde.AllocRid();
|
||||
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
|
||||
SendCmd(new CmdCursorCreate { Bytes = cloneImg, Hotspot = hotSpot, Cursor = id });
|
||||
|
||||
return new CursorImpl(this, id, false);
|
||||
}
|
||||
|
||||
private unsafe void WinThreadCursorCreate(CmdCursorCreate cmd)
|
||||
{
|
||||
var (img, (hotX, hotY), id) = cmd;
|
||||
using var img = cmd.Bytes;
|
||||
|
||||
fixed (Rgba32* pixPtr = img.GetPixelSpan())
|
||||
{
|
||||
var surface = SDL_CreateRGBSurfaceWithFormatFrom(
|
||||
var surface = SDL.SDL_CreateSurfaceFrom(
|
||||
img.Width,
|
||||
img.Height,
|
||||
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
|
||||
(IntPtr)pixPtr,
|
||||
img.Width, img.Height, 0,
|
||||
sizeof(Rgba32) * img.Width,
|
||||
SDL_PIXELFORMAT_RGBA8888);
|
||||
sizeof(Rgba32) * img.Width);
|
||||
|
||||
var cursor = SDL_CreateColorCursor(surface, hotX, hotY);
|
||||
var cursor = SDL.SDL_CreateColorCursor(surface, cmd.Hotspot.X, cmd.Hotspot.Y);
|
||||
if (cursor == 0)
|
||||
throw new InvalidOperationException("SDL_CreateColorCursor failed");
|
||||
|
||||
_winThreadCursors.Add(id, new WinThreadCursorReg { Ptr = cursor });
|
||||
_winThreadCursors.Add(cmd.Cursor, new WinThreadCursorReg { Ptr = cursor });
|
||||
|
||||
SDL_FreeSurface(surface);
|
||||
SDL.SDL_DestroySurface(surface);
|
||||
}
|
||||
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
public void CursorSet(WindowReg window, ICursor? cursor)
|
||||
@@ -62,7 +62,7 @@ internal partial class Clyde
|
||||
// SDL_SetCursor(NULL) does redraw, not reset.
|
||||
cursor ??= CursorGetStandard(StandardCursorShape.Arrow);
|
||||
|
||||
var reg = (Sdl2WindowReg)window;
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
|
||||
if (reg.Cursor == cursor)
|
||||
return;
|
||||
@@ -74,7 +74,7 @@ internal partial class Clyde
|
||||
throw new ObjectDisposedException(nameof(cursor));
|
||||
|
||||
reg.Cursor = impl;
|
||||
SendCmd(new CmdWinCursorSet(reg.Sdl2Window, impl.Id));
|
||||
SendCmd(new CmdWinCursorSet { Window = reg.Sdl3Window, Cursor = impl.Id });
|
||||
}
|
||||
|
||||
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
|
||||
@@ -83,22 +83,22 @@ internal partial class Clyde
|
||||
var ptr = _winThreadCursors[cmd.Cursor].Ptr;
|
||||
|
||||
// TODO: multi-window??
|
||||
SDL_SetCursor(ptr);
|
||||
SDL.SDL_SetCursor(ptr);
|
||||
}
|
||||
|
||||
private void InitCursors()
|
||||
{
|
||||
Add(StandardCursorShape.Arrow, SDL_SYSTEM_CURSOR_ARROW);
|
||||
Add(StandardCursorShape.IBeam, SDL_SYSTEM_CURSOR_IBEAM);
|
||||
Add(StandardCursorShape.Crosshair, SDL_SYSTEM_CURSOR_CROSSHAIR);
|
||||
Add(StandardCursorShape.Hand, SDL_SYSTEM_CURSOR_HAND);
|
||||
Add(StandardCursorShape.HResize, SDL_SYSTEM_CURSOR_SIZEWE);
|
||||
Add(StandardCursorShape.VResize, SDL_SYSTEM_CURSOR_SIZENS);
|
||||
Add(StandardCursorShape.Arrow, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_DEFAULT);
|
||||
Add(StandardCursorShape.IBeam, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_TEXT);
|
||||
Add(StandardCursorShape.Crosshair, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_CROSSHAIR);
|
||||
Add(StandardCursorShape.Hand, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_POINTER);
|
||||
Add(StandardCursorShape.HResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_EW_RESIZE);
|
||||
Add(StandardCursorShape.VResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NS_RESIZE);
|
||||
|
||||
void Add(StandardCursorShape shape, SDL_SystemCursor sysCursor)
|
||||
void Add(StandardCursorShape shape, SDL.SDL_SystemCursor sysCursor)
|
||||
{
|
||||
var id = _clyde.AllocRid();
|
||||
var cursor = SDL_CreateSystemCursor(sysCursor);
|
||||
var cursor = SDL.SDL_CreateSystemCursor(sysCursor);
|
||||
|
||||
var impl = new CursorImpl(this, id, true);
|
||||
|
||||
@@ -107,13 +107,21 @@ internal partial class Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
|
||||
{
|
||||
if (!_winThreadCursors.TryGetValue(cmd.Cursor, out var cursor))
|
||||
return;
|
||||
|
||||
SDL.SDL_DestroyCursor(cursor.Ptr);
|
||||
}
|
||||
|
||||
private sealed class CursorImpl : ICursor
|
||||
{
|
||||
private readonly bool _standard;
|
||||
public Sdl2WindowingImpl Owner { get; }
|
||||
public Sdl3WindowingImpl Owner { get; }
|
||||
public ClydeHandle Id { get; private set; }
|
||||
|
||||
public CursorImpl(Sdl2WindowingImpl clyde, ClydeHandle id, bool standard)
|
||||
public CursorImpl(Sdl3WindowingImpl clyde, ClydeHandle id, bool standard)
|
||||
{
|
||||
_standard = standard;
|
||||
Owner = clyde;
|
||||
@@ -127,7 +135,7 @@ internal partial class Clyde
|
||||
|
||||
private void DisposeImpl()
|
||||
{
|
||||
Owner.SendCmd(new CmdCursorDestroy(Id));
|
||||
Owner.SendCmd(new CmdCursorDestroy { Cursor = Id });
|
||||
Id = default;
|
||||
}
|
||||
|
||||
@@ -147,9 +155,5 @@ internal partial class Clyde
|
||||
{
|
||||
public nint Ptr;
|
||||
}
|
||||
|
||||
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Map;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static SDL2.SDL;
|
||||
using static SDL2.SDL.SDL_EventType;
|
||||
using static SDL2.SDL.SDL_Keymod;
|
||||
using static SDL2.SDL.SDL_WindowEventID;
|
||||
using SDL3;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using ET = SDL3.SDL.SDL_EventType;
|
||||
using SDL_Keymod = SDL3.SDL.SDL_Keymod;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
public void ProcessEvents(bool single = false)
|
||||
{
|
||||
@@ -47,15 +45,18 @@ internal partial class Clyde
|
||||
case EventWindowCreate wCreate:
|
||||
FinishWindowCreate(wCreate);
|
||||
break;
|
||||
case EventWindow ev:
|
||||
ProcessEventWindow(ev);
|
||||
case EventWindowMisc ev:
|
||||
ProcessEventWindowMisc(ev);
|
||||
break;
|
||||
case EventKey ev:
|
||||
ProcessEventKey(ev);
|
||||
break;
|
||||
case EventWindowSize ev:
|
||||
case EventWindowPixelSize ev:
|
||||
ProcessEventWindowSize(ev);
|
||||
break;
|
||||
case EventWindowContentScale ev:
|
||||
ProcessEventWindowContentScale(ev);
|
||||
break;
|
||||
case EventText ev:
|
||||
ProcessEventText(ev);
|
||||
break;
|
||||
@@ -74,9 +75,6 @@ internal partial class Clyde
|
||||
case EventMonitorSetup ev:
|
||||
ProcessSetupMonitor(ev);
|
||||
break;
|
||||
case EventWindowsFakeV ev:
|
||||
ProcessWindowsFakeV(ev);
|
||||
break;
|
||||
case EventKeyMapChanged:
|
||||
ProcessKeyMapChanged();
|
||||
break;
|
||||
@@ -84,7 +82,7 @@ internal partial class Clyde
|
||||
ProcessEventQuit();
|
||||
break;
|
||||
default:
|
||||
_sawmill.Error($"Unknown SDL2 event type: {evb.GetType().Name}");
|
||||
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +94,7 @@ internal partial class Clyde
|
||||
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
|
||||
}
|
||||
|
||||
private void ProcessEventWindow(EventWindow ev)
|
||||
private void ProcessEventWindowMisc(EventWindowMisc ev)
|
||||
{
|
||||
var window = FindWindow(ev.WindowId);
|
||||
if (window == null)
|
||||
@@ -104,33 +102,38 @@ internal partial class Clyde
|
||||
|
||||
switch (ev.EventId)
|
||||
{
|
||||
case SDL_WINDOWEVENT_CLOSE:
|
||||
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_ENTER:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||
_clyde._currentHoveredWindow = window;
|
||||
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, true));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_LEAVE:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
if (_clyde._currentHoveredWindow == window)
|
||||
_clyde._currentHoveredWindow = null;
|
||||
|
||||
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, false));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_MINIMIZED:
|
||||
case ET.SDL_EVENT_WINDOW_MINIMIZED:
|
||||
window.IsMinimized = true;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_RESTORED:
|
||||
case ET.SDL_EVENT_WINDOW_RESTORED:
|
||||
window.IsMinimized = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
window.IsFocused = true;
|
||||
_clyde.SendWindowFocus(new WindowFocusedEventArgs(true, window.Handle));
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
window.IsFocused = false;
|
||||
_clyde.SendWindowFocus(new WindowFocusedEventArgs(false, window.Handle));
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_MOVED:
|
||||
window.WindowPos = (ev.Data1, ev.Data2);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +156,9 @@ internal partial class Clyde
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
var mods = SDL_GetModState();
|
||||
var button = ConvertSdl2Button(ev.Button);
|
||||
var button = ConvertSdl3Button(ev.Button);
|
||||
var key = Mouse.MouseButtonToKey(button);
|
||||
EmitKeyEvent(key, ev.Type, false, mods, 0);
|
||||
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
|
||||
}
|
||||
|
||||
private void ProcessEventMouseMotion(EventMouseMotion ev)
|
||||
@@ -166,8 +168,7 @@ internal partial class Clyde
|
||||
return;
|
||||
|
||||
var newPos = new Vector2(ev.X, ev.Y) * windowReg.PixelRatio;
|
||||
// SDL2 does give us delta positions, but I'm worried about rounding errors thanks to DPI stuff.
|
||||
var delta = newPos - windowReg.LastMousePos;
|
||||
var delta = new Vector2(ev.XRel, ev.YRel);
|
||||
windowReg.LastMousePos = newPos;
|
||||
|
||||
_clyde._currentHoveredWindow = windowReg;
|
||||
@@ -185,7 +186,7 @@ internal partial class Clyde
|
||||
_clyde.SendTextEditing(new TextEditingEventArgs(ev.Text, ev.Start, ev.Length));
|
||||
}
|
||||
|
||||
private void ProcessEventWindowSize(EventWindowSize ev)
|
||||
private void ProcessEventWindowSize(EventWindowPixelSize ev)
|
||||
{
|
||||
var window = ev.WindowId;
|
||||
var width = ev.Width;
|
||||
@@ -205,28 +206,31 @@ internal partial class Clyde
|
||||
return;
|
||||
|
||||
windowReg.PixelRatio = windowReg.FramebufferSize / (Vector2)windowReg.WindowSize;
|
||||
var newScale = new Vector2(ev.XScale, ev.YScale);
|
||||
|
||||
if (!windowReg.WindowScale.Equals(newScale))
|
||||
{
|
||||
windowReg.WindowScale = newScale;
|
||||
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
_clyde.SendWindowResized(windowReg, oldSize);
|
||||
}
|
||||
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
|
||||
{
|
||||
EmitKeyEvent(ConvertSdl2Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
|
||||
var windowReg = FindWindow(ev.WindowId);
|
||||
if (windowReg == null)
|
||||
return;
|
||||
|
||||
windowReg.WindowScale = new Vector2(ev.Scale, ev.Scale);
|
||||
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Key key, SDL_EventType type, bool repeat, SDL_Keymod mods, SDL_Scancode scancode)
|
||||
private void ProcessEventKey(EventKey ev)
|
||||
{
|
||||
var shift = (mods & KMOD_SHIFT) != 0;
|
||||
var alt = (mods & KMOD_ALT) != 0;
|
||||
var control = (mods & KMOD_CTRL) != 0;
|
||||
var system = (mods & KMOD_GUI) != 0;
|
||||
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
|
||||
}
|
||||
|
||||
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
|
||||
{
|
||||
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
|
||||
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
|
||||
var control = (mods & SDL_Keymod.SDL_KMOD_CTRL) != 0;
|
||||
var system = (mods & SDL_Keymod.SDL_KMOD_GUI) != 0;
|
||||
|
||||
var ev = new KeyEventArgs(
|
||||
key,
|
||||
@@ -236,36 +240,17 @@ internal partial class Clyde
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case SDL_KEYUP:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
case ET.SDL_EVENT_KEY_UP:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
_clyde.SendKeyUp(ev);
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case ET.SDL_EVENT_KEY_DOWN:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
_clyde.SendKeyDown(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessWindowsFakeV(EventWindowsFakeV ev)
|
||||
{
|
||||
var type = (int)ev.Message switch
|
||||
{
|
||||
WM.WM_KEYUP => SDL_KEYUP,
|
||||
WM.WM_KEYDOWN => SDL_KEYDOWN,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var key = (int)ev.WParam switch
|
||||
{
|
||||
0x56 /* V */ => Key.V,
|
||||
VK.VK_CONTROL => Key.Control,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
EmitKeyEvent(key, type, false, 0, 0);
|
||||
}
|
||||
|
||||
private void ProcessKeyMapChanged()
|
||||
{
|
||||
_clyde.SendInputModeChanged();
|
||||
145
Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs
Normal file
145
Robust.Client/Graphics/Clyde/Windowing/Sdl3.FileDialog.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.UserInterface;
|
||||
using SDL3;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
|
||||
{
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
return File.OpenRead(fileName);
|
||||
}
|
||||
|
||||
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(fileName, FileMode.Create), false);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
|
||||
{
|
||||
var props = SDL.SDL_CreateProperties();
|
||||
|
||||
SDL.SDL_DialogFileFilter* filtersAlloc = null;
|
||||
if (filters != null)
|
||||
{
|
||||
filtersAlloc = (SDL.SDL_DialogFileFilter*)NativeMemory.Alloc(
|
||||
(UIntPtr)filters.Groups.Count,
|
||||
(UIntPtr)sizeof(SDL.SDL_DialogFileFilter));
|
||||
|
||||
SDL.SDL_SetNumberProperty(props, SDL.SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, filters.Groups.Count);
|
||||
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (nint)filtersAlloc);
|
||||
|
||||
// All these mallocs aren't gonna win any performance awards, but oh well.
|
||||
for (var i = 0; i < filters.Groups.Count; i++)
|
||||
{
|
||||
var (name, pattern) = ConvertFilterGroup(filters.Groups[i]);
|
||||
filtersAlloc[i].name = StringToNative(name);
|
||||
filtersAlloc[i].pattern = StringToNative(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
var task = ShowFileDialogWithProperties(type, props);
|
||||
|
||||
SDL.SDL_DestroyProperties(props);
|
||||
|
||||
if (filtersAlloc != null)
|
||||
{
|
||||
for (var i = 0; i < filters!.Groups.Count; i++)
|
||||
{
|
||||
var filter = filtersAlloc[i];
|
||||
NativeMemory.Free(filter.name);
|
||||
NativeMemory.Free(filter.pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private static unsafe byte* StringToNative(string str)
|
||||
{
|
||||
var byteCount = Encoding.UTF8.GetByteCount(str);
|
||||
|
||||
var mem = (byte*) NativeMemory.Alloc((nuint)(byteCount + 1));
|
||||
Encoding.UTF8.GetBytes(str, new Span<byte>(mem, byteCount));
|
||||
mem[byteCount] = 0; // null-terminate
|
||||
|
||||
return mem;
|
||||
}
|
||||
|
||||
private (string name, string pattern) ConvertFilterGroup(FileDialogFilters.Group group)
|
||||
{
|
||||
var name = string.Join(", ", group.Extensions.Select(e => $"*.{e}"));
|
||||
var pattern = string.Join(";", group.Extensions);
|
||||
return (name, pattern);
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogWithProperties(int type, uint properties)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
|
||||
var gcHandle = GCHandle.Alloc(new FileDialogState
|
||||
{
|
||||
Parent = this,
|
||||
Tcs = tcs
|
||||
});
|
||||
|
||||
SDL.SDL_ShowFileDialogWithProperties(
|
||||
type,
|
||||
&FileDialogCallback,
|
||||
(void*)GCHandle.ToIntPtr(gcHandle),
|
||||
properties);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe void FileDialogCallback(void* userdata, byte** filelist, int filter)
|
||||
{
|
||||
var stateHandle = GCHandle.FromIntPtr((IntPtr)userdata);
|
||||
var state = (FileDialogState)stateHandle.Target!;
|
||||
stateHandle.Free();
|
||||
|
||||
if (filelist == null)
|
||||
{
|
||||
// Error
|
||||
state.Parent._sawmill.Error("File dialog failed: {error}", SDL.SDL_GetError());
|
||||
state.Tcs.SetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handles null (cancelled/none selected) transparently.
|
||||
var str = Marshal.PtrToStringUTF8((nint) filelist[0]);
|
||||
state.Tcs.SetResult(str);
|
||||
}
|
||||
|
||||
private sealed class FileDialogState
|
||||
{
|
||||
public required Sdl3WindowingImpl Parent;
|
||||
public required TaskCompletionSource<string?> Tcs;
|
||||
}
|
||||
}
|
||||
}
|
||||
225
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs
Normal file
225
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL3;
|
||||
using Key = Robust.Client.Input.Keyboard.Key;
|
||||
using Button = Robust.Client.Input.Mouse.Button;
|
||||
using SC = SDL3.SDL.SDL_Scancode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
// Indices are values of SDL_Scancode
|
||||
private static readonly Key[] KeyMap;
|
||||
private static readonly FrozenDictionary<Key, SC> KeyMapReverse;
|
||||
private static readonly Button[] MouseButtonMap;
|
||||
|
||||
// TODO: to avoid having to ask the windowing thread, key names are cached.
|
||||
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
|
||||
|
||||
private void ReloadKeyMap()
|
||||
{
|
||||
// This may be ran concurrently from the windowing thread.
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
_printableKeyNameMap.Clear();
|
||||
|
||||
// TODO: Validate this is correct in SDL3.
|
||||
|
||||
// List of mappable keys from SDL2's source appears to be:
|
||||
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
|
||||
// (the actual logic is more nuanced, but it appears to match the above)
|
||||
// Comes out to these two ranges:
|
||||
|
||||
for (var k = SC.SDL_SCANCODE_A; k <= SC.SDL_SCANCODE_0; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
for (var k = SC.SDL_SCANCODE_MINUS; k <= SC.SDL_SCANCODE_SLASH; k++)
|
||||
{
|
||||
CacheKey(k);
|
||||
}
|
||||
|
||||
void CacheKey(SC scancode)
|
||||
{
|
||||
var rKey = ConvertSdl3Scancode(scancode);
|
||||
if (rKey == Key.Unknown)
|
||||
return;
|
||||
|
||||
// TODO: SDL_GetKeyFromScancode correct?
|
||||
var name = SDL.SDL_GetKeyName(
|
||||
SDL.SDL_GetKeyFromScancode(scancode, SDL.SDL_Keymod.SDL_KMOD_NONE, false));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_printableKeyNameMap.Add(rKey, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? KeyGetName(Key key)
|
||||
{
|
||||
lock (_printableKeyNameMap)
|
||||
{
|
||||
if (_printableKeyNameMap.TryGetValue(key, out var name))
|
||||
return name;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Key ConvertSdl3Scancode(SC scancode)
|
||||
{
|
||||
return KeyMap[(int) scancode];
|
||||
}
|
||||
|
||||
public static Button ConvertSdl3Button(int button)
|
||||
{
|
||||
return MouseButtonMap[button];
|
||||
}
|
||||
|
||||
static Sdl3WindowingImpl()
|
||||
{
|
||||
MouseButtonMap = new Button[6];
|
||||
MouseButtonMap[SDL.SDL_BUTTON_LEFT] = Button.Left;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_RIGHT] = Button.Right;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_MIDDLE] = Button.Middle;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_X1] = Button.Button4;
|
||||
MouseButtonMap[SDL.SDL_BUTTON_X2] = Button.Button5;
|
||||
|
||||
KeyMap = new Key[(int) SC.SDL_SCANCODE_COUNT];
|
||||
MapKey(SC.SDL_SCANCODE_A, Key.A);
|
||||
MapKey(SC.SDL_SCANCODE_B, Key.B);
|
||||
MapKey(SC.SDL_SCANCODE_C, Key.C);
|
||||
MapKey(SC.SDL_SCANCODE_D, Key.D);
|
||||
MapKey(SC.SDL_SCANCODE_E, Key.E);
|
||||
MapKey(SC.SDL_SCANCODE_F, Key.F);
|
||||
MapKey(SC.SDL_SCANCODE_G, Key.G);
|
||||
MapKey(SC.SDL_SCANCODE_H, Key.H);
|
||||
MapKey(SC.SDL_SCANCODE_I, Key.I);
|
||||
MapKey(SC.SDL_SCANCODE_J, Key.J);
|
||||
MapKey(SC.SDL_SCANCODE_K, Key.K);
|
||||
MapKey(SC.SDL_SCANCODE_L, Key.L);
|
||||
MapKey(SC.SDL_SCANCODE_M, Key.M);
|
||||
MapKey(SC.SDL_SCANCODE_N, Key.N);
|
||||
MapKey(SC.SDL_SCANCODE_O, Key.O);
|
||||
MapKey(SC.SDL_SCANCODE_P, Key.P);
|
||||
MapKey(SC.SDL_SCANCODE_Q, Key.Q);
|
||||
MapKey(SC.SDL_SCANCODE_R, Key.R);
|
||||
MapKey(SC.SDL_SCANCODE_S, Key.S);
|
||||
MapKey(SC.SDL_SCANCODE_T, Key.T);
|
||||
MapKey(SC.SDL_SCANCODE_U, Key.U);
|
||||
MapKey(SC.SDL_SCANCODE_V, Key.V);
|
||||
MapKey(SC.SDL_SCANCODE_W, Key.W);
|
||||
MapKey(SC.SDL_SCANCODE_X, Key.X);
|
||||
MapKey(SC.SDL_SCANCODE_Y, Key.Y);
|
||||
MapKey(SC.SDL_SCANCODE_Z, Key.Z);
|
||||
MapKey(SC.SDL_SCANCODE_0, Key.Num0);
|
||||
MapKey(SC.SDL_SCANCODE_1, Key.Num1);
|
||||
MapKey(SC.SDL_SCANCODE_2, Key.Num2);
|
||||
MapKey(SC.SDL_SCANCODE_3, Key.Num3);
|
||||
MapKey(SC.SDL_SCANCODE_4, Key.Num4);
|
||||
MapKey(SC.SDL_SCANCODE_5, Key.Num5);
|
||||
MapKey(SC.SDL_SCANCODE_6, Key.Num6);
|
||||
MapKey(SC.SDL_SCANCODE_7, Key.Num7);
|
||||
MapKey(SC.SDL_SCANCODE_8, Key.Num8);
|
||||
MapKey(SC.SDL_SCANCODE_9, Key.Num9);
|
||||
MapKey(SC.SDL_SCANCODE_KP_0, Key.NumpadNum0);
|
||||
MapKey(SC.SDL_SCANCODE_KP_1, Key.NumpadNum1);
|
||||
MapKey(SC.SDL_SCANCODE_KP_2, Key.NumpadNum2);
|
||||
MapKey(SC.SDL_SCANCODE_KP_3, Key.NumpadNum3);
|
||||
MapKey(SC.SDL_SCANCODE_KP_4, Key.NumpadNum4);
|
||||
MapKey(SC.SDL_SCANCODE_KP_5, Key.NumpadNum5);
|
||||
MapKey(SC.SDL_SCANCODE_KP_6, Key.NumpadNum6);
|
||||
MapKey(SC.SDL_SCANCODE_KP_7, Key.NumpadNum7);
|
||||
MapKey(SC.SDL_SCANCODE_KP_8, Key.NumpadNum8);
|
||||
MapKey(SC.SDL_SCANCODE_KP_9, Key.NumpadNum9);
|
||||
MapKey(SC.SDL_SCANCODE_ESCAPE, Key.Escape);
|
||||
MapKey(SC.SDL_SCANCODE_LCTRL, Key.Control);
|
||||
MapKey(SC.SDL_SCANCODE_RCTRL, Key.Control);
|
||||
MapKey(SC.SDL_SCANCODE_RSHIFT, Key.Shift);
|
||||
MapKey(SC.SDL_SCANCODE_LSHIFT, Key.Shift);
|
||||
MapKey(SC.SDL_SCANCODE_LALT, Key.Alt);
|
||||
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
|
||||
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
|
||||
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
|
||||
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
|
||||
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
|
||||
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
|
||||
MapKey(SC.SDL_SCANCODE_COMMA, Key.Comma);
|
||||
MapKey(SC.SDL_SCANCODE_PERIOD, Key.Period);
|
||||
MapKey(SC.SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
|
||||
MapKey(SC.SDL_SCANCODE_SLASH, Key.Slash);
|
||||
MapKey(SC.SDL_SCANCODE_BACKSLASH, Key.BackSlash);
|
||||
MapKey(SC.SDL_SCANCODE_GRAVE, Key.Tilde);
|
||||
MapKey(SC.SDL_SCANCODE_EQUALS, Key.Equal);
|
||||
MapKey(SC.SDL_SCANCODE_SPACE, Key.Space);
|
||||
MapKey(SC.SDL_SCANCODE_RETURN, Key.Return);
|
||||
MapKey(SC.SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
|
||||
MapKey(SC.SDL_SCANCODE_BACKSPACE, Key.BackSpace);
|
||||
MapKey(SC.SDL_SCANCODE_TAB, Key.Tab);
|
||||
MapKey(SC.SDL_SCANCODE_PAGEUP, Key.PageUp);
|
||||
MapKey(SC.SDL_SCANCODE_PAGEDOWN, Key.PageDown);
|
||||
MapKey(SC.SDL_SCANCODE_END, Key.End);
|
||||
MapKey(SC.SDL_SCANCODE_HOME, Key.Home);
|
||||
MapKey(SC.SDL_SCANCODE_INSERT, Key.Insert);
|
||||
MapKey(SC.SDL_SCANCODE_DELETE, Key.Delete);
|
||||
MapKey(SC.SDL_SCANCODE_MINUS, Key.Minus);
|
||||
MapKey(SC.SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
|
||||
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
|
||||
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
|
||||
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
|
||||
MapKey(SC.SDL_SCANCODE_UP, Key.Up);
|
||||
MapKey(SC.SDL_SCANCODE_DOWN, Key.Down);
|
||||
MapKey(SC.SDL_SCANCODE_F1, Key.F1);
|
||||
MapKey(SC.SDL_SCANCODE_F2, Key.F2);
|
||||
MapKey(SC.SDL_SCANCODE_F3, Key.F3);
|
||||
MapKey(SC.SDL_SCANCODE_F4, Key.F4);
|
||||
MapKey(SC.SDL_SCANCODE_F5, Key.F5);
|
||||
MapKey(SC.SDL_SCANCODE_F6, Key.F6);
|
||||
MapKey(SC.SDL_SCANCODE_F7, Key.F7);
|
||||
MapKey(SC.SDL_SCANCODE_F8, Key.F8);
|
||||
MapKey(SC.SDL_SCANCODE_F9, Key.F9);
|
||||
MapKey(SC.SDL_SCANCODE_F10, Key.F10);
|
||||
MapKey(SC.SDL_SCANCODE_F11, Key.F11);
|
||||
MapKey(SC.SDL_SCANCODE_F12, Key.F12);
|
||||
MapKey(SC.SDL_SCANCODE_F13, Key.F13);
|
||||
MapKey(SC.SDL_SCANCODE_F14, Key.F14);
|
||||
MapKey(SC.SDL_SCANCODE_F15, Key.F15);
|
||||
MapKey(SC.SDL_SCANCODE_F16, Key.F16);
|
||||
MapKey(SC.SDL_SCANCODE_F17, Key.F17);
|
||||
MapKey(SC.SDL_SCANCODE_F18, Key.F18);
|
||||
MapKey(SC.SDL_SCANCODE_F19, Key.F19);
|
||||
MapKey(SC.SDL_SCANCODE_F20, Key.F20);
|
||||
MapKey(SC.SDL_SCANCODE_F21, Key.F21);
|
||||
MapKey(SC.SDL_SCANCODE_F22, Key.F22);
|
||||
MapKey(SC.SDL_SCANCODE_F23, Key.F23);
|
||||
MapKey(SC.SDL_SCANCODE_F24, Key.F24);
|
||||
MapKey(SC.SDL_SCANCODE_PAUSE, Key.Pause);
|
||||
|
||||
var keyMapReverse = new Dictionary<Key, SC>();
|
||||
|
||||
for (var code = 0; code < KeyMap.Length; code++)
|
||||
{
|
||||
var key = KeyMap[code];
|
||||
if (key != Key.Unknown)
|
||||
keyMapReverse[key] = (SC) code;
|
||||
}
|
||||
|
||||
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void MapKey(SC code, Key key)
|
||||
{
|
||||
KeyMap[(int)code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Monitor.cs
Normal file
133
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Monitor.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SDL3;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
// NOTE: SDL3 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
|
||||
|
||||
private int _nextMonitorId = 1;
|
||||
|
||||
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
|
||||
private readonly Dictionary<int, Sdl3MonitorReg> _monitors = new();
|
||||
|
||||
private unsafe void InitMonitors()
|
||||
{
|
||||
var displayList = (uint*)SDL.SDL_GetDisplays(out var count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
WinThreadSetupMonitor(displayList[i]);
|
||||
}
|
||||
|
||||
SDL.SDL_free((nint)displayList);
|
||||
|
||||
// Needed so that monitor creation events get processed.
|
||||
ProcessEvents();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void WinThreadSetupMonitor(uint displayId)
|
||||
{
|
||||
var id = _nextMonitorId++;
|
||||
|
||||
var name = SDL.SDL_GetDisplayName(displayId);
|
||||
var modePtr = (SDL.SDL_DisplayMode**)SDL.SDL_GetFullscreenDisplayModes(displayId, out var modeCount);
|
||||
var curMode = (SDL.SDL_DisplayMode*)SDL.SDL_GetCurrentDisplayMode(displayId);
|
||||
var modes = new VideoMode[modeCount];
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
modes[i] = ConvertVideoMode(in *modePtr[i]);
|
||||
}
|
||||
|
||||
SDL.SDL_free((nint)modePtr);
|
||||
|
||||
_winThreadMonitors.Add(id, new WinThreadMonitorReg { DisplayId = displayId });
|
||||
|
||||
if (SDL.SDL_GetPrimaryDisplay() == displayId)
|
||||
_clyde._primaryMonitorId = id;
|
||||
|
||||
SendEvent(new EventMonitorSetup
|
||||
{
|
||||
Id = id,
|
||||
DisplayId = displayId,
|
||||
Name = name,
|
||||
AllModes = modes,
|
||||
CurrentMode = ConvertVideoMode(in *curMode),
|
||||
});
|
||||
}
|
||||
|
||||
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = (ushort)mode.w,
|
||||
Height = (ushort)mode.h,
|
||||
RefreshRate = (ushort)mode.refresh_rate,
|
||||
// TODO: set bits count based on format (I'm lazy)
|
||||
RedBits = 8,
|
||||
GreenBits = 8,
|
||||
BlueBits = 8,
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessSetupMonitor(EventMonitorSetup ev)
|
||||
{
|
||||
var impl = new MonitorHandle(
|
||||
ev.Id,
|
||||
ev.Name,
|
||||
(ev.CurrentMode.Width, ev.CurrentMode.Height),
|
||||
ev.CurrentMode.RefreshRate,
|
||||
ev.AllModes);
|
||||
|
||||
_clyde._monitorHandles.Add(ev.Id, impl);
|
||||
_monitors[ev.Id] = new Sdl3MonitorReg
|
||||
{
|
||||
DisplayId = ev.DisplayId,
|
||||
Handle = impl
|
||||
};
|
||||
}
|
||||
|
||||
private void WinThreadDestroyMonitor(uint displayId)
|
||||
{
|
||||
var monitorId = GetMonitorIdFromDisplayId(displayId);
|
||||
if (monitorId == 0)
|
||||
return;
|
||||
|
||||
_winThreadMonitors.Remove(monitorId);
|
||||
SendEvent(new EventMonitorDestroy { Id = monitorId });
|
||||
}
|
||||
|
||||
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
|
||||
{
|
||||
_monitors.Remove(ev.Id);
|
||||
_clyde._monitorHandles.Remove(ev.Id);
|
||||
}
|
||||
|
||||
private int GetMonitorIdFromDisplayId(uint displayId)
|
||||
{
|
||||
foreach (var (id, monitorReg) in _winThreadMonitors)
|
||||
{
|
||||
if (monitorReg.DisplayId == displayId)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed class Sdl3MonitorReg : MonitorReg
|
||||
{
|
||||
public uint DisplayId;
|
||||
}
|
||||
|
||||
private sealed class WinThreadMonitorReg
|
||||
{
|
||||
public uint DisplayId;
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Robust.Client/Graphics/Clyde/Windowing/Sdl3.RawEvent.cs
Normal file
282
Robust.Client/Graphics/Clyde/Windowing/Sdl3.RawEvent.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using SDL3;
|
||||
using ET = SDL3.SDL.SDL_EventType;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe byte EventWatch(void* userdata, SDL.SDL_Event* sdlevent)
|
||||
{
|
||||
var obj = (Sdl3WindowingImpl)GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
obj.ProcessSdl3Event(in *sdlevent);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ProcessSdl3Event(in SDL.SDL_Event ev)
|
||||
{
|
||||
switch ((ET)ev.type)
|
||||
{
|
||||
case ET.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
ProcessSdl3EventWindowPixelSizeChanged(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
|
||||
ProcessSdl3EventWindowDisplayScaleChanged(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
|
||||
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
case ET.SDL_EVENT_WINDOW_MINIMIZED:
|
||||
case ET.SDL_EVENT_WINDOW_RESTORED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
case ET.SDL_EVENT_WINDOW_MOVED:
|
||||
ProcessSdl3EventWindowMisc(in ev.window);
|
||||
break;
|
||||
case ET.SDL_EVENT_KEY_DOWN:
|
||||
case ET.SDL_EVENT_KEY_UP:
|
||||
ProcessSdl3KeyEvent(in ev.key);
|
||||
break;
|
||||
case ET.SDL_EVENT_TEXT_INPUT:
|
||||
ProcessSdl3EventTextInput(in ev.text);
|
||||
break;
|
||||
case ET.SDL_EVENT_TEXT_EDITING:
|
||||
ProcessSdl3EventTextEditing(in ev.edit);
|
||||
break;
|
||||
case ET.SDL_EVENT_KEYMAP_CHANGED:
|
||||
ProcessSdl3EventKeyMapChanged();
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_MOTION:
|
||||
ProcessSdl3EventMouseMotion(in ev.motion);
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
ProcessSdl3EventMouseButton(in ev.button);
|
||||
break;
|
||||
case ET.SDL_EVENT_MOUSE_WHEEL:
|
||||
ProcessSdl3EventMouseWheel(in ev.wheel);
|
||||
break;
|
||||
case ET.SDL_EVENT_DISPLAY_ADDED:
|
||||
WinThreadSetupMonitor(ev.display.displayID);
|
||||
break;
|
||||
case ET.SDL_EVENT_DISPLAY_REMOVED:
|
||||
WinThreadDestroyMonitor(ev.display.displayID);
|
||||
break;
|
||||
case ET.SDL_EVENT_QUIT:
|
||||
ProcessSdl3EventQuit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventQuit()
|
||||
{
|
||||
SendEvent(new EventQuit());
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseWheel(in SDL.SDL_MouseWheelEvent ev)
|
||||
{
|
||||
SendEvent(new EventWheel { WindowId = ev.windowID, XOffset = ev.x, YOffset = ev.y });
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseButton(in SDL.SDL_MouseButtonEvent ev)
|
||||
{
|
||||
var mods = SDL.SDL_GetModState();
|
||||
SendEvent(new EventMouseButton
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Type = ev.type,
|
||||
Button = ev.button,
|
||||
Mods = mods
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventMouseMotion(in SDL.SDL_MouseMotionEvent ev)
|
||||
{
|
||||
SendEvent(new EventMouseMotion
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
X = ev.x,
|
||||
Y = ev.y,
|
||||
XRel = ev.xrel,
|
||||
YRel = ev.yrel
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl3EventTextInput(in SDL.SDL_TextInputEvent ev)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
|
||||
SendEvent(new EventText { WindowId = ev.windowID, Text = str });
|
||||
}
|
||||
|
||||
private unsafe void ProcessSdl3EventTextEditing(in SDL.SDL_TextEditingEvent ev)
|
||||
{
|
||||
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
|
||||
SendEvent(new EventTextEditing
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Text = str,
|
||||
Start = ev.start,
|
||||
Length = ev.length
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventKeyMapChanged()
|
||||
{
|
||||
ReloadKeyMap();
|
||||
SendEvent(new EventKeyMapChanged());
|
||||
}
|
||||
|
||||
private void ProcessSdl3KeyEvent(in SDL.SDL_KeyboardEvent ev)
|
||||
{
|
||||
SendEvent(new EventKey
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Scancode = ev.scancode,
|
||||
Type = ev.type,
|
||||
Repeat = ev.repeat,
|
||||
Mods = ev.mod,
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowPixelSizeChanged(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL.SDL_GetWindowFromID(ev.windowID);
|
||||
SDL.SDL_GetWindowSize(window, out var width, out var height);
|
||||
var fbW = ev.data1;
|
||||
var fbH = ev.data2;
|
||||
|
||||
SendEvent(new EventWindowPixelSize
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
Width = width,
|
||||
Height = height,
|
||||
FramebufferWidth = fbW,
|
||||
FramebufferHeight = fbH,
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowDisplayScaleChanged(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
var window = SDL.SDL_GetWindowFromID(ev.windowID);
|
||||
var scale = SDL.SDL_GetWindowDisplayScale(window);
|
||||
|
||||
SendEvent(new EventWindowContentScale { WindowId = ev.windowID, Scale = scale });
|
||||
}
|
||||
|
||||
private void ProcessSdl3EventWindowMisc(in SDL.SDL_WindowEvent ev)
|
||||
{
|
||||
SendEvent(new EventWindowMisc
|
||||
{
|
||||
WindowId = ev.windowID,
|
||||
EventId = ev.type,
|
||||
Data1 = ev.data1,
|
||||
Data2 = ev.data2
|
||||
});
|
||||
}
|
||||
|
||||
private abstract class EventBase;
|
||||
|
||||
private sealed class EventWindowCreate : EventBase
|
||||
{
|
||||
public required Sdl3WindowCreateResult Result;
|
||||
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
|
||||
}
|
||||
|
||||
private sealed class EventKey : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public SDL.SDL_Scancode Scancode;
|
||||
public ET Type;
|
||||
public bool Repeat;
|
||||
public SDL.SDL_Keymod Mods;
|
||||
}
|
||||
|
||||
private sealed class EventMouseMotion : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float X;
|
||||
public float Y;
|
||||
public float XRel;
|
||||
public float YRel;
|
||||
}
|
||||
|
||||
private sealed class EventMouseButton : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public ET Type;
|
||||
public byte Button;
|
||||
public SDL.SDL_Keymod Mods;
|
||||
}
|
||||
|
||||
private sealed class EventText : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public required string Text;
|
||||
}
|
||||
|
||||
private sealed class EventTextEditing : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public required string Text;
|
||||
public int Start;
|
||||
public int Length;
|
||||
}
|
||||
|
||||
private sealed class EventWindowPixelSize : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public int FramebufferWidth;
|
||||
public int FramebufferHeight;
|
||||
}
|
||||
|
||||
private sealed class EventWindowContentScale : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float Scale;
|
||||
}
|
||||
|
||||
private sealed class EventWheel : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public float XOffset;
|
||||
public float YOffset;
|
||||
}
|
||||
|
||||
// SDL_WindowEvents that don't need any handling on the window thread itself.
|
||||
private sealed class EventWindowMisc : EventBase
|
||||
{
|
||||
public uint WindowId;
|
||||
public ET EventId;
|
||||
public int Data1;
|
||||
public int Data2;
|
||||
}
|
||||
|
||||
private sealed class EventMonitorSetup : EventBase
|
||||
{
|
||||
public int Id;
|
||||
public uint DisplayId;
|
||||
public required string Name;
|
||||
public VideoMode CurrentMode;
|
||||
public required VideoMode[] AllModes;
|
||||
}
|
||||
|
||||
private sealed class EventMonitorDestroy : EventBase
|
||||
{
|
||||
public int Id;
|
||||
}
|
||||
|
||||
private sealed class EventKeyMapChanged : EventBase;
|
||||
|
||||
private sealed class EventQuit : EventBase;
|
||||
}
|
||||
}
|
||||
654
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs
Normal file
654
Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs
Normal file
@@ -0,0 +1,654 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using SDL3;
|
||||
using TerraFX.Interop.Windows;
|
||||
using TerraFX.Interop.Xlib;
|
||||
using BOOL = TerraFX.Interop.Windows.BOOL;
|
||||
using Windows = TerraFX.Interop.Windows.Windows;
|
||||
using GLAttr = SDL3.SDL.SDL_GLAttr;
|
||||
using X11Window = TerraFX.Interop.Xlib.Window;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private int _nextWindowId = 1;
|
||||
|
||||
public (WindowReg?, string? error) WindowCreate(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
WindowReg? share,
|
||||
WindowReg? owner)
|
||||
{
|
||||
nint shareWindow = 0;
|
||||
nint shareContext = 0;
|
||||
if (share is Sdl3WindowReg shareReg)
|
||||
{
|
||||
shareWindow = shareReg.Sdl3Window;
|
||||
shareContext = shareReg.GlContext;
|
||||
}
|
||||
|
||||
nint ownerPtr = 0;
|
||||
if (owner is Sdl3WindowReg ownerReg)
|
||||
ownerPtr = ownerReg.Sdl3Window;
|
||||
|
||||
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
|
||||
|
||||
// Block the main thread (to avoid stuff like texture uploads being problematic).
|
||||
WaitWindowCreate(task);
|
||||
|
||||
#pragma warning disable RA0004
|
||||
// Block above ensured task is done, this is safe.
|
||||
var result = task.Result;
|
||||
#pragma warning restore RA0004
|
||||
if (result.Reg != null)
|
||||
{
|
||||
result.Reg.Owner = result.Reg.Handle;
|
||||
}
|
||||
|
||||
return (result.Reg, result.Error);
|
||||
}
|
||||
|
||||
private void WaitWindowCreate(Task<Sdl3WindowCreateResult> windowTask)
|
||||
{
|
||||
while (!windowTask.IsCompleted)
|
||||
{
|
||||
// Keep processing events until the window task gives either an error or success.
|
||||
WaitEvents();
|
||||
ProcessEvents(single: true);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Sdl3WindowCreateResult> SharedWindowCreate(
|
||||
GLContextSpec? glSpec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint owner)
|
||||
{
|
||||
//
|
||||
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
|
||||
// I originally wanted this to be async so we could avoid blocking the main thread
|
||||
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
|
||||
// This doesn't *work* because
|
||||
// we have to release the GL context while the shared context is being created.
|
||||
// (at least on WGL, I didn't test other platforms and I don't care to.)
|
||||
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
|
||||
// because rendering would be locked up *anyways*.
|
||||
//
|
||||
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
|
||||
// and I should get on either Veldrid or Vulkan some time.
|
||||
// Probably Veldrid tbh.
|
||||
//
|
||||
|
||||
// Yes we ping-pong this TCS through the window thread and back, deal with it.
|
||||
var tcs = new TaskCompletionSource<Sdl3WindowCreateResult>();
|
||||
SendCmd(new CmdWinCreate
|
||||
{
|
||||
GLSpec = glSpec,
|
||||
Parameters = parameters,
|
||||
ShareWindow = shareWindow,
|
||||
ShareContext = shareContext,
|
||||
OwnerWindow = owner,
|
||||
Tcs = tcs
|
||||
});
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void FinishWindowCreate(EventWindowCreate ev)
|
||||
{
|
||||
ev.Tcs.TrySetResult(ev.Result);
|
||||
}
|
||||
|
||||
private void WinThreadWinCreate(CmdWinCreate cmd)
|
||||
{
|
||||
var (window, context) = CreateSdl3WindowForRenderer(
|
||||
cmd.GLSpec,
|
||||
cmd.Parameters,
|
||||
cmd.ShareWindow,
|
||||
cmd.ShareContext,
|
||||
cmd.OwnerWindow);
|
||||
|
||||
if (window == 0)
|
||||
{
|
||||
var err = SDL.SDL_GetError();
|
||||
|
||||
SendEvent(new EventWindowCreate
|
||||
{
|
||||
Result = new Sdl3WindowCreateResult { Error = err },
|
||||
Tcs = cmd.Tcs
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't invoke the TCS directly from the windowing thread because:
|
||||
// * it'd hit the synchronization context,
|
||||
// which would make (blocking) main window init more annoying.
|
||||
// * it'd not be synchronized to other incoming window events correctly which might be icky.
|
||||
// So we send the TCS back to the game thread
|
||||
// which processes events in the correct order and has better control of stuff during init.
|
||||
var reg = WinThreadSetupWindow(window, context);
|
||||
|
||||
SendEvent(new EventWindowCreate
|
||||
{
|
||||
Result = new Sdl3WindowCreateResult { Reg = reg },
|
||||
Tcs = cmd.Tcs
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
|
||||
{
|
||||
SDL.SDL_DestroyWindow(cmd.Window);
|
||||
}
|
||||
|
||||
private (nint window, nint context) CreateSdl3WindowForRenderer(
|
||||
GLContextSpec? spec,
|
||||
WindowCreateParameters parameters,
|
||||
nint shareWindow,
|
||||
nint shareContext,
|
||||
nint ownerWindow)
|
||||
{
|
||||
var createProps = SDL.SDL_CreateProperties();
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
|
||||
|
||||
if (spec is { } s)
|
||||
{
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_RED_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_GREEN_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_BLUE_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_ALPHA_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL.SDL_GL_SetAttribute(
|
||||
GLAttr.SDL_GL_FRAMEBUFFER_SRGB_CAPABLE,
|
||||
s.Profile == GLContextProfile.Es ? 0 : 1);
|
||||
int ctxFlags = 0;
|
||||
#if DEBUG
|
||||
ctxFlags |= SDL.SDL_GL_CONTEXT_DEBUG_FLAG;
|
||||
#endif
|
||||
if (s.Profile == GLContextProfile.Core)
|
||||
ctxFlags |= SDL.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
|
||||
|
||||
if (shareContext != 0)
|
||||
{
|
||||
SDL.SDL_GL_MakeCurrent(shareWindow, shareContext);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||||
}
|
||||
|
||||
var profile = s.Profile switch
|
||||
{
|
||||
GLContextProfile.Compatibility => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
GLContextProfile.Core => SDL.SDL_GL_CONTEXT_PROFILE_CORE,
|
||||
GLContextProfile.Es => SDL.SDL_GL_CONTEXT_PROFILE_ES,
|
||||
_ => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
|
||||
};
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, profile);
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_OPENGL_ES_DRIVER, s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
|
||||
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
|
||||
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
|
||||
|
||||
if (s.CreationApi == GLContextCreationApi.Egl)
|
||||
WsiShared.EnsureEglAvailable();
|
||||
}
|
||||
|
||||
if (parameters.Fullscreen)
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
|
||||
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
|
||||
|
||||
if (ownerWindow != 0)
|
||||
{
|
||||
SDL.SDL_SetPointerProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_PARENT_POINTER, ownerWindow);
|
||||
|
||||
if (parameters.StartupLocation == WindowStartupLocation.CenterOwner)
|
||||
{
|
||||
SDL.SDL_GetWindowSize(ownerWindow, out var parentW, out var parentH);
|
||||
SDL.SDL_GetWindowPosition(ownerWindow, out var parentX, out var parentY);
|
||||
|
||||
SDL.SDL_SetNumberProperty(
|
||||
createProps,
|
||||
SDL.SDL_PROP_WINDOW_CREATE_X_NUMBER,
|
||||
parentX + (parentW - parameters.Width) / 2);
|
||||
SDL.SDL_SetNumberProperty(
|
||||
createProps,
|
||||
SDL.SDL_PROP_WINDOW_CREATE_Y_NUMBER,
|
||||
parentY + (parentH - parameters.Height) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, parameters.Width);
|
||||
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, parameters.Height);
|
||||
SDL.SDL_SetStringProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_TITLE_STRING, parameters.Title);
|
||||
|
||||
// ---> CREATE <---
|
||||
var window = SDL.SDL_CreateWindowWithProperties(createProps);
|
||||
|
||||
SDL.SDL_DestroyProperties(createProps);
|
||||
|
||||
if (window == 0)
|
||||
return default;
|
||||
|
||||
nint glContext = SDL.SDL_GL_CreateContext(window);
|
||||
if (glContext == 0)
|
||||
{
|
||||
SDL.SDL_DestroyWindow(window);
|
||||
return default;
|
||||
}
|
||||
|
||||
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
|
||||
{
|
||||
var props = SDL.SDL_GetWindowProperties(window);
|
||||
switch (_videoDriver)
|
||||
{
|
||||
case SdlVideoDriver.Windows:
|
||||
{
|
||||
var hWnd = SDL.SDL_GetPointerProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
|
||||
0);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsWindows((HWND)hWnd);
|
||||
break;
|
||||
}
|
||||
case SdlVideoDriver.X11:
|
||||
unsafe
|
||||
{
|
||||
var x11Display = (Display*)SDL.SDL_GetPointerProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
|
||||
0);
|
||||
var x11Window = (X11Window)SDL.SDL_GetNumberProperty(
|
||||
props,
|
||||
SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
|
||||
0);
|
||||
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
_sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this video driver");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Monitors, window maximize.
|
||||
|
||||
// Make sure window thread doesn't keep hold of the GL context.
|
||||
SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (parameters.Visible)
|
||||
SDL.SDL_ShowWindow(window);
|
||||
|
||||
return (window, glContext);
|
||||
}
|
||||
|
||||
private Sdl3WindowReg WinThreadSetupWindow(nint window, nint context)
|
||||
{
|
||||
var reg = new Sdl3WindowReg
|
||||
{
|
||||
Sdl3Window = window,
|
||||
GlContext = context,
|
||||
WindowId = SDL.SDL_GetWindowID(window),
|
||||
Id = new WindowId(_nextWindowId++)
|
||||
};
|
||||
var handle = new WindowHandle(_clyde, reg);
|
||||
reg.Handle = handle;
|
||||
|
||||
var windowProps = SDL.SDL_GetWindowProperties(window);
|
||||
switch (_videoDriver)
|
||||
{
|
||||
case SdlVideoDriver.Windows:
|
||||
reg.WindowsHwnd = SDL.SDL_GetPointerProperty(
|
||||
windowProps,
|
||||
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
|
||||
0);
|
||||
break;
|
||||
case SdlVideoDriver.X11:
|
||||
reg.X11Display = SDL.SDL_GetPointerProperty(
|
||||
windowProps,
|
||||
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
|
||||
0);
|
||||
reg.X11Id = (uint)SDL.SDL_GetNumberProperty(windowProps, SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
AssignWindowIconToWindow(window);
|
||||
|
||||
SDL.SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
|
||||
reg.FramebufferSize = (fbW, fbH);
|
||||
|
||||
var scale = SDL.SDL_GetWindowDisplayScale(window);
|
||||
reg.WindowScale = new Vector2(scale, scale);
|
||||
|
||||
SDL.SDL_GetWindowSize(window, out var w, out var h);
|
||||
reg.PrevWindowSize = reg.WindowSize = (w, h);
|
||||
|
||||
SDL.SDL_GetWindowPosition(window, out var x, out var y);
|
||||
reg.PrevWindowPos = reg.WindowPos = (x, y);
|
||||
|
||||
reg.PixelRatio = reg.FramebufferSize / (Vector2)reg.WindowSize;
|
||||
|
||||
return reg;
|
||||
}
|
||||
|
||||
public void WindowDestroy(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
SendCmd(new CmdWinDestroy
|
||||
{
|
||||
Window = reg.Sdl3Window,
|
||||
HadOwner = window.Owner != null
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateMainWindowMode()
|
||||
{
|
||||
if (_clyde._mainWindow == null)
|
||||
return;
|
||||
|
||||
var win = (Sdl3WindowReg)_clyde._mainWindow;
|
||||
|
||||
if (_clyde._windowMode == WindowMode.Fullscreen)
|
||||
{
|
||||
win.PrevWindowSize = win.WindowSize;
|
||||
win.PrevWindowPos = win.WindowPos;
|
||||
|
||||
SendCmd(new CmdWinWinSetFullscreen
|
||||
{
|
||||
Window = win.Sdl3Window,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SendCmd(new CmdWinSetWindowed
|
||||
{
|
||||
Window = win.Sdl3Window,
|
||||
Width = win.PrevWindowSize.X,
|
||||
Height = win.PrevWindowSize.Y,
|
||||
PosX = win.PrevWindowPos.X,
|
||||
PosY = win.PrevWindowPos.Y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetFullscreen(CmdWinWinSetFullscreen cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowFullscreen(cmd.Window, true);
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetWindowed(CmdWinSetWindowed cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowFullscreen(cmd.Window, false);
|
||||
SDL.SDL_SetWindowSize(cmd.Window, cmd.Width, cmd.Height);
|
||||
SDL.SDL_SetWindowPosition(cmd.Window, cmd.PosX, cmd.PosY);
|
||||
}
|
||||
|
||||
public void WindowSetTitle(WindowReg window, string title)
|
||||
{
|
||||
SendCmd(new CmdWinSetTitle
|
||||
{
|
||||
Window = WinPtr(window),
|
||||
Title = title,
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowTitle(cmd.Window, cmd.Title);
|
||||
}
|
||||
|
||||
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
|
||||
{
|
||||
// API isn't really used and kinda wack, don't feel like figuring it out for SDL3 yet.
|
||||
_sawmill.Warning("WindowSetMonitor not implemented on SDL3");
|
||||
}
|
||||
|
||||
public void WindowSetSize(WindowReg window, Vector2i size)
|
||||
{
|
||||
SendCmd(new CmdWinSetSize { Window = WinPtr(window), W = size.X, H = size.Y });
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
window.IsVisible = visible;
|
||||
SendCmd(new CmdWinSetVisible { Window = WinPtr(window), Visible = visible });
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
|
||||
{
|
||||
SDL.SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
|
||||
{
|
||||
if (cmd.Visible)
|
||||
SDL.SDL_ShowWindow(cmd.Window);
|
||||
else
|
||||
SDL.SDL_HideWindow(cmd.Window);
|
||||
}
|
||||
|
||||
public void WindowRequestAttention(WindowReg window)
|
||||
{
|
||||
SendCmd(new CmdWinRequestAttention { Window = WinPtr(window) });
|
||||
}
|
||||
|
||||
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
|
||||
{
|
||||
var res = SDL.SDL_FlashWindow(cmd.Window, SDL.SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED);
|
||||
if (!res)
|
||||
_sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public unsafe void WindowSwapBuffers(WindowReg window)
|
||||
{
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
var windowPtr = WinPtr(reg);
|
||||
|
||||
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
|
||||
// This means OpenGL vsync is effectively broken by default on Windows.
|
||||
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
|
||||
//
|
||||
// Windows DwmFlush logic partly taken from:
|
||||
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
|
||||
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
|
||||
// See also: https://github.com/libsdl-org/SDL/issues/5797
|
||||
|
||||
var dwmFlush = false;
|
||||
var swapInterval = 0;
|
||||
|
||||
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
|
||||
{
|
||||
BOOL compositing;
|
||||
// 6.2 is Windows 8
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|
||||
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
|
||||
{
|
||||
var curCtx = SDL.SDL_GL_GetCurrentContext();
|
||||
var curWin = SDL.SDL_GL_GetCurrentWindow();
|
||||
|
||||
if (curCtx != reg.GlContext || curWin != reg.Sdl3Window)
|
||||
throw new InvalidOperationException("Window context must be current!");
|
||||
|
||||
SDL.SDL_GL_SetSwapInterval(0);
|
||||
dwmFlush = true;
|
||||
swapInterval = reg.SwapInterval;
|
||||
}
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SwapWindow(windowPtr);
|
||||
|
||||
if (dwmFlush)
|
||||
{
|
||||
var i = swapInterval;
|
||||
while (i-- > 0)
|
||||
{
|
||||
Windows.DwmFlush();
|
||||
}
|
||||
|
||||
SDL.SDL_GL_SetSwapInterval(swapInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public uint? WindowGetX11Id(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.X11)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.X11Id;
|
||||
}
|
||||
|
||||
public nint? WindowGetX11Display(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.X11)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.X11Display;
|
||||
}
|
||||
|
||||
public nint? WindowGetWin32Window(WindowReg window)
|
||||
{
|
||||
CheckWindowDisposed(window);
|
||||
|
||||
if (_videoDriver != SdlVideoDriver.Windows)
|
||||
return null;
|
||||
|
||||
var reg = (Sdl3WindowReg)window;
|
||||
return reg.WindowsHwnd;
|
||||
}
|
||||
|
||||
public void RunOnWindowThread(Action a)
|
||||
{
|
||||
SendCmd(new CmdRunAction { Action = a });
|
||||
}
|
||||
|
||||
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
|
||||
{
|
||||
SendCmd(new CmdTextInputSetRect
|
||||
{
|
||||
Window = WinPtr(reg),
|
||||
Rect = new SDL.SDL_Rect
|
||||
{
|
||||
x = rect.Left,
|
||||
y = rect.Top,
|
||||
w = rect.Width,
|
||||
h = rect.Height
|
||||
},
|
||||
Cursor = cursor
|
||||
});
|
||||
}
|
||||
|
||||
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
|
||||
{
|
||||
var rect = cmdTextInput.Rect;
|
||||
SDL.SDL_SetTextInputArea(cmdTextInput.Window, ref rect, cmdTextInput.Cursor);
|
||||
}
|
||||
|
||||
public void TextInputStart(WindowReg reg)
|
||||
{
|
||||
SendCmd(new CmdTextInputStart { Window = WinPtr(reg) });
|
||||
}
|
||||
|
||||
private static void WinThreadStartTextInput(CmdTextInputStart cmd)
|
||||
{
|
||||
SDL.SDL_StartTextInput(cmd.Window);
|
||||
}
|
||||
|
||||
public void TextInputStop(WindowReg reg)
|
||||
{
|
||||
SendCmd(new CmdTextInputStop { Window = WinPtr(reg) });
|
||||
}
|
||||
|
||||
private static void WinThreadStopTextInput(CmdTextInputStop cmd)
|
||||
{
|
||||
SDL.SDL_StopTextInput(cmd.Window);
|
||||
}
|
||||
|
||||
public void ClipboardSetText(WindowReg mainWindow, string text)
|
||||
{
|
||||
SendCmd(new CmdSetClipboard { Text = text });
|
||||
}
|
||||
|
||||
private void WinThreadSetClipboard(CmdSetClipboard cmd)
|
||||
{
|
||||
var res = SDL.SDL_SetClipboardText(cmd.Text);
|
||||
if (res)
|
||||
_sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public Task<string> ClipboardGetText(WindowReg mainWindow)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
SendCmd(new CmdGetClipboard { Tcs = tcs });
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
|
||||
{
|
||||
cmd.Tcs.TrySetResult(SDL.SDL_GetClipboardText());
|
||||
}
|
||||
|
||||
private static void CheckWindowDisposed(WindowReg reg)
|
||||
{
|
||||
if (reg.IsDisposed)
|
||||
throw new ObjectDisposedException("Window disposed");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nint WinPtr(WindowReg reg) => ((Sdl3WindowReg)reg).Sdl3Window;
|
||||
|
||||
private WindowReg? FindWindow(uint windowId)
|
||||
{
|
||||
foreach (var windowReg in _clyde._windows)
|
||||
{
|
||||
var glfwReg = (Sdl3WindowReg)windowReg;
|
||||
if (glfwReg.WindowId == windowId)
|
||||
return windowReg;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private sealed class Sdl3WindowReg : WindowReg
|
||||
{
|
||||
public nint Sdl3Window;
|
||||
public uint WindowId;
|
||||
public nint GlContext;
|
||||
#pragma warning disable CS0649
|
||||
public bool Fullscreen;
|
||||
#pragma warning restore CS0649
|
||||
public int SwapInterval;
|
||||
|
||||
// Kept around to avoid it being GCd.
|
||||
public CursorImpl? Cursor;
|
||||
|
||||
public nint WindowsHwnd;
|
||||
public nint X11Display;
|
||||
public uint X11Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
Robust.Client/Graphics/Clyde/Windowing/Sdl3.WindowIcons.cs
Normal file
105
Robust.Client/Graphics/Clyde/Windowing/Sdl3.WindowIcons.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Utility;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed unsafe partial class Sdl3WindowingImpl
|
||||
{
|
||||
// Experimentally on my system, SM_CXICON is 32.
|
||||
// I doubt MS is ever changing that, so...
|
||||
// I wish SDL would take care of this instead of us having to figure out what the "main" icon is. Ugh.
|
||||
private const int MainWindowIconSize = 32;
|
||||
|
||||
// Writing this out like this makes me realize we're spending multiple hundred KBs on storing the window icon.
|
||||
// You know, come to think about it, what if we used LZ4 or Zstd to compress the window icon stored here?
|
||||
// This is absolutely not worth the optimization but hilarious for me to think about.
|
||||
|
||||
// The surface used for the window icon.
|
||||
// This may store additional surfaces as alternate forms.
|
||||
private nint _windowIconSurface;
|
||||
// The data for all the window icons surfaces.
|
||||
// Must be kept around! Pinned!
|
||||
// ReSharper disable once CollectionNeverQueried.Local
|
||||
private byte[][]? _windowIconData;
|
||||
|
||||
private void LoadWindowIcons()
|
||||
{
|
||||
// Sort such that closest to 64 is first.
|
||||
// SDL3 doesn't "figure it out itself" as much as GLFW does, which sucks.
|
||||
var icons = _clyde.LoadWindowIcons().OrderBy(i => Math.Abs(i.Width - MainWindowIconSize)).ToArray();
|
||||
if (icons.Length == 0)
|
||||
{
|
||||
// No window icons at all!
|
||||
return;
|
||||
}
|
||||
|
||||
_windowIconData = new byte[icons.Length][];
|
||||
|
||||
var mainImg = icons[0];
|
||||
|
||||
_sawmill.Verbose(
|
||||
"Have {iconCount} window icons available, choosing {mainIconWidth}x{mainIconHeight} as main",
|
||||
icons.Length,
|
||||
mainImg.Width,
|
||||
mainImg.Height);
|
||||
|
||||
(_windowIconSurface, var mainData) = CreateSurfaceFromImage(mainImg);
|
||||
_windowIconData[0] = mainData;
|
||||
|
||||
for (var i = 1; i < icons.Length; i++)
|
||||
{
|
||||
var (surface, data) = CreateSurfaceFromImage(icons[i]);
|
||||
_windowIconData[i] = data;
|
||||
SDL.SDL_AddSurfaceAlternateImage(_windowIconSurface, surface);
|
||||
// Kept alive by the main surface.
|
||||
SDL.SDL_DestroySurface(surface);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static (nint, byte[]) CreateSurfaceFromImage(Image<Rgba32> img)
|
||||
{
|
||||
var span = MemoryMarshal.AsBytes(img.GetPixelSpan());
|
||||
var copied = GC.AllocateUninitializedArray<byte>(span.Length, pinned: true);
|
||||
|
||||
span.CopyTo(copied);
|
||||
|
||||
IntPtr surface;
|
||||
fixed (byte* ptr = copied)
|
||||
{
|
||||
surface = SDL.SDL_CreateSurfaceFrom(
|
||||
img.Width,
|
||||
img.Height,
|
||||
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
|
||||
(IntPtr)ptr,
|
||||
sizeof(Rgba32) * img.Width);
|
||||
}
|
||||
|
||||
return (surface, copied);
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyWindowIcons()
|
||||
{
|
||||
SDL.SDL_DestroySurface(_windowIconSurface);
|
||||
_windowIconSurface = 0;
|
||||
_windowIconData = null;
|
||||
}
|
||||
|
||||
private void AssignWindowIconToWindow(nint window)
|
||||
{
|
||||
if (_windowIconSurface == 0)
|
||||
return;
|
||||
|
||||
SDL.SDL_SetWindowIcon(window, (nint) _windowIconSurface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using SDL3;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using static SDL2.SDL;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl2WindowingImpl
|
||||
private sealed partial class Sdl3WindowingImpl
|
||||
{
|
||||
private bool _windowingRunning;
|
||||
private ChannelWriter<CmdBase> _cmdWriter = default!;
|
||||
@@ -29,34 +29,34 @@ internal partial class Clyde
|
||||
|
||||
while (_windowingRunning)
|
||||
{
|
||||
var res = SDL_WaitEvent(out Unsafe.NullRef<SDL_Event>());
|
||||
if (res == 0)
|
||||
var res = SDL.SDL_WaitEventRef(ref Unsafe.NullRef<SDL.SDL_Event>());
|
||||
if (!res)
|
||||
{
|
||||
_sawmill.Error("Error while waiting on SDL2 events: {error}", SDL_GetError());
|
||||
_sawmill.Error("Error while waiting on SDL3 events: {error}", SDL.SDL_GetError());
|
||||
continue; // Assume it's a transient failure?
|
||||
}
|
||||
|
||||
while (SDL_PollEvent(out _) == 1)
|
||||
while (SDL.SDL_PollEvent(out _))
|
||||
{
|
||||
// We let callbacks process all events because of stuff like resizing.
|
||||
}
|
||||
|
||||
while (_cmdReader.TryRead(out var cmd) && _windowingRunning)
|
||||
{
|
||||
ProcessSdl2Cmd(cmd);
|
||||
ProcessSdl3Cmd(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PollEvents()
|
||||
{
|
||||
while (SDL_PollEvent(out _) == 1)
|
||||
while (SDL.SDL_PollEvent(out _))
|
||||
{
|
||||
// We let callbacks process all events because of stuff like resizing.
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSdl2Cmd(CmdBase cmdb)
|
||||
private void ProcessSdl3Cmd(CmdBase cmdb)
|
||||
{
|
||||
switch (cmdb)
|
||||
{
|
||||
@@ -93,6 +93,10 @@ internal partial class Clyde
|
||||
WinThreadWinRequestAttention(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetSize cmd:
|
||||
WinThreadWinSetSize(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetVisible cmd:
|
||||
WinThreadWinSetVisible(cmd);
|
||||
break;
|
||||
@@ -109,20 +113,24 @@ internal partial class Clyde
|
||||
WinThreadWinCursorSet(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinWinSetMode cmd:
|
||||
WinThreadWinSetMode(cmd);
|
||||
case CmdWinWinSetFullscreen cmd:
|
||||
WinThreadWinSetFullscreen(cmd);
|
||||
break;
|
||||
|
||||
case CmdWinSetWindowed cmd:
|
||||
WinThreadWinSetWindowed(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputSetRect cmd:
|
||||
WinThreadSetTextInputRect(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputStart:
|
||||
WinThreadStartTextInput();
|
||||
case CmdTextInputStart cmd:
|
||||
WinThreadStartTextInput(cmd);
|
||||
break;
|
||||
|
||||
case CmdTextInputStop:
|
||||
WinThreadStopTextInput();
|
||||
case CmdTextInputStop cmd:
|
||||
WinThreadStopTextInput(cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -167,25 +175,25 @@ internal partial class Clyde
|
||||
_eventWriter = eventChannel.Writer;
|
||||
}
|
||||
|
||||
private unsafe void SendCmd(CmdBase cmd)
|
||||
private void SendCmd(CmdBase cmd)
|
||||
{
|
||||
if (_clyde._threadWindowApi)
|
||||
{
|
||||
_cmdWriter.TryWrite(cmd);
|
||||
|
||||
SDL_Event ev = default;
|
||||
ev.type = (SDL_EventType)_sdlEventWakeup;
|
||||
SDL.SDL_Event ev = default;
|
||||
ev.type = _sdlEventWakeup;
|
||||
// Post empty event to unstuck WaitEvents if necessary.
|
||||
// This self-registered event type is ignored by the winthread, but it'll still wake it up.
|
||||
|
||||
// This can fail if the event queue is full.
|
||||
// That's not really a problem since in that case something else will be sure to wake the thread up anyways.
|
||||
// NOTE: have to avoid using PushEvents since that invokes callbacks which causes a deadlock.
|
||||
SDL_PeepEvents(&ev, 1, SDL_eventaction.SDL_ADDEVENT, ev.type, ev.type);
|
||||
SDL.SDL_PeepEvents(new Span<SDL.SDL_Event>(ref ev), 1, SDL.SDL_EventAction.SDL_ADDEVENT, ev.type, ev.type);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessSdl2Cmd(cmd);
|
||||
ProcessSdl3Cmd(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,88 +215,119 @@ internal partial class Clyde
|
||||
}
|
||||
|
||||
|
||||
private abstract record CmdBase;
|
||||
private abstract class CmdBase;
|
||||
|
||||
private sealed record CmdTerminate : CmdBase;
|
||||
private sealed class CmdTerminate : CmdBase;
|
||||
|
||||
private sealed record CmdWinCreate(
|
||||
GLContextSpec? GLSpec,
|
||||
WindowCreateParameters Parameters,
|
||||
nint ShareWindow,
|
||||
nint ShareContext,
|
||||
nint OwnerWindow,
|
||||
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinCreate : CmdBase
|
||||
{
|
||||
public required GLContextSpec? GLSpec;
|
||||
public required WindowCreateParameters Parameters;
|
||||
public required nint ShareWindow;
|
||||
public required nint ShareContext;
|
||||
public required nint OwnerWindow;
|
||||
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
|
||||
}
|
||||
|
||||
private sealed record CmdWinDestroy(
|
||||
nint Window,
|
||||
bool HadOwner
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinDestroy : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public bool HadOwner;
|
||||
}
|
||||
|
||||
private sealed record Sdl2WindowCreateResult(
|
||||
Sdl2WindowReg? Reg,
|
||||
string? Error
|
||||
);
|
||||
private sealed class Sdl3WindowCreateResult
|
||||
{
|
||||
public Sdl3WindowReg? Reg;
|
||||
public string? Error;
|
||||
}
|
||||
|
||||
private sealed record CmdRunAction(
|
||||
Action Action
|
||||
) : CmdBase;
|
||||
private sealed class CmdRunAction : CmdBase
|
||||
{
|
||||
public required Action Action;
|
||||
}
|
||||
|
||||
private sealed record CmdSetClipboard(
|
||||
string Text
|
||||
) : CmdBase;
|
||||
private sealed class CmdSetClipboard : CmdBase
|
||||
{
|
||||
public required string Text;
|
||||
}
|
||||
|
||||
private sealed record CmdGetClipboard(
|
||||
TaskCompletionSource<string> Tcs
|
||||
) : CmdBase;
|
||||
private sealed class CmdGetClipboard : CmdBase
|
||||
{
|
||||
public required TaskCompletionSource<string> Tcs;
|
||||
}
|
||||
|
||||
private sealed record CmdWinRequestAttention(
|
||||
nint Window
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinRequestAttention : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetVisible(
|
||||
nint Window,
|
||||
bool Visible
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetSize : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public int W;
|
||||
public int H;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetTitle(
|
||||
nint Window,
|
||||
string Title
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetVisible : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public bool Visible;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorCreate(
|
||||
Image<Rgba32> Bytes,
|
||||
Vector2i Hotspot,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetTitle : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public required string Title;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorDestroy(
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorCreate : CmdBase
|
||||
{
|
||||
public required Image<Rgba32> Bytes;
|
||||
public Vector2i Hotspot;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinCursorSet(
|
||||
nint Window,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorDestroy : CmdBase
|
||||
{
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinWinSetMode(
|
||||
nint Window,
|
||||
WindowMode Mode
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinCursorSet : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed class CmdWinWinSetFullscreen : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed class CmdWinSetWindowed : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public int PosX;
|
||||
public int PosY;
|
||||
}
|
||||
|
||||
// IME
|
||||
private sealed record CmdTextInputStart : CmdBase
|
||||
private sealed class CmdTextInputStart : CmdBase
|
||||
{
|
||||
public static readonly CmdTextInputStart Instance = new();
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdTextInputStop : CmdBase
|
||||
private sealed class CmdTextInputStop : CmdBase
|
||||
{
|
||||
public static readonly CmdTextInputStop Instance = new();
|
||||
public nint Window;
|
||||
}
|
||||
|
||||
private sealed record CmdTextInputSetRect(
|
||||
SDL_Rect Rect
|
||||
) : CmdBase;
|
||||
private sealed class CmdTextInputSetRect : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public SDL.SDL_Rect Rect;
|
||||
public int Cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
226
Robust.Client/Graphics/Clyde/Windowing/Sdl3.cs
Normal file
226
Robust.Client/Graphics/Clyde/Windowing/Sdl3.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using SDL3;
|
||||
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IWindowingImpl
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private readonly Clyde _clyde;
|
||||
private GCHandle _selfGCHandle;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly ISawmill _sawmillSdl3;
|
||||
|
||||
private SdlVideoDriver _videoDriver;
|
||||
|
||||
public Sdl3WindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
_clyde = clyde;
|
||||
deps.InjectDependencies(this, true);
|
||||
|
||||
_sawmill = _logManager.GetSawmill("clyde.win");
|
||||
_sawmillSdl3 = _logManager.GetSawmill("clyde.win.sdl3");
|
||||
}
|
||||
|
||||
public bool Init()
|
||||
{
|
||||
InitChannels();
|
||||
|
||||
if (!InitSdl3())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool InitSdl3()
|
||||
{
|
||||
CheckThreadApartment();
|
||||
|
||||
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SDL.SDL_SetLogPriorities(SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
|
||||
SDL.SDL_SetLogOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_IME_IMPLEMENTED_UI, "composition");
|
||||
|
||||
// SDL3's GameInput support is currently broken and leaving it on
|
||||
// causes a "that operation is not supported" error to be logged on startup.
|
||||
// https://github.com/libsdl-org/SDL/issues/11813
|
||||
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
|
||||
|
||||
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
|
||||
if (!res)
|
||||
{
|
||||
_sawmill.Fatal("Failed to initialize SDL3: {error}", SDL.SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
var version = SDL.SDL_GetVersion();
|
||||
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
|
||||
_sawmill.Debug(
|
||||
"SDL3 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}",
|
||||
SDL.SDL_VERSIONNUM_MAJOR(version),
|
||||
SDL.SDL_VERSIONNUM_MINOR(version),
|
||||
SDL.SDL_VERSIONNUM_MICRO(version),
|
||||
videoDriver);
|
||||
|
||||
LoadSdl3VideoDriver();
|
||||
|
||||
_sdlEventWakeup = SDL.SDL_RegisterEvents(1);
|
||||
if (_sdlEventWakeup == 0)
|
||||
throw new InvalidOperationException("SDL_RegisterEvents failed");
|
||||
|
||||
LoadWindowIcons();
|
||||
InitCursors();
|
||||
InitMonitors();
|
||||
ReloadKeyMap();
|
||||
|
||||
SDL.SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CheckThreadApartment()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return;
|
||||
|
||||
if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
|
||||
_sawmill.Error("Thread apartment state isn't STA. This will likely break things!!!");
|
||||
}
|
||||
|
||||
private void LoadSdl3VideoDriver()
|
||||
{
|
||||
_videoDriver = SDL.SDL_GetCurrentVideoDriver() switch
|
||||
{
|
||||
"windows" => SdlVideoDriver.Windows,
|
||||
"x11" => SdlVideoDriver.X11,
|
||||
_ => SdlVideoDriver.Other,
|
||||
};
|
||||
}
|
||||
|
||||
public unsafe void Shutdown()
|
||||
{
|
||||
if (_selfGCHandle != default)
|
||||
{
|
||||
SDL.SDL_RemoveEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
|
||||
_selfGCHandle.Free();
|
||||
_selfGCHandle = default;
|
||||
}
|
||||
|
||||
SDL.SDL_SetLogOutputFunction(null, null);
|
||||
|
||||
if (SDL.SDL_WasInit(0) != 0)
|
||||
{
|
||||
_sawmill.Debug("Terminating SDL3");
|
||||
SDL.SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDispose()
|
||||
{
|
||||
// Not currently used
|
||||
}
|
||||
|
||||
public void GLMakeContextCurrent(WindowReg? reg)
|
||||
{
|
||||
SDL.SDLBool res;
|
||||
if (reg is Sdl3WindowReg sdlReg)
|
||||
res = SDL.SDL_GL_MakeCurrent(sdlReg.Sdl3Window, sdlReg.GlContext);
|
||||
else
|
||||
res = SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (!res)
|
||||
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
public void GLSwapInterval(WindowReg reg, int interval)
|
||||
{
|
||||
((Sdl3WindowReg)reg).SwapInterval = interval;
|
||||
SDL.SDL_GL_SetSwapInterval(interval);
|
||||
}
|
||||
|
||||
public unsafe void* GLGetProcAddress(string procName)
|
||||
{
|
||||
return (void*) SDL.SDL_GL_GetProcAddress(procName);
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
var version = SDL.SDL_GetVersion();
|
||||
|
||||
var major = SDL.SDL_VERSIONNUM_MAJOR(version);
|
||||
var minor = SDL.SDL_VERSIONNUM_MINOR(version);
|
||||
var micro = SDL.SDL_VERSIONNUM_MICRO(version);
|
||||
|
||||
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
|
||||
var revision = SDL.SDL_GetRevision();
|
||||
|
||||
return $"SDL {major}.{minor}.{micro} (rev: {revision}, video driver: {videoDriver})";
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static unsafe void LogOutputFunction(
|
||||
void* userdata,
|
||||
int category,
|
||||
SDL.SDL_LogPriority priority,
|
||||
byte* message)
|
||||
{
|
||||
var obj = (Sdl3WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
|
||||
|
||||
var level = priority switch
|
||||
{
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
|
||||
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
|
||||
_ => LogLevel.Error
|
||||
};
|
||||
|
||||
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
|
||||
var categoryName = SdlLogCategoryName(category);
|
||||
obj._sawmillSdl3.Log(level, $"[{categoryName}] {msg}");
|
||||
}
|
||||
|
||||
private static string SdlLogCategoryName(int category)
|
||||
{
|
||||
return (SDL.SDL_LogCategory) category switch {
|
||||
// @formatter:off
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION => "application",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ERROR => "error",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ASSERT => "assert",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_SYSTEM => "system",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_AUDIO => "audio",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_VIDEO => "video",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_RENDER => "render",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_INPUT => "input",
|
||||
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_TEST => "test",
|
||||
_ => "unknown"
|
||||
// @formatter:on
|
||||
};
|
||||
}
|
||||
|
||||
private enum SdlVideoDriver
|
||||
{
|
||||
// These are the ones we need to be able to check against.
|
||||
Other,
|
||||
Windows,
|
||||
X11
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop.Windows;
|
||||
using TerraFX.Interop.Xlib;
|
||||
using X11Window = TerraFX.Interop.Xlib.Window;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
@@ -33,6 +36,8 @@ internal partial class Clyde
|
||||
|
||||
public static unsafe void WindowsSharedWindowCreate(HWND hWnd, IConfigurationManager cfg)
|
||||
{
|
||||
// TODO: REMOVE, only used by GLFW, SDL3 does DWMWA_USE_IMMERSIVE_DARK_MODE automatically.
|
||||
|
||||
// >= Windows 11 22000 check
|
||||
if (cfg.GetCVar(CVars.DisplayWin11ImmersiveDarkMode) && Environment.OSVersion.Version.Build >= 22000)
|
||||
{
|
||||
@@ -40,5 +45,37 @@ internal partial class Clyde
|
||||
Windows.DwmSetWindowAttribute(hWnd, 20, &b, (uint) sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetWindowStyleNoTitleOptionsWindows(HWND hWnd)
|
||||
{
|
||||
DebugTools.Assert(hWnd != HWND.NULL);
|
||||
|
||||
Windows.SetWindowLongPtrW(
|
||||
hWnd,
|
||||
GWL.GWL_STYLE,
|
||||
// Cast to long here to work around a bug in rider with nint bitwise operators.
|
||||
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
|
||||
}
|
||||
|
||||
public static unsafe void SetWindowStyleNoTitleOptionsX11(Display* x11Display, X11Window x11Window)
|
||||
{
|
||||
DebugTools.Assert(x11Window != X11Window.NULL);
|
||||
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
|
||||
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
|
||||
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
|
||||
DebugTools.Assert(newPropVal != Atom.NULL);
|
||||
|
||||
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
|
||||
#pragma warning disable CA1806
|
||||
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
|
||||
Xlib.XChangeProperty(x11Display, x11Window,
|
||||
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
|
||||
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
|
||||
(byte*)&newPropVal, 1);
|
||||
#pragma warning restore CA1806
|
||||
|
||||
Marshal.FreeCoTaskMem(newPropValString);
|
||||
Marshal.FreeCoTaskMem(propNameString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,8 @@ namespace Robust.Client.Graphics
|
||||
public abstract void DrawLine(Vector2 from, Vector2 to, Color color);
|
||||
|
||||
public abstract void RenderInRenderTarget(IRenderTarget target, Action a, Color? clearColor);
|
||||
|
||||
public abstract void DrawTexture(Texture texture, Vector2 position, Color? modulate = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user