mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
52 Commits
v238.0.1
...
serializat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
190c35dbdf | ||
|
|
b52c26d0eb | ||
|
|
e03aec47ef | ||
|
|
35c983d2fd | ||
|
|
88cfd04f01 | ||
|
|
ee906af16e | ||
|
|
e205ae3627 | ||
|
|
d818c5aa0c | ||
|
|
aa8fe8ac92 | ||
|
|
4ba6687b9d | ||
|
|
8f2817aa4e | ||
|
|
89c7839fe2 | ||
|
|
9799132001 | ||
|
|
34ffa56c57 | ||
|
|
f8410a4674 | ||
|
|
43b991c690 | ||
|
|
c463fc5e78 | ||
|
|
e21b3e069a | ||
|
|
c8f94ab40d | ||
|
|
2a882b5555 | ||
|
|
32d8a1cba9 | ||
|
|
5d84be9c78 | ||
|
|
eaaa70437a | ||
|
|
448e8b0c2c | ||
|
|
42948d8f8e | ||
|
|
039468f4b6 | ||
|
|
6a3f88b1c6 | ||
|
|
a314c5f797 | ||
|
|
bcc4cd77cf | ||
|
|
941cb4c1d6 | ||
|
|
7b58760331 | ||
|
|
f0306b593a | ||
|
|
5e1935c310 | ||
|
|
67c44a5fc5 | ||
|
|
3ea0a0244b | ||
|
|
325a39ee4b | ||
|
|
e4190f4f29 | ||
|
|
4d163ed818 | ||
|
|
09c6a816e0 | ||
|
|
dfc4894c8b | ||
|
|
f2b096f145 | ||
|
|
1984e97d2f | ||
|
|
57291b88c0 | ||
|
|
f40dd51648 | ||
|
|
d08fdd3a18 | ||
|
|
80dbf02af4 | ||
|
|
a2983a5ee0 | ||
|
|
7810cd0c2e | ||
|
|
6c8b863731 | ||
|
|
c2ca7c7811 | ||
|
|
347d240fae | ||
|
|
87a5745519 |
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
2
.github/workflows/publish-client.yml
vendored
2
.github/workflows/publish-client.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Package client
|
||||
run: Tools/package_client_build.py -p windows mac linux
|
||||
|
||||
2
.github/workflows/test-content.yml
vendored
2
.github/workflows/test-content.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Disable submodule autoupdate
|
||||
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
|
||||
|
||||
|
||||
@@ -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.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>
|
||||
|
||||
|
||||
113
RELEASE-NOTES.md
113
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,117 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 240.1.3-source-gen-debug
|
||||
|
||||
|
||||
## 240.1.2-source-gen-debug
|
||||
|
||||
|
||||
## 240.1.2
|
||||
|
||||
|
||||
## 240.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed one of the `IOverlayManager.RemoveOverlay` overrides not fully removing the overlay.
|
||||
|
||||
|
||||
## 240.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added an `AsNullable` extension method for converting an `Entity<T>` into an `Entity<T?>`
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed an exception in `PhysicsSystem.DestroyContacts()` that could result in entities getting stuck with broken physics.
|
||||
|
||||
### Other
|
||||
|
||||
* `GamePrototypeLoadManager` will now send all uploaded prototypes to connecting players in a single `GamePrototypeLoadMessage`, as opposed to one message per upload.
|
||||
|
||||
|
||||
## 240.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `SharedBroadphaseSystem.GetBroadphases()` not returning the map itself, which was causing physics to not work properly off-grid.
|
||||
|
||||
|
||||
## 240.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `ComponentRegistry` no longer implements `ISerializationContext`
|
||||
* Tickrate values are now `ushort`, allowing them to go up to 65535.
|
||||
|
||||
### New features
|
||||
|
||||
* Console completion options now have new flags for preventing suggestions from being escaped or quoted.
|
||||
* Added `ILocalizationManager.HasCulture()`.
|
||||
* Static `EntProtoId<T>` fields are now validated to exist.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a state handling bug in replays, which was causing exceptions to be thrown when applying delta states.
|
||||
|
||||
### Other
|
||||
|
||||
* Reduced amount of `DynamicMethod`s used by serialization system. This should improve performance somewhat.
|
||||
|
||||
### Internal
|
||||
|
||||
* Avoided sorting overlays every render frame.
|
||||
* Various clean up to grid fixture code/adding asserts.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -106,6 +107,7 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -147,6 +149,7 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
@@ -188,6 +191,7 @@ public sealed class ComponentPauseGeneratorTest
|
||||
public partial class FooComponent
|
||||
{
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
|
||||
@@ -1,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 =
|
||||
{
|
||||
|
||||
@@ -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.DuplicateDependencyAnalyzer>;
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DuplicateDependencyAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
@@ -15,7 +14,7 @@ public sealed class DuplicateDependencyAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, NUnitVerifier>()
|
||||
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, 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.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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
|
||||
<CETCompat>false</CETCompat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -88,10 +88,10 @@ namespace Robust.Client
|
||||
{
|
||||
if (GameInfo != null)
|
||||
{
|
||||
GameInfo.TickRate = (byte) tickrate;
|
||||
GameInfo.TickRate = (ushort) tickrate;
|
||||
}
|
||||
|
||||
_timing.SetTickRateAt((byte) tickrate, info.TickChanged);
|
||||
_timing.SetTickRateAt((ushort) tickrate, info.TickChanged);
|
||||
_logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
|
||||
}
|
||||
|
||||
@@ -395,6 +395,6 @@ namespace Robust.Client
|
||||
/// </summary>
|
||||
public int ServerMaxPlayers { get; set; }
|
||||
|
||||
public byte TickRate { get; internal set; }
|
||||
public uint TickRate { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
protected override void OnAppearanceGetState(EntityUid uid, AppearanceComponent component, ref ComponentGetState args)
|
||||
{
|
||||
// TODO Game State
|
||||
// Force the client to serialize & de-serialize implicitly generated component states.
|
||||
var clone = CloneAppearanceData(component.AppearanceData);
|
||||
args.State = new AppearanceComponentState(clone);
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("MergeImplicitData"))
|
||||
{
|
||||
MergeImplicitData(createdEntities);
|
||||
GenerateImplicitStates(createdEntities);
|
||||
}
|
||||
|
||||
if (_lastProcessedInput < curState.LastProcessedInput)
|
||||
@@ -671,7 +671,7 @@ namespace Robust.Client.GameStates
|
||||
/// initial server state for any newly created entity. It does this by simply using the standard <see
|
||||
/// cref="IEntityManager.GetComponentState"/>.
|
||||
/// </remarks>
|
||||
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
|
||||
public void GenerateImplicitStates(IEnumerable<NetEntity> createdEntities)
|
||||
{
|
||||
var bus = _entityManager.EventBus;
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ namespace Robust.Client.GameStates
|
||||
/// </summary>
|
||||
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
|
||||
|
||||
/// <summary>
|
||||
/// Generates implicit component states for newly created entities.
|
||||
/// This should always be called after running <see cref="ApplyGameState(GameState, GameState)"/>.
|
||||
/// </summary>
|
||||
void GenerateImplicitStates(IEnumerable<NetEntity> states);
|
||||
|
||||
/// <summary>
|
||||
/// Resets any entities that have changed while predicting future ticks.
|
||||
/// </summary>
|
||||
|
||||
@@ -215,8 +215,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
_overlays.Sort(OverlayComparer.Instance);
|
||||
|
||||
return _overlays;
|
||||
}
|
||||
|
||||
@@ -574,17 +572,5 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
return new Box2Rotated(aabb, rotation, aabb.Center);
|
||||
}
|
||||
|
||||
private sealed class OverlayComparer : IComparer<Overlay>
|
||||
{
|
||||
public static readonly OverlayComparer Instance = new();
|
||||
|
||||
public int Compare(Overlay? x, Overlay? y)
|
||||
{
|
||||
var zX = x?.ZIndex ?? 0;
|
||||
var zY = y?.ZIndex ?? 0;
|
||||
return zX.CompareTo(zY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -467,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
|
||||
{
|
||||
@@ -590,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -468,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,
|
||||
@@ -485,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())
|
||||
{
|
||||
@@ -509,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)
|
||||
{
|
||||
@@ -643,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,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)
|
||||
@@ -770,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;
|
||||
|
||||
@@ -66,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,586 +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 WindowSetSize(WindowReg window, Vector2i size)
|
||||
{
|
||||
SendCmd(new CmdWinSetSize(WinPtr(window), size.X, size.Y));
|
||||
}
|
||||
|
||||
public void WindowSetVisible(WindowReg window, bool visible)
|
||||
{
|
||||
window.IsVisible = visible;
|
||||
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
|
||||
}
|
||||
|
||||
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
|
||||
{
|
||||
SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -113,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;
|
||||
}
|
||||
}
|
||||
@@ -171,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,93 +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 CmdWinSetSize(
|
||||
nint Window,
|
||||
int W, int H
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetSize : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public int W;
|
||||
public int H;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetVisible(
|
||||
nint Window,
|
||||
bool Visible
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetVisible : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public bool Visible;
|
||||
}
|
||||
|
||||
private sealed record CmdWinSetTitle(
|
||||
nint Window,
|
||||
string Title
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinSetTitle : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public required string Title;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorCreate(
|
||||
Image<Rgba32> Bytes,
|
||||
Vector2i Hotspot,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorCreate : CmdBase
|
||||
{
|
||||
public required Image<Rgba32> Bytes;
|
||||
public Vector2i Hotspot;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdCursorDestroy(
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdCursorDestroy : CmdBase
|
||||
{
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinCursorSet(
|
||||
nint Window,
|
||||
ClydeHandle Cursor
|
||||
) : CmdBase;
|
||||
private sealed class CmdWinCursorSet : CmdBase
|
||||
{
|
||||
public nint Window;
|
||||
public ClydeHandle Cursor;
|
||||
}
|
||||
|
||||
private sealed record CmdWinWinSetMode(
|
||||
nint Window,
|
||||
WindowMode Mode
|
||||
) : CmdBase;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace Robust.Client.Graphics
|
||||
|
||||
baseline += new Vector2(metrics.Value.BearingX, -metrics.Value.BearingY);
|
||||
if(handle is DrawingHandleWorld worldhandle)
|
||||
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size));
|
||||
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size), color);
|
||||
else
|
||||
handle.DrawTexture(texture, baseline, color);
|
||||
return metrics.Value.Advance;
|
||||
|
||||
@@ -133,29 +133,5 @@ namespace Robust.Client.Graphics
|
||||
IEnumerable<IClydeMonitor> EnumerateMonitors();
|
||||
|
||||
IClydeWindow CreateWindow(WindowCreateParameters parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Set the active text input area in window pixel coordinates.
|
||||
/// </summary>
|
||||
/// <param name="rect">
|
||||
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
|
||||
/// </param>
|
||||
void TextInputSetRect(UIBox2i rect);
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that the game should start accepting text input on the currently focused window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On some platforms, this will cause an on-screen keyboard to appear.
|
||||
/// The game will also start accepting IME input if configured by the user.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TextInputStop"/>
|
||||
void TextInputStart();
|
||||
|
||||
/// <summary>
|
||||
/// Stop text input, opposite of <see cref="TextInputStart"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="TextInputStart"/>
|
||||
void TextInputStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,7 @@ namespace Robust.Client.Graphics
|
||||
void ShutdownGridEcsEvents();
|
||||
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManager? FileDialogImpl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,30 @@ namespace Robust.Client.Graphics
|
||||
/// Raised when the window has been resized.
|
||||
/// </summary>
|
||||
event Action<WindowResizedEventArgs> Resized;
|
||||
|
||||
/// <summary>
|
||||
/// Set the active text input area in window pixel coordinates.
|
||||
/// </summary>
|
||||
/// <param name="rect">
|
||||
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
|
||||
/// </param>
|
||||
void TextInputSetRect(UIBox2i rect, int cursor);
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that the game should start accepting text input on the currently focused window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On some platforms, this will cause an on-screen keyboard to appear.
|
||||
/// The game will also start accepting IME input if configured by the user.
|
||||
/// </remarks>
|
||||
/// <seealso cref="TextInputStop"/>
|
||||
void TextInputStart();
|
||||
|
||||
/// <summary>
|
||||
/// Stop text input, opposite of <see cref="TextInputStart"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="TextInputStart"/>
|
||||
void TextInputStop();
|
||||
}
|
||||
|
||||
public interface IClydeWindowInternal : IClydeWindow
|
||||
|
||||
@@ -13,10 +13,22 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
|
||||
private readonly Dictionary<Type, Overlay> _overlays = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list that duplicates a value from <see cref="_overlays"/>,
|
||||
/// but already sorted, by invoking <see cref="Sort"/>
|
||||
/// in <see cref="AddOverlay"/> and <see cref="RemoveOverlay(System.Type)"/>.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private readonly List<Overlay> _sortedOverlays = [];
|
||||
|
||||
private ISawmill _logger = default!;
|
||||
|
||||
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
|
||||
/// <summary>
|
||||
/// Returns a list of all overlays sorted by <see cref="Overlay.ZIndex"/>
|
||||
/// </summary>
|
||||
public IEnumerable<Overlay> AllOverlays => _sortedOverlays;
|
||||
|
||||
public void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
@@ -28,9 +40,10 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
|
||||
public bool AddOverlay(Overlay overlay)
|
||||
{
|
||||
if (_overlays.ContainsKey(overlay.GetType()))
|
||||
if (!_overlays.TryAdd(overlay.GetType(), overlay))
|
||||
return false;
|
||||
_overlays.Add(overlay.GetType(), overlay);
|
||||
|
||||
Sort();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,7 +55,9 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
return false;
|
||||
}
|
||||
|
||||
return _overlays.Remove(overlayClass);
|
||||
var result = _overlays.Remove(overlayClass);
|
||||
Sort();
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool RemoveOverlay<T>() where T : Overlay
|
||||
@@ -52,7 +67,7 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
|
||||
public bool RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
return _overlays.Remove(overlay.GetType());
|
||||
return RemoveOverlay(overlay.GetType());
|
||||
}
|
||||
|
||||
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
|
||||
@@ -104,8 +119,27 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
|
||||
return _overlays.ContainsKey(typeof(T));
|
||||
}
|
||||
|
||||
private void Sort()
|
||||
{
|
||||
_sortedOverlays.Clear();
|
||||
_sortedOverlays.AddRange(_overlays.Values);
|
||||
_sortedOverlays.Sort(OverlayComparer.Instance);
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logger = _logMan.GetSawmill("overlay");
|
||||
}
|
||||
|
||||
private sealed class OverlayComparer : IComparer<Overlay>
|
||||
{
|
||||
public static readonly OverlayComparer Instance = new();
|
||||
|
||||
public int Compare(Overlay? x, Overlay? y)
|
||||
{
|
||||
var zX = x?.ZIndex ?? 0;
|
||||
var zY = y?.ZIndex ?? 0;
|
||||
return zX.CompareTo(zY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,21 @@ public sealed partial class ReplayLoadManager
|
||||
_gameState.ClearDetachQueue();
|
||||
_gameState.ApplyGameState(checkpoint.State, next);
|
||||
|
||||
// Sort entities to ensure that we initialize parents before children
|
||||
var sorted = new List<EntityUid>(entities.Count);
|
||||
var added = new HashSet<EntityUid>(entities.Count);
|
||||
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
|
||||
foreach (var uid in entities)
|
||||
{
|
||||
AddSorted(uid, sorted, added, xformQuery);
|
||||
}
|
||||
DebugTools.AssertEqual(sorted.Count, entities.Count);
|
||||
DebugTools.AssertEqual(added.Count, entities.Count);
|
||||
await callback(i, total, LoadingState.Initializing, false);
|
||||
|
||||
i = 0;
|
||||
var query = _entMan.GetEntityQuery<MetaDataComponent>();
|
||||
foreach (var uid in entities)
|
||||
foreach (var uid in sorted)
|
||||
{
|
||||
_entMan.InitializeEntity(uid, query.GetComponent(uid));
|
||||
if (i++ % 50 == 0)
|
||||
@@ -109,7 +121,7 @@ public sealed partial class ReplayLoadManager
|
||||
|
||||
i = 0;
|
||||
await callback(0, total, LoadingState.Starting, true);
|
||||
foreach (var uid in entities)
|
||||
foreach (var uid in sorted)
|
||||
{
|
||||
_entMan.StartEntity(uid);
|
||||
if (i++ % 50 == 0)
|
||||
@@ -132,4 +144,16 @@ public sealed partial class ReplayLoadManager
|
||||
_replayPlayback.StartReplay(data);
|
||||
_timing.Paused = false;
|
||||
}
|
||||
|
||||
private void AddSorted(EntityUid uid, List<EntityUid> sortedList, HashSet<EntityUid> added, EntityQuery<TransformComponent> query)
|
||||
{
|
||||
if (!added.Add(uid))
|
||||
return;
|
||||
|
||||
var parent = query.Comp(uid).ParentUid;
|
||||
if (parent != EntityUid.Invalid)
|
||||
AddSorted(parent, sortedList, added, query);
|
||||
|
||||
sortedList.Add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ internal sealed partial class ReplayPlaybackManager
|
||||
_gameState.UpdateFullRep(state, cloneDelta: true);
|
||||
var next = Replay.NextState;
|
||||
BeforeApplyState?.Invoke((state, next));
|
||||
_gameState.ApplyGameState(state, next);
|
||||
var created = _gameState.ApplyGameState(state, next);
|
||||
_gameState.GenerateImplicitStates(created);
|
||||
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
|
||||
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
|
||||
Replay.LastApplied = state.ToSequence;
|
||||
|
||||
@@ -21,12 +21,15 @@
|
||||
<PackageReference Include="OpenTK.OpenAL" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.SharpFont" PrivateAssets="compile" />
|
||||
<PackageReference Include="Robust.Natives" />
|
||||
<PackageReference Include="System.Numerics.Vectors" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.Sodium" PrivateAssets="compile" />
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />
|
||||
<PackageReference Include="TerraFX.Interop.Xlib" />
|
||||
|
||||
<!-- Needed to pin transitive dependency versions. -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" PrivateAssets="compile" />
|
||||
|
||||
@@ -220,7 +220,7 @@ internal partial class UserInterfaceManager
|
||||
continue;
|
||||
|
||||
var typeDict = _assignerRegistry.GetOrNew(fieldInfo.FieldType);
|
||||
var assigner = (InternalReflectionUtils.AssignField<UIController, object?>)InternalReflectionUtils.EmitFieldAssigner<UIController>(backingField, true);
|
||||
var assigner = (InternalReflectionUtils.AssignField<UIController, object?>)InternalReflectionUtils.EmitFieldAssigner(typeof(UIController), backingField, true);
|
||||
typeDict.Add(controllerType, assigner);
|
||||
}
|
||||
|
||||
|
||||
@@ -880,7 +880,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
if (Editable)
|
||||
{
|
||||
_clyde.TextInputStart();
|
||||
Root?.Window?.TextInputStart();
|
||||
}
|
||||
|
||||
_focusedOnFrame = _timing.CurFrame;
|
||||
@@ -897,7 +897,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
OnFocusExit?.Invoke(new LineEditEventArgs(this, _text));
|
||||
|
||||
_clyde.TextInputStop();
|
||||
Root?.Window?.TextInputStop();
|
||||
|
||||
AbortIme(delete: false);
|
||||
}
|
||||
|
||||
@@ -1145,15 +1146,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
contentBox.Bottom),
|
||||
cursorColor);
|
||||
|
||||
if (Root?.Window is { } window)
|
||||
{
|
||||
// Update IME position.
|
||||
var imeBox = new UIBox2(
|
||||
actualCursorPosition,
|
||||
contentBox.Left,
|
||||
contentBox.Top,
|
||||
contentBox.Right,
|
||||
contentBox.Bottom);
|
||||
|
||||
_master._clyde.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition));
|
||||
window.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition), actualCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1366,15 +1366,16 @@ public sealed class TextEdit : Control
|
||||
baseLine.Y + descent),
|
||||
cursorColor);
|
||||
|
||||
if (UserInterfaceManager.KeyboardFocused == _master)
|
||||
if (UserInterfaceManager.KeyboardFocused == _master && Root?.Window is { } window)
|
||||
{
|
||||
var box = (UIBox2i)new UIBox2(
|
||||
baseLine.X,
|
||||
drawBox.Left,
|
||||
baseLine.Y - height + descent,
|
||||
drawBox.Right,
|
||||
baseLine.Y + descent);
|
||||
var cursorOffset = baseLine.X - drawBox.Left;
|
||||
|
||||
_master._clyde.TextInputSetRect(box.Translated(GlobalPixelPosition));
|
||||
window.TextInputSetRect(box.Translated(GlobalPixelPosition), (int) cursorOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,7 +1447,7 @@ public sealed class TextEdit : Control
|
||||
|
||||
if (Editable)
|
||||
{
|
||||
_clyde.TextInputStart();
|
||||
Root?.Window?.TextInputStart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1454,7 +1455,7 @@ public sealed class TextEdit : Control
|
||||
{
|
||||
base.KeyboardFocusExited();
|
||||
|
||||
_clyde.TextInputStop();
|
||||
Root?.Window?.TextInputStop();
|
||||
AbortIme(delete: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -276,10 +276,14 @@ public sealed partial class DebugConsole
|
||||
// This means that letter casing will match the completion suggestion.
|
||||
CommandBar.CursorPosition = lastRange.end;
|
||||
CommandBar.SelectionStart = lastRange.start;
|
||||
var insertValue = CommandParsing.Escape(completion);
|
||||
|
||||
var insertValue = (completionFlags & CompletionOptionFlags.NoEscape) == 0
|
||||
? CommandParsing.Escape(completion)
|
||||
: completion;
|
||||
|
||||
// If the replacement contains a space, we must quote it to treat it as a single argument.
|
||||
var mustQuote = insertValue.Contains(' ');
|
||||
var mustQuote = (completionFlags & CompletionOptionFlags.NoQuote) == 0 && insertValue.Contains(' ');
|
||||
|
||||
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
|
||||
{
|
||||
if (mustQuote)
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.OpenFile(filters);
|
||||
|
||||
var name = await GetOpenFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
@@ -53,6 +56,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
|
||||
{
|
||||
if (_clyde.FileDialogImpl is { } clydeImpl)
|
||||
return await clydeImpl.SaveFile(filters);
|
||||
|
||||
var name = await GetSaveFileName(filters);
|
||||
if (name == null)
|
||||
{
|
||||
|
||||
@@ -153,6 +153,7 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
|
||||
|
||||
builder.AppendLine($$"""
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class {{info.PartialTypeInfo.Name}}_AutoPauseSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
@@ -586,7 +586,7 @@ namespace Robust.Server
|
||||
{
|
||||
_config.OnValueChanged(CVars.NetTickrate, i =>
|
||||
{
|
||||
var b = (byte) i;
|
||||
var b = (ushort) i;
|
||||
_time.TickRate = b;
|
||||
|
||||
_logger.Info($"Tickrate changed to: {b} on tick {_time.CurTick}");
|
||||
@@ -594,7 +594,7 @@ namespace Robust.Server
|
||||
|
||||
var startOffset = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetTimeStartOffset));
|
||||
_time.TimeBase = (startOffset, GameTick.First);
|
||||
_time.TickRate = (byte) _config.GetCVar(CVars.NetTickrate);
|
||||
_time.TickRate = (ushort) _config.GetCVar(CVars.NetTickrate);
|
||||
|
||||
_logger.Info($"Name: {ServerName}");
|
||||
_logger.Info($"TickRate: {_time.TickRate}({_time.TickPeriod.TotalMilliseconds:0.00}ms)");
|
||||
|
||||
@@ -1017,8 +1017,8 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
// Skip removed tile definitions.
|
||||
if (!_tileDefManager.TryGetDefinition(prototypeId, out var definition))
|
||||
continue;
|
||||
|
||||
tileIdMap.Add(definition.TileId, origId);
|
||||
if (!tileIdMap.ContainsKey(definition.TileId))
|
||||
tileIdMap.Add(definition.TileId, origId);
|
||||
}
|
||||
|
||||
// Assign new IDs for all new tile types.
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<PackageReference Include="SharpZstd.Interop" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" />
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" />
|
||||
|
||||
<!-- Needed to pin transitive dependency versions. -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
|
||||
|
||||
@@ -336,6 +336,7 @@ namespace Robust.Server.ServerStatus
|
||||
{
|
||||
RespondShared();
|
||||
|
||||
_context.Response.StatusCode = (int)code;
|
||||
_context.Response.ContentType = "application/json";
|
||||
|
||||
await JsonSerializer.SerializeAsync(_context.Response.OutputStream, jsonData);
|
||||
|
||||
@@ -51,13 +51,10 @@ public sealed class GamePrototypeLoadManager : SharedPrototypeLoadManager
|
||||
internal void SendToNewUser(INetChannel channel)
|
||||
{
|
||||
// Just dump all the prototypes on connect, before them missing could be an issue.
|
||||
foreach (var prototype in LoadedPrototypes)
|
||||
var msg = new GamePrototypeLoadMessage
|
||||
{
|
||||
var msg = new GamePrototypeLoadMessage
|
||||
{
|
||||
PrototypeData = prototype
|
||||
};
|
||||
channel.SendMessage(msg);
|
||||
}
|
||||
PrototypeData = string.Join("\n\n", LoadedPrototypes)
|
||||
};
|
||||
channel.SendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ using Microsoft.CodeAnalysis.Text;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayFormat;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayMiscellaneousOptions;
|
||||
|
||||
// Yes dude I know this source generator isn't incremental, I'll fix it eventually.
|
||||
#pragma warning disable RS1035
|
||||
|
||||
namespace Robust.Shared.CompNetworkGenerator
|
||||
{
|
||||
[Generator]
|
||||
@@ -136,8 +139,11 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
// component.Count = state.Count;
|
||||
var handleStateSetters = new StringBuilder();
|
||||
|
||||
// Builds the string for copying delta fields in IComponentDelta to a new full state.
|
||||
var deltaCreate = new StringBuilder();
|
||||
// Builds the string for duplicating a full component state, in preparation for applying a delta state state
|
||||
// without modifying the original. Note that this will not do a proper clone of any collections, under the
|
||||
// assumption that nothing should ever try to modify them. Applying the delta state should just override the
|
||||
// referenced collection, not modify it.
|
||||
var shallowClone = new StringBuilder();
|
||||
|
||||
// Delta field states
|
||||
var deltaGetFields = new StringBuilder();
|
||||
@@ -189,8 +195,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
|
||||
deltaHandleFields.Append(@$"
|
||||
case {deltaStateName} {deltaStateName}_State:
|
||||
{{
|
||||
");
|
||||
{{");
|
||||
|
||||
var fieldHandleValue = $"{deltaStateName}_State.{name}!";
|
||||
|
||||
@@ -207,16 +212,15 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"(NetEntity{nullableAnnotation})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
|
||||
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityCoordinatesName:
|
||||
@@ -230,16 +234,15 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"(NetCoordinates{nullableAnnotation})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
|
||||
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($@"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityUidSetName:
|
||||
@@ -252,16 +255,15 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"({GlobalNetEntityUidSetName})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($@"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityUidListName:
|
||||
@@ -274,16 +276,15 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"({GlobalNetEntityUidListName})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($@"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -319,35 +320,34 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"(Dictionary<{key}, {value}>)";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
cast = $"({castString})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
}
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($@"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
|
||||
{
|
||||
networkedType = $"Dictionary<{key}, {value}>";
|
||||
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
|
||||
networkedType = $"Dictionary<{key}, {value}>";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
@@ -356,16 +356,15 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
cast = $"(Dictionary<{key}, {value}>)";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($@"fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -384,10 +383,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString;
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
if (state.{name} == null)
|
||||
component.{name} = null!;
|
||||
else
|
||||
component.{name} = new(state.{name});");
|
||||
component.{name} = state.{name} == null ? null! : new(state.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
var {name}Value = {cast} {fieldHandleValue};
|
||||
@@ -396,22 +392,10 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
else
|
||||
component.{name} = new {nullCast}({name}Value);");
|
||||
|
||||
if (nullable)
|
||||
{
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name} == null ? null : new(fullState.{name}),");
|
||||
}
|
||||
else
|
||||
{
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
}
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
if ({name} == null)
|
||||
fullState.{name} = null!;
|
||||
else
|
||||
fullState.{name} = new({name});");
|
||||
deltaApply.Add($"fullState.{name} = {name} == null ? null! : new({name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -424,11 +408,10 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = {cast} {fieldHandleValue};");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
shallowClone.Append($@"
|
||||
{name} = this.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
deltaApply.Add($"fullState.{name} = {name};");
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -447,28 +430,29 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
}};
|
||||
return;");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
}}
|
||||
break;");
|
||||
|
||||
}
|
||||
|
||||
var eventRaise = "";
|
||||
if (raiseAfterAutoHandle)
|
||||
{
|
||||
eventRaise = @"
|
||||
var ev = new AfterAutoHandleStateEvent(args.Current);
|
||||
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
|
||||
deltaHandleFields.Append(@"
|
||||
break;
|
||||
}
|
||||
");
|
||||
}
|
||||
|
||||
var deltaGetState = "";
|
||||
var deltaHandleState = "";
|
||||
var deltaInterface = "";
|
||||
var deltaCompFields = "";
|
||||
var deltaNetRegister = "";
|
||||
|
||||
var cloneMethod = "";
|
||||
if (fieldDeltas)
|
||||
{
|
||||
cloneMethod = $@"
|
||||
public {stateName} ShallowClone()
|
||||
{{
|
||||
return new {stateName}()
|
||||
{{{shallowClone}
|
||||
}};
|
||||
}}
|
||||
";
|
||||
|
||||
for (var i = 0; i < fields.Count; i++)
|
||||
{
|
||||
var name = fields[i].FieldName;
|
||||
@@ -484,20 +468,18 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
public {networkedType} {name} = default!;
|
||||
|
||||
public void ApplyToFullState({stateName} fullState)
|
||||
{{{apply}
|
||||
{{
|
||||
{apply}
|
||||
}}
|
||||
|
||||
public {stateName} CreateNewFullState({stateName} fullState)
|
||||
{{
|
||||
var newState = new {stateName}
|
||||
{{{deltaCreate}
|
||||
}};
|
||||
{apply}
|
||||
|
||||
var newState = fullState.ShallowClone();
|
||||
ApplyToFullState(newState);
|
||||
return newState;
|
||||
}}
|
||||
}}
|
||||
");
|
||||
");
|
||||
}
|
||||
|
||||
deltaNetRegister = $@"EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldsStr});";
|
||||
@@ -515,12 +497,6 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
}}
|
||||
}}";
|
||||
|
||||
deltaHandleState = $@"switch(args.Current)
|
||||
{{{deltaHandleFields}
|
||||
default:
|
||||
break;
|
||||
}}";
|
||||
|
||||
deltaInterface = " : IComponentDelta";
|
||||
|
||||
deltaCompFields = @$"/// <inheritdoc />
|
||||
@@ -530,6 +506,55 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty<GameTick>();";
|
||||
}
|
||||
|
||||
string handleState;
|
||||
if (!fieldDeltas)
|
||||
{
|
||||
var eventRaise = "";
|
||||
if (raiseAfterAutoHandle)
|
||||
{
|
||||
eventRaise = @"
|
||||
|
||||
var ev = new AfterAutoHandleStateEvent(args.Current);
|
||||
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
|
||||
}
|
||||
|
||||
handleState = $@"
|
||||
if (args.Current is not {stateName} state)
|
||||
return;
|
||||
{handleStateSetters}{eventRaise}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Re-indent handleStateSetters so it aligns with the switch block
|
||||
var stateSetters = handleStateSetters.ToString();
|
||||
stateSetters = stateSetters.Replace(" ", " ");
|
||||
|
||||
|
||||
var eventRaise = "";
|
||||
if (raiseAfterAutoHandle)
|
||||
{
|
||||
eventRaise = @"
|
||||
|
||||
if (args.Current is not {} current)
|
||||
return;
|
||||
|
||||
var ev = new AfterAutoHandleStateEvent(current);
|
||||
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
|
||||
}
|
||||
|
||||
handleState = $@"
|
||||
switch(args.Current)
|
||||
{{{deltaHandleFields}
|
||||
case {stateName} state:
|
||||
{{{stateSetters}
|
||||
break;
|
||||
}}
|
||||
|
||||
default:
|
||||
return;
|
||||
}}{eventRaise}";
|
||||
}
|
||||
|
||||
return $@"// <auto-generated />
|
||||
#nullable enable
|
||||
using System;
|
||||
@@ -550,11 +575,14 @@ public partial class {componentName}{deltaInterface}
|
||||
{deltaCompFields}
|
||||
|
||||
[System.Serializable, NetSerializable]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class {stateName} : IComponentState
|
||||
{{{stateFields}
|
||||
{cloneMethod}
|
||||
}}
|
||||
|
||||
[RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public sealed class {componentName}_AutoNetworkSystem : EntitySystem
|
||||
{{
|
||||
public override void Initialize()
|
||||
@@ -575,12 +603,7 @@ public partial class {componentName}{deltaInterface}
|
||||
}}
|
||||
|
||||
private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)
|
||||
{{
|
||||
{deltaHandleState}
|
||||
|
||||
if (args.Current is not {stateName} state)
|
||||
return;
|
||||
{handleStateSetters}{eventRaise}
|
||||
{{{handleState}
|
||||
}}
|
||||
}}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Reflection;
|
||||
using ILReader;
|
||||
using ILReader.Readers;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed;
|
||||
|
||||
namespace Robust.Shared.Scripting;
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class ILCommand : ToolshedCommand
|
||||
{
|
||||
// Disabled due to relying on external dependency ILReader.
|
||||
/*
|
||||
[CommandImplementation("dumpil")]
|
||||
public void DumpIL(
|
||||
IInvocationContext ctx,
|
||||
@@ -30,5 +29,6 @@ public sealed class ILCommand : ToolshedCommand
|
||||
IILReaderConfiguration cfg = ILReader.Configuration.Resolve(method);
|
||||
return cfg.GetReader(method);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ILReader.Core" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
||||
|
||||
<!-- Needed to pin transitive dependency versions. -->
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -460,10 +460,17 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public void LoadCVarsFromAssembly(Assembly assembly)
|
||||
{
|
||||
foreach (var defField in assembly
|
||||
foreach (var type in assembly
|
||||
.GetTypes()
|
||||
.Where(p => Attribute.IsDefined(p, typeof(CVarDefsAttribute)))
|
||||
.SelectMany(p => p.GetFields(BindingFlags.Public | BindingFlags.Static)))
|
||||
.Where(p => Attribute.IsDefined(p, typeof(CVarDefsAttribute))))
|
||||
{
|
||||
LoadCVarsFromType(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCVarsFromType(Type containingType)
|
||||
{
|
||||
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
var fieldType = defField.FieldType;
|
||||
if (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(CVarDef<>))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -7,6 +8,7 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
void OverrideConVars(IEnumerable<(string key, string value)> cVars);
|
||||
void LoadCVarsFromAssembly(Assembly assembly);
|
||||
void LoadCVarsFromType(Type containingType);
|
||||
|
||||
void Initialize(bool isServer);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class CommandBuffer
|
||||
_commandBuffer.AddFirst(command);
|
||||
}
|
||||
|
||||
public void Tick(byte tickRate)
|
||||
public void Tick(ushort tickRate)
|
||||
{
|
||||
_tickrate = tickRate;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class LogSetLevelCommand : LocalizedCommands
|
||||
{
|
||||
private const string LevelNull = "null";
|
||||
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
public override string Command => "loglevel";
|
||||
@@ -21,7 +23,7 @@ internal sealed class LogSetLevelCommand : LocalizedCommands
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
LogLevel? level;
|
||||
if (levelname == "null")
|
||||
if (levelname == LevelNull)
|
||||
{
|
||||
level = null;
|
||||
}
|
||||
@@ -49,7 +51,7 @@ internal sealed class LogSetLevelCommand : LocalizedCommands
|
||||
"<sawmill>");
|
||||
case 2:
|
||||
return CompletionResult.FromHintOptions(
|
||||
Enum.GetNames<LogLevel>(),
|
||||
Enum.GetNames<LogLevel>().Union([LevelNull]),
|
||||
"<level>");
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Console;
|
||||
|
||||
@@ -74,4 +75,14 @@ public enum CompletionOptionFlags
|
||||
/// (instead of adding a space to go to the next one).
|
||||
/// </summary>
|
||||
PartialCompletion = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Prevents suggestions containing spaces from being automatically wrapped in quotes.
|
||||
/// </summary>
|
||||
NoQuote = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Prevents suggestions from being escaped using <see cref="CommandParsing.Escape"/>.
|
||||
/// </summary>
|
||||
NoEscape = 1 << 2,
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace Robust.Shared.ContentPack
|
||||
Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle =>
|
||||
{
|
||||
var ver = new Verifier(resolver);
|
||||
ver.SetSystemModuleName(new AssemblyName(SystemAssemblyName));
|
||||
ver.SetSystemModuleName(new AssemblyNameInfo(SystemAssemblyName));
|
||||
while (handle.MoveNext())
|
||||
{
|
||||
foreach (var result in ver.Verify(peReader, handle.Current, verifyMethods: true))
|
||||
@@ -926,12 +926,12 @@ namespace Robust.Shared.ContentPack
|
||||
return null;
|
||||
}
|
||||
|
||||
public PEReader? ResolveAssembly(AssemblyName assemblyName)
|
||||
public PEReader? ResolveAssembly(AssemblyNameInfo assemblyName)
|
||||
{
|
||||
return _dictionary.GetOrAdd(assemblyName.Name!, ResolveCore);
|
||||
}
|
||||
|
||||
public PEReader? ResolveModule(AssemblyName referencingAssembly, string fileName)
|
||||
public PEReader? ResolveModule(AssemblyNameInfo referencingAssembly, string fileName)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
@@ -467,6 +467,8 @@ Types:
|
||||
IComponent: { All: True }
|
||||
IContainer: { All: True }
|
||||
ITypeDescriptorContext: { All: True }
|
||||
EditorBrowsableAttribute: { All: True }
|
||||
EditorBrowsableState: { All: True }
|
||||
System.Diagnostics.CodeAnalysis:
|
||||
AllowNullAttribute: { All: True }
|
||||
DisallowNullAttribute: { All: True }
|
||||
@@ -1197,6 +1199,7 @@ Types:
|
||||
RuntimeFieldHandle: { }
|
||||
RuntimeMethodHandle: { }
|
||||
RuntimeTypeHandle: { }
|
||||
STAThreadAttribute: { All: True }
|
||||
SByte: { All: True }
|
||||
Single: { All: True }
|
||||
Span`1:
|
||||
@@ -1367,6 +1370,7 @@ Types:
|
||||
- "string[] Split(char, int, System.StringSplitOptions)"
|
||||
- "string[] Split(char, System.StringSplitOptions)"
|
||||
- "string[] Split(char[])"
|
||||
- "string[] Split(System.ReadOnlySpan`1<char>)"
|
||||
- "string[] Split(char[], int)"
|
||||
- "string[] Split(char[], int, System.StringSplitOptions)"
|
||||
- "string[] Split(char[], System.StringSplitOptions)"
|
||||
|
||||
@@ -40,17 +40,17 @@ public interface IComponentDeltaState<TState> : IComponentDeltaState where TStat
|
||||
|
||||
void IComponentDeltaState.ApplyToFullState(IComponentState fullState)
|
||||
{
|
||||
if (fullState is TState state)
|
||||
ApplyToFullState(state);
|
||||
else
|
||||
throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
|
||||
if (fullState is not TState state)
|
||||
throw new Exception($"Unexpected type. Expected {typeof(TState).Name} but got {fullState.GetType().Name}");
|
||||
|
||||
ApplyToFullState(state);
|
||||
}
|
||||
|
||||
IComponentState IComponentDeltaState.CreateNewFullState(IComponentState fullState)
|
||||
{
|
||||
if (fullState is TState state)
|
||||
return CreateNewFullState(state);
|
||||
else
|
||||
throw new Exception($"Unexpected type. Expected {nameof(TState)} but got {fullState.GetType().Name}");
|
||||
if (fullState is not TState state)
|
||||
throw new Exception($"Unexpected type. Expected {typeof(TState).Name} but got {fullState.GetType().Name}");
|
||||
|
||||
return CreateNewFullState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T : IComponent?
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent?
|
||||
{
|
||||
@@ -118,6 +121,7 @@ public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent?
|
||||
{
|
||||
@@ -222,6 +226,7 @@ public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent?
|
||||
{
|
||||
@@ -350,6 +355,7 @@ public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUi
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent?
|
||||
{
|
||||
@@ -502,6 +508,7 @@ public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<Enti
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent?
|
||||
{
|
||||
@@ -678,6 +685,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<
|
||||
public EntityUid AsType() => Owner;
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent?
|
||||
{
|
||||
@@ -879,6 +887,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsT
|
||||
|
||||
}
|
||||
|
||||
[NotYamlSerializable]
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid, IAsType<EntityUid>
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? where T8 : IComponent?
|
||||
{
|
||||
|
||||
104
Robust.Shared/GameObjects/EntityExt.cs
Normal file
104
Robust.Shared/GameObjects/EntityExt.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public static class EntityExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an Entity{T} to Entity{T?}, making it compatible with methods that take nullable components.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The component type. Must implement IComponent.</typeparam>
|
||||
/// <param name="ent">The source entity to convert.</param>
|
||||
/// <returns>An Entity{T?} with the same owner and component as the source entity.</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Instead of:
|
||||
/// Entity{MyComponent?} nullable = (ent, ent.Comp);
|
||||
///
|
||||
/// // You can write:
|
||||
/// Entity{MyComponent?} nullable = ent.AsNullable();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static Entity<T?> AsNullable<T>(this Entity<T> ent) where T : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?> AsNullable<T1, T2>(this Entity<T1, T2> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?> AsNullable<T1, T2, T3>(this Entity<T1, T2, T3> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?> AsNullable<T1, T2, T3, T4>(this Entity<T1, T2, T3, T4> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?> AsNullable<T1, T2, T3, T4, T5>(this Entity<T1, T2, T3, T4, T5> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?> AsNullable<T1, T2, T3, T4, T5, T6>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?> AsNullable<T1, T2, T3, T4, T5, T6, T7>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6, T7> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
where T7 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6, ent.Comp7);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AsNullable{T}" />
|
||||
public static Entity<T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?> AsNullable<T1, T2, T3, T4, T5, T6, T7, T8>(
|
||||
this Entity<T1, T2, T3, T4, T5, T6, T7, T8> ent)
|
||||
where T1 : IComponent
|
||||
where T2 : IComponent
|
||||
where T3 : IComponent
|
||||
where T4 : IComponent
|
||||
where T5 : IComponent
|
||||
where T6 : IComponent
|
||||
where T7 : IComponent
|
||||
where T8 : IComponent
|
||||
{
|
||||
return new(ent.Owner, ent.Comp1, ent.Comp2, ent.Comp3, ent.Comp4, ent.Comp5, ent.Comp6, ent.Comp7, ent.Comp8);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,6 @@ namespace Robust.Shared.GameObjects
|
||||
newFixtures.Add(($"grid_chunk-{bounds.Left}-{bounds.Bottom}", newFixture));
|
||||
}
|
||||
|
||||
var toRemove = new ValueList<(string Id, Fixture Fixture)>();
|
||||
// Check if we even need to issue an eventbus event
|
||||
var updated = false;
|
||||
|
||||
@@ -167,6 +166,13 @@ namespace Robust.Shared.GameObjects
|
||||
for (var i = newFixtures.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var fixture = newFixtures[i].Fixture;
|
||||
|
||||
// TODO GRIDS
|
||||
// Fix this
|
||||
// This **only** works if we assume the density is always the default (PhysicsConstants.DefaultDensity).
|
||||
// Hence, this always fails in SS14 because ShuttleSystem.OnGridFixtureChange changes the density.
|
||||
// So it constantly creats & destroys fixtures unnecessarily
|
||||
// AAAAA
|
||||
if (!oldFixture.Equals(fixture))
|
||||
continue;
|
||||
|
||||
@@ -175,21 +181,16 @@ namespace Robust.Shared.GameObjects
|
||||
break;
|
||||
}
|
||||
|
||||
if (existing)
|
||||
continue;
|
||||
|
||||
// Doesn't align with any new fixtures so delete
|
||||
if (existing) continue;
|
||||
|
||||
toRemove.Add((oldId, oldFixture));
|
||||
chunk.Fixtures.Remove(oldId);
|
||||
_fixtures.DestroyFixture(uid, oldId, oldFixture, false, body: body, manager: manager, xform: xform);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
foreach (var (id, fixture) in toRemove.Span)
|
||||
{
|
||||
// TODO add a DestroyFixture() override that takes in a list.
|
||||
// reduced broadphase lookups
|
||||
chunk.Fixtures.Remove(id);
|
||||
_fixtures.DestroyFixture(uid, id, fixture, false, body: body, manager: manager, xform: xform);
|
||||
}
|
||||
|
||||
if (newFixtures.Count > 0 || toRemove.Count > 0)
|
||||
if (newFixtures.Count > 0)
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
@@ -200,10 +201,11 @@ namespace Robust.Shared.GameObjects
|
||||
chunk.Fixtures.Add(id);
|
||||
var existingFixture = _fixtures.GetFixtureOrNull(uid, id, manager: manager);
|
||||
// Check if it's the same (otherwise remove anyway).
|
||||
// TODO GRIDS
|
||||
// wasn't this already checked?
|
||||
if (existingFixture?.Shape is PolygonShape poly &&
|
||||
poly.EqualsApprox((PolygonShape) fixture.Shape))
|
||||
{
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -245,13 +245,10 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
private void OnGridHandleState(EntityUid uid, MapGridComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
HashSet<MapChunk> modifiedChunks;
|
||||
|
||||
switch (args.Current)
|
||||
{
|
||||
case MapGridComponentDeltaState delta:
|
||||
{
|
||||
modifiedChunks = new();
|
||||
DebugTools.Assert(component.ChunkSize == delta.ChunkSize || component.Chunks.Count == 0,
|
||||
"Can't modify chunk size of an existing grid.");
|
||||
|
||||
@@ -261,7 +258,7 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
foreach (var (index, chunkData) in delta.ChunkData)
|
||||
{
|
||||
ApplyChunkData(uid, component, index, chunkData, modifiedChunks);
|
||||
ApplyChunkData(uid, component, index, chunkData);
|
||||
}
|
||||
|
||||
component.LastTileModifiedTick = delta.LastTileModifiedTick;
|
||||
@@ -269,7 +266,6 @@ public abstract partial class SharedMapSystem
|
||||
}
|
||||
case MapGridComponentState state:
|
||||
{
|
||||
modifiedChunks = new();
|
||||
DebugTools.Assert(component.ChunkSize == state.ChunkSize || component.Chunks.Count == 0,
|
||||
"Can't modify chunk size of an existing grid.");
|
||||
|
||||
@@ -279,12 +275,13 @@ public abstract partial class SharedMapSystem
|
||||
foreach (var index in component.Chunks.Keys)
|
||||
{
|
||||
if (!state.FullGridData.ContainsKey(index))
|
||||
ApplyChunkData(uid, component, index, ChunkDatum.Empty, modifiedChunks);
|
||||
ApplyChunkData(uid, component, index, ChunkDatum.Empty);
|
||||
}
|
||||
|
||||
foreach (var (index, data) in state.FullGridData)
|
||||
{
|
||||
ApplyChunkData(uid, component, index, new(data), modifiedChunks);
|
||||
DebugTools.Assert(!data.IsDeleted());
|
||||
ApplyChunkData(uid, component, index, data);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -309,10 +306,8 @@ public abstract partial class SharedMapSystem
|
||||
EntityUid uid,
|
||||
MapGridComponent component,
|
||||
Vector2i index,
|
||||
ChunkDatum data,
|
||||
HashSet<MapChunk> modifiedChunks)
|
||||
ChunkDatum data)
|
||||
{
|
||||
bool shapeChanged = false;
|
||||
var counter = 0;
|
||||
|
||||
if (data.IsDeleted())
|
||||
@@ -326,7 +321,7 @@ public abstract partial class SharedMapSystem
|
||||
{
|
||||
for (ushort y = 0; y < component.ChunkSize; y++)
|
||||
{
|
||||
if (!deletedChunk.TrySetTile(x, y, Tile.Empty, out var oldTile, out var chunkShapeChanged))
|
||||
if (!deletedChunk.TrySetTile(x, y, Tile.Empty, out var oldTile, out _))
|
||||
continue;
|
||||
|
||||
var gridIndices = deletedChunk.ChunkTileToGridTile((x, y));
|
||||
@@ -336,9 +331,6 @@ public abstract partial class SharedMapSystem
|
||||
}
|
||||
|
||||
component.Chunks.Remove(index);
|
||||
|
||||
// TODO is this required?
|
||||
modifiedChunks.Add(deletedChunk);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,30 +343,26 @@ public abstract partial class SharedMapSystem
|
||||
for (ushort y = 0; y < component.ChunkSize; y++)
|
||||
{
|
||||
var tile = data.TileData[counter++];
|
||||
if (!chunk.TrySetTile(x, y, tile, out var oldTile, out var tileShapeChanged))
|
||||
if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _))
|
||||
continue;
|
||||
|
||||
shapeChanged |= tileShapeChanged;
|
||||
var gridIndices = chunk.ChunkTileToGridTile((x, y));
|
||||
var newTileRef = new TileRef(uid, gridIndices, tile);
|
||||
_mapInternal.RaiseOnTileChanged(newTileRef, oldTile, index);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Fixtures != null && !chunk.Fixtures.SetEquals(data.Fixtures))
|
||||
// These should never refer to the same object
|
||||
DebugTools.AssertNotEqual(chunk.Fixtures, data.Fixtures);
|
||||
|
||||
if (!chunk.Fixtures.SetEquals(data.Fixtures))
|
||||
{
|
||||
chunk.Fixtures.Clear();
|
||||
|
||||
if (data.Fixtures != null)
|
||||
chunk.Fixtures.UnionWith(data.Fixtures);
|
||||
chunk.Fixtures.UnionWith(data.Fixtures);
|
||||
}
|
||||
|
||||
chunk.CachedBounds = data.CachedBounds!.Value;
|
||||
chunk.SuppressCollisionRegeneration = false;
|
||||
if (shapeChanged)
|
||||
{
|
||||
modifiedChunks.Add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGridGetState(EntityUid uid, MapGridComponent component, ref ComponentGetState args)
|
||||
@@ -429,7 +417,15 @@ public abstract partial class SharedMapSystem
|
||||
tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y);
|
||||
}
|
||||
}
|
||||
chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, chunk.Fixtures, chunk.CachedBounds));
|
||||
|
||||
// The client needs to clone the fixture set instead of storing a reference.
|
||||
// TODO Game State
|
||||
// Force the client to serialize & de-serialize implicitly generated component states.
|
||||
var fixtures = chunk.Fixtures;
|
||||
if (_netManager.IsClient)
|
||||
fixtures = new(fixtures);
|
||||
|
||||
chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +462,15 @@ public abstract partial class SharedMapSystem
|
||||
tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y);
|
||||
}
|
||||
}
|
||||
chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, chunk.Fixtures, chunk.CachedBounds));
|
||||
|
||||
// The client needs to clone the fixture set instead of storing a reference.
|
||||
// TODO Game State
|
||||
// Force the client to serialize & de-serialize implicitly generated component states.
|
||||
var fixtures = chunk.Fixtures;
|
||||
if (_netManager.IsClient)
|
||||
fixtures = new(fixtures);
|
||||
|
||||
chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds));
|
||||
}
|
||||
|
||||
args.State = new MapGridComponentState(component.ChunkSize, chunkData, component.LastTileModifiedTick);
|
||||
@@ -644,10 +648,12 @@ public abstract partial class SharedMapSystem
|
||||
|
||||
foreach (var id in mapChunk.Fixtures)
|
||||
{
|
||||
mapChunk.Fixtures.Remove(id);
|
||||
_fixtures.DestroyFixture(uid, id, false, manager: manager, body: body, xform: xform);
|
||||
}
|
||||
|
||||
RemoveChunk(uid, grid, mapChunk.Indices);
|
||||
DebugTools.AssertEqual(mapChunk.Fixtures.Count, 0);
|
||||
removedChunks.Add(mapChunk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,12 +322,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
case 1 << 2:
|
||||
{
|
||||
var state = new UserInterfaceStatesDeltaState()
|
||||
{
|
||||
States = new Dictionary<Enum, BoundUserInterfaceState>(ent.Comp.States),
|
||||
};
|
||||
var states = ent.Comp.States;
|
||||
|
||||
args.State = state;
|
||||
// TODO Game State
|
||||
// Force the client to serialize & de-serialize implicitly generated component states.
|
||||
if (_netManager.IsClient)
|
||||
states = new(states);
|
||||
|
||||
args.State = new UserInterfaceStatesDeltaState {States = states};
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -336,6 +338,8 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
var actors = new Dictionary<Enum, List<NetEntity>>();
|
||||
var dataCopy = new Dictionary<Enum, InterfaceData>(ent.Comp.Interfaces.Count);
|
||||
|
||||
// TODO Game State
|
||||
// Force the client to serialize & de-serialize implicitly generated component states.
|
||||
foreach (var (weh, a) in ent.Comp.Interfaces)
|
||||
{
|
||||
dataCopy[weh] = new InterfaceData(a);
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Robust.Shared.GameStates
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct ChunkDatum
|
||||
{
|
||||
public static readonly ChunkDatum Empty = new ChunkDatum();
|
||||
public static readonly ChunkDatum Empty = new();
|
||||
|
||||
public readonly HashSet<string>? Fixtures;
|
||||
|
||||
@@ -21,28 +21,12 @@ namespace Robust.Shared.GameStates
|
||||
|
||||
public readonly Box2i? CachedBounds;
|
||||
|
||||
[MemberNotNullWhen(false, nameof(TileData))]
|
||||
[MemberNotNullWhen(false, nameof(TileData), nameof(Fixtures))]
|
||||
public bool IsDeleted()
|
||||
{
|
||||
return TileData == null;
|
||||
}
|
||||
|
||||
internal ChunkDatum(ChunkDatum data)
|
||||
{
|
||||
if (data.TileData != null)
|
||||
{
|
||||
TileData = new Tile[data.TileData.Length];
|
||||
data.TileData.CopyTo(TileData, 0);
|
||||
}
|
||||
|
||||
if (data.Fixtures != null)
|
||||
{
|
||||
Fixtures = new HashSet<string>(data.Fixtures);
|
||||
}
|
||||
|
||||
CachedBounds = data.CachedBounds;
|
||||
}
|
||||
|
||||
private ChunkDatum(Tile[] tileData, HashSet<string> fixtures, Box2i cachedBounds)
|
||||
{
|
||||
TileData = tileData;
|
||||
|
||||
@@ -94,6 +94,12 @@ namespace Robust.Shared.Localization
|
||||
/// </summary>
|
||||
CultureInfo? DefaultCulture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the culture has been loaded.
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
bool HasCulture(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Load data for a culture.
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user