diff --git a/.editorconfig b/.editorconfig index ef3f4a74e..c2a9068d1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ resharper_place_field_attribute_on_same_line = if_owner_is_single_line resharper_wrap_chained_binary_patterns = chop_if_long resharper_wrap_chained_method_calls = chop_if_long -[*.{csproj,xml,yml,dll.config,targets,props}] +[*.{csproj,xml,yml,dll.config,targets,props,slnx}] indent_size = 2 [nuget.config] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 37e0c0c33..1a2a36762 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,5 +8,3 @@ *Command.cs @moonheart08 *Commands.cs @moonheart08 -# Physics -**/Robust.Shared/Physics/** @metalgearsloth diff --git a/.github/workflows/build-all-configurations.yml b/.github/workflows/build-all-configurations.yml index 4558c073e..2f209bda4 100644 --- a/.github/workflows/build-all-configurations.yml +++ b/.github/workflows/build-all-configurations.yml @@ -22,7 +22,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml index 314263fed..46f443ac2 100644 --- a/.github/workflows/build-docfx.yml +++ b/.github/workflows/build-docfx.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 613699a20..0082fb91f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -22,12 +22,10 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore - name: Build run: dotnet build --no-restore /p:WarningsAsErrors=nullable - - name: Robust.UnitTesting - run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0 - - name: Robust.Analyzers.Tests - run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0 + - name: Run tests + run: dotnet test --no-build -- NUnit.ConsoleOut=0 diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index 7d9377bcb..4767740a2 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Package client run: Tools/package_client_build.py diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index f66884ebb..395482bb9 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Disable submodule autoupdate run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE @@ -30,6 +30,8 @@ jobs: git fetch origin ${{ github.sha }} git checkout FETCH_HEAD git submodule update --init --recursive + - name: Replace global.json + run: cp RobustToolbox/global.json . - name: Install dependencies run: dotnet restore - name: Build diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..446a43ae8 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,27 @@ + + + + + compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers + + + + + + True + + + $(NoWarn);NU1510 + + diff --git a/Directory.Packages.props b/Directory.Packages.props index addbf98dd..d844ccbbf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,73 +10,75 @@ - + - - + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + - - - + + + - + - + - + - - + + - + + - - - + + + + - + - + - - - - - + + + + + diff --git a/Imports/Benchmarks.props b/Imports/Benchmarks.props new file mode 100644 index 000000000..5be70ac48 --- /dev/null +++ b/Imports/Benchmarks.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/Imports/Client.props b/Imports/Client.props new file mode 100644 index 000000000..bd45aa661 --- /dev/null +++ b/Imports/Client.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/Imports/Lidgren.props b/Imports/Lidgren.props new file mode 100644 index 000000000..7358309dc --- /dev/null +++ b/Imports/Lidgren.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/Imports/Packaging.props b/Imports/Packaging.props new file mode 100644 index 000000000..13f6d0d69 --- /dev/null +++ b/Imports/Packaging.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/Imports/Server.props b/Imports/Server.props new file mode 100644 index 000000000..174d03988 --- /dev/null +++ b/Imports/Server.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/Imports/Shared.props b/Imports/Shared.props new file mode 100644 index 000000000..208729fb5 --- /dev/null +++ b/Imports/Shared.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Imports/Testing.props b/Imports/Testing.props new file mode 100644 index 000000000..2e610b875 --- /dev/null +++ b/Imports/Testing.props @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Imports/WebView.props b/Imports/WebView.props new file mode 100644 index 000000000..545037ccd --- /dev/null +++ b/Imports/WebView.props @@ -0,0 +1,5 @@ + + + + + diff --git a/MSBuild/Robust.DefineConstants.targets b/MSBuild/Robust.DefineConstants.targets index 8055f669b..f67e982e5 100644 --- a/MSBuild/Robust.DefineConstants.targets +++ b/MSBuild/Robust.DefineConstants.targets @@ -13,7 +13,7 @@ - $(DefineConstants);LINUX;UNIX + $(DefineConstants);LINUX;UNIX;FREEDESKTOP diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 00a28fef6..beb41f6e9 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 267.3.0 + 270.0.0 diff --git a/MSBuild/Robust.Engine.props b/MSBuild/Robust.Engine.props index 939fa900b..e8e9a0210 100644 --- a/MSBuild/Robust.Engine.props +++ b/MSBuild/Robust.Engine.props @@ -1,8 +1,8 @@  - net9.0 - 13 + net10.0 + 14 enable nullable diff --git a/MSBuild/Robust.Platform.props b/MSBuild/Robust.Platform.props index 01702ec8c..e4c91cb38 100644 --- a/MSBuild/Robust.Platform.props +++ b/MSBuild/Robust.Platform.props @@ -31,5 +31,6 @@ python3 py -3 True + True diff --git a/MSBuild/Robust.ProjectReferences.targets b/MSBuild/Robust.ProjectReferences.targets new file mode 100644 index 000000000..b00dab6e9 --- /dev/null +++ b/MSBuild/Robust.ProjectReferences.targets @@ -0,0 +1,17 @@ + + + + + false + + + + + + diff --git a/MSBuild/Robust.Properties.targets b/MSBuild/Robust.Properties.targets index 613a1a0fb..6c6cbdbf5 100644 --- a/MSBuild/Robust.Properties.targets +++ b/MSBuild/Robust.Properties.targets @@ -3,8 +3,9 @@ - net9.0 + net10.0 true + true @@ -29,4 +30,18 @@ + + + + + + + + + + + diff --git a/NetSerializer b/NetSerializer index 84ab8fec6..61b47fbbb 160000 --- a/NetSerializer +++ b/NetSerializer @@ -1 +1 @@ -Subproject commit 84ab8fec649a1345a627366d161ab46ede748d93 +Subproject commit 61b47fbbbd15af3369e80d670e2959d83e826e2c diff --git a/OpenToolkit.GraphicsLibraryFramework/Cursor.cs b/OpenToolkit.GraphicsLibraryFramework/Cursor.cs deleted file mode 100644 index 4ee4de599..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Cursor.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// Cursor.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Opaque handle to a GLFW cursor. - /// - public struct Cursor - { - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/ClientApi.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/ClientApi.cs deleted file mode 100644 index b96629bc6..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/ClientApi.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// ClientApi.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// The context client APIs. - /// - /// - public enum ClientApi - { - /// - /// No context API is created. - /// - NoApi = 0, - - /// - /// OpenGL context is created. - /// - OpenGlApi = 0x00030001, - - /// - /// OpenGL ES context is created. - /// - OpenGlEsApi = 0x00030002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/ConnectedState.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/ConnectedState.cs deleted file mode 100644 index 7cec51798..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/ConnectedState.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// ConnectedState.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Specifies connected state of devices. - /// - public enum ConnectedState - { - /// - /// Indicates that a device is connected. - /// - Connected = 0x00040001, - - /// - /// Indicates that a device is disconnected. - /// - Disconnected = 0x00040002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/ContextApi.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/ContextApi.cs deleted file mode 100644 index f395917af..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/ContextApi.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// ContextApi.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// The context API used to create the window context. - /// - public enum ContextApi - { - /// - /// Uses the native context API to create the window context. - /// - NativeContextApi = 0x00036001, - - /// - /// Uses Egl to create the window context. - /// - EglContextApi = 0x00036002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorModeValue.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/CursorModeValue.cs deleted file mode 100644 index 90ab85306..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorModeValue.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// CursorModeValue.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// The GLFW cursor modes. - /// See cursor modes. - /// - public enum CursorModeValue - { - /// - /// The regular arrow cursor (or another cursor set with ) is used - /// and cursor motion is not limited. - /// - CursorNormal = 0x00034001, - - /// - /// Hides the arrow cursor when over a window. - /// - CursorHidden = 0x00034002, - - /// - /// Will hide the cursor and lock it to the specified window. - /// GLFW will then take care of all the details of cursor re-centering and offset calculation - /// and providing the application with a virtual cursor position. - /// This virtual position is provided normally via both the cursor position callback and through polling. - /// - CursorDisabled = 0x00034003 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorShape.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/CursorShape.cs deleted file mode 100644 index b49c5d852..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorShape.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// CursorShape.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Standard cursor shapes. - /// - public enum CursorShape - { - /// - /// The standard arrow shape. Used in almost all situations. - /// - Arrow = 0x00036001, - - /// - /// The I-Beam shape. Used when mousing over a place where text can be entered. - /// - IBeam = 0x00036002, - - /// - /// The crosshair shape. Used when dragging and dropping. - /// - Crosshair = 0x00036003, - - /// - /// The hand shape. Used when mousing over something that can be dragged around. - /// - Hand = 0x00036004, - - /// - /// The horizontal resize shape. Used when mousing over something that can be horizontally resized. - /// - HResize = 0x00036005, - - /// - /// The vertical resize shape. Used when mousing over something that can be vertically resized. - /// - VResize = 0x00036006 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorStateAttribute.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/CursorStateAttribute.cs deleted file mode 100644 index 9f083c918..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/CursorStateAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// CursorStateAttribute.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Attribute for setting of the cursor. - /// - /// - /// - public enum CursorStateAttribute - { - /// - /// Attribute for setting of the cursor. - /// - Cursor = 0x00033001, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/ErrorCode.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/ErrorCode.cs deleted file mode 100644 index c309b6d19..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/ErrorCode.cs +++ /dev/null @@ -1,103 +0,0 @@ -// -// ErrorCode.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Error codes, used in the error callback. - /// - public enum ErrorCode - { - /// - /// Everything is running as intended. Yay! - /// - NoError = 0, - - /// - /// Called a function before calling . Initialize GLFW and then try again. - /// - NotInitialized = 0x00010001, - - /// - /// No OpenGL/OpenGL ES context on this thread. - /// - NoContext = 0x00010002, - - /// - /// Used an invalid enum value on a function. - /// - /// - /// - /// This should hopefully never happen in the bindings, due to the added type safety of C# enums VS. GLFW's native #defines - /// - /// - InvalidEnum = 0x00010003, - - /// - /// Called a function with an invalid argument. - /// - /// - /// - /// This can happen if you request an OpenGL version that doesn't exist, like 2.7. - /// - /// - /// If you request a version of OpenGL that exists, but isn't supported by this graphics card, it will return VersionUnavailable instead. - /// - /// - InvalidValue = 0x00010004, - - /// - /// A memory allocation failed on GLFW's end. - /// - /// - /// - /// Report this to the GLFW issue tracker if encountered. - /// - /// - OutOfMemory = 0x00010005, - - /// - /// The requested API is not available on the system. - /// - ApiUnavailable = 0x00010006, - - /// - /// The requested OpenGL version is not available on the system. - /// - VersionUnavailable = 0x00010007, - - /// - /// A platform-specific error occurred that doesn't fit into any more specific category. - /// - /// - /// - /// Report this to the GLFW issue tracker if encountered. - /// - /// - PlatformError = 0x00010008, - - /// - /// The requested format is unavailable. - /// - /// - /// - /// If emitted during window creation, the requested pixel format isn't available. - /// - /// - /// If emitted when using the clipboard, the contents of the clipboard couldn't be converted to the requested format. - /// - /// - FormatUnavailable = 0x00010009, - - /// - /// There is no OpenGL/OpenGL ES context attached to this window. - /// - NoWindowContext = 0x0001000A - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintBool.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintBool.cs deleted file mode 100644 index 321b57848..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintBool.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// InitHint.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Initialization hints are set before and affect how the library behaves until termination. - /// Hints are set with . - /// - public enum InitHintBool - { - /// - /// Used to specify whether to also expose joystick hats as buttons, - /// for compatibility with earlier versions of GLFW that did not have - /// . - /// Set this with . - /// - JoystickHatButtons = 0x00050001, - - /// - /// Used to specify whether to set the current directory to the application to the Contents/Resources - /// subdirectory of the application's bundle, if present. - /// Set this with . - /// - /// - /// Only affects macOS; no effect on other platforms. - /// - CocoaChdirResources = 0x00051001, - - /// - /// Used to specify whether to create a basic menu bar, either from a nib or manually, - /// when the first window is created, which is when AppKit is initialized. - /// Set this with . - /// - /// - /// Only affects macOS; no effect on other platforms. - /// - CocoaMenubar = 0x00051002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintInt.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintInt.cs deleted file mode 100644 index 5c91f850a..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/InitHintInt.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Initialization hints are set before and affect how the library behaves until termination. - /// Hints are set with . - /// - /// - /// While this enum has no members, - /// it can still be useful because it allows you to access the direct glfwInitHint(int, int) API. - /// In case a future version of GLFW adds an int-taking int hint and we don't handle it. - /// - public enum InitHintInt - { - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/InputAction.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/InputAction.cs deleted file mode 100644 index 26360b990..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/InputAction.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// InputAction.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Defines event information for - /// or . - /// - public enum InputAction : byte - { - /// - /// The key or mouse button was released. - /// - Release = 0, - - /// - /// The key or mouse button was pressed. - /// - Press = 1, - - /// - /// The key was held down until it repeated. - /// - Repeat = 2 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/JoystickHats.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/JoystickHats.cs deleted file mode 100644 index 5fdabd4c1..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/JoystickHats.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Status of a joystick hat. - /// - public enum JoystickHats : byte - { - /// - /// Hat is centered. - /// - Centered = 0, - - /// - /// Hat is pointing up. - /// - Up = 1, - - /// - /// Hat is pointing right. - /// - Right = 2, - - /// - /// Hat is pointing down. - /// - Down = 4, - - /// - /// Hat is pointing left. - /// - Left = 8, - - /// - /// Hat is pointing up and to the right. - /// - RightUp = Right | Up, - - /// - /// Hat is pointing down and to the right. - /// - RightDown = Right | Down, - - /// - /// Hat is pointing up and to the left. - /// - LeftUp = Left | Up, - - /// - /// Hat is pointing down and to the left. - /// - LeftDown = Left | Down, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/KeyModifiers.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/KeyModifiers.cs deleted file mode 100644 index 92a571f7f..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/KeyModifiers.cs +++ /dev/null @@ -1,50 +0,0 @@ -// -// KeyModifiers.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Key modifiers, such as Shift or CTRL. - /// - [Flags] - public enum KeyModifiers : byte - { - /// - /// if one or more Shift keys were held down. - /// - Shift = 0x0001, - - /// - /// If one or more Control keys were held down. - /// - Control = 0x0002, - - /// - /// If one or more Alt keys were held down. - /// - Alt = 0x0004, - - /// - /// If one or more Super keys were held down. - /// - Super = 0x0008, - - /// - /// If caps lock is enabled. - /// - CapsLock = 0x0010, - - /// - /// If num lock is enabled. - /// - NumLock = 0x0020, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/Keys.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/Keys.cs deleted file mode 100644 index 99002820a..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/Keys.cs +++ /dev/null @@ -1,627 +0,0 @@ -// -// Keys.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Specifies key codes and modifiers in US keyboard layout. - /// - public enum Keys : short - { - /// - /// An unknown key. - /// - Unknown = -1, - - /// - /// The spacebar key. - /// - Space = 32, - - /// - /// The apostrophe key. - /// - Apostrophe = 39 /* ' */, - - /// - /// The comma key. - /// - Comma = 44 /* , */, - - /// - /// The minus key. - /// - Minus = 45 /* - */, - - /// - /// The period key. - /// - Period = 46 /* . */, - - /// - /// The slash key. - /// - Slash = 47 /* / */, - - /// - /// The 0 key. - /// - D0 = 48, - - /// - /// The 1 key. - /// - D1 = 49, - - /// - /// The 2 key. - /// - D2 = 50, - - /// - /// The 3 key. - /// - D3 = 51, - - /// - /// The 4 key. - /// - D4 = 52, - - /// - /// The 5 key. - /// - D5 = 53, - - /// - /// The 6 key. - /// - D6 = 54, - - /// - /// The 7 key. - /// - D7 = 55, - - /// - /// The 8 key. - /// - D8 = 56, - - /// - /// The 9 key. - /// - D9 = 57, - - /// - /// The semicolon key. - /// - Semicolon = 59 /* ; */, - - /// - /// The equal key. - /// - Equal = 61 /* = */, - - /// - /// The A key. - /// - A = 65, - - /// - /// The B key. - /// - B = 66, - - /// - /// The C key. - /// - C = 67, - - /// - /// The D key. - /// - D = 68, - - /// - /// The E key. - /// - E = 69, - - /// - /// The F key. - /// - F = 70, - - /// - /// The G key. - /// - G = 71, - - /// - /// The H key. - /// - H = 72, - - /// - /// The I key. - /// - I = 73, - - /// - /// The J key. - /// - J = 74, - - /// - /// The K key. - /// - K = 75, - - /// - /// The L key. - /// - L = 76, - - /// - /// The M key. - /// - M = 77, - - /// - /// The N key. - /// - N = 78, - - /// - /// The O key. - /// - O = 79, - - /// - /// The P key. - /// - P = 80, - - /// - /// The Q key. - /// - Q = 81, - - /// - /// The R key. - /// - R = 82, - - /// - /// The S key. - /// - S = 83, - - /// - /// The T key. - /// - T = 84, - - /// - /// The U key. - /// - U = 85, - - /// - /// The V key. - /// - V = 86, - - /// - /// The W key. - /// - W = 87, - - /// - /// The X key. - /// - X = 88, - - /// - /// The Y key. - /// - Y = 89, - - /// - /// The Z key. - /// - Z = 90, - - /// - /// The left bracket(opening bracket) key. - /// - LeftBracket = 91 /* [ */, - - /// - /// The backslash. - /// - Backslash = 92 /* \ */, - - /// - /// The right bracket(closing bracket) key. - /// - RightBracket = 93 /* ] */, - - /// - /// The grave accent key. - /// - GraveAccent = 96 /* ` */, - - /// - /// Non US keyboard layout key 1. - /// - World1 = 161 /* non-US #1 */, - - /// - /// Non US keyboard layout key 2. - /// - World2 = 162 /* non-US #2 */, - - /// - /// The escape key. - /// - Escape = 256, - - /// - /// The enter key. - /// - Enter = 257, - - /// - /// The tab key. - /// - Tab = 258, - - /// - /// The backspace key. - /// - Backspace = 259, - - /// - /// The insert key. - /// - Insert = 260, - - /// - /// The delete key. - /// - Delete = 261, - - /// - /// The right arrow key. - /// - Right = 262, - - /// - /// The left arrow key. - /// - Left = 263, - - /// - /// The down arrow key. - /// - Down = 264, - - /// - /// The up arrow key. - /// - Up = 265, - - /// - /// The page up key. - /// - PageUp = 266, - - /// - /// The page down key. - /// - PageDown = 267, - - /// - /// The home key. - /// - Home = 268, - - /// - /// The end key. - /// - End = 269, - - /// - /// The caps lock key. - /// - CapsLock = 280, - - /// - /// The scroll lock key. - /// - ScrollLock = 281, - - /// - /// The num lock key. - /// - NumLock = 282, - - /// - /// The print screen key. - /// - PrintScreen = 283, - - /// - /// The pause key. - /// - Pause = 284, - - /// - /// The F1 key. - /// - F1 = 290, - - /// - /// The F2 key. - /// - F2 = 291, - - /// - /// The F3 key. - /// - F3 = 292, - - /// - /// The F4 key. - /// - F4 = 293, - - /// - /// The F5 key. - /// - F5 = 294, - - /// - /// The F6 key. - /// - F6 = 295, - - /// - /// The F7 key. - /// - F7 = 296, - - /// - /// The F8 key. - /// - F8 = 297, - - /// - /// The F9 key. - /// - F9 = 298, - - /// - /// The F10 key. - /// - F10 = 299, - - /// - /// The F11 key. - /// - F11 = 300, - - /// - /// The F12 key. - /// - F12 = 301, - - /// - /// The F13 key. - /// - F13 = 302, - - /// - /// The F14 key. - /// - F14 = 303, - - /// - /// The F15 key. - /// - F15 = 304, - - /// - /// The F16 key. - /// - F16 = 305, - - /// - /// The F17 key. - /// - F17 = 306, - - /// - /// The F18 key. - /// - F18 = 307, - - /// - /// The F19 key. - /// - F19 = 308, - - /// - /// The F20 key. - /// - F20 = 309, - - /// - /// The F21 key. - /// - F21 = 310, - - /// - /// The F22 key. - /// - F22 = 311, - - /// - /// The F23 key. - /// - F23 = 312, - - /// - /// The F24 key. - /// - F24 = 313, - - /// - /// The F25 key. - /// - F25 = 314, - - /// - /// The 0 key on the key pad. - /// - KeyPad0 = 320, - - /// - /// The 1 key on the key pad. - /// - KeyPad1 = 321, - - /// - /// The 2 key on the key pad. - /// - KeyPad2 = 322, - - /// - /// The 3 key on the key pad. - /// - KeyPad3 = 323, - - /// - /// The 4 key on the key pad. - /// - KeyPad4 = 324, - - /// - /// The 5 key on the key pad. - /// - KeyPad5 = 325, - - /// - /// The 6 key on the key pad. - /// - KeyPad6 = 326, - - /// - /// The 7 key on the key pad. - /// - KeyPad7 = 327, - - /// - /// The 8 key on the key pad. - /// - KeyPad8 = 328, - - /// - /// The 9 key on the key pad. - /// - KeyPad9 = 329, - - /// - /// The decimal key on the key pad. - /// - KeyPadDecimal = 330, - - /// - /// The divide key on the key pad. - /// - KeyPadDivide = 331, - - /// - /// The multiply key on the key pad. - /// - KeyPadMultiply = 332, - - /// - /// The subtract key on the key pad. - /// - KeyPadSubtract = 333, - - /// - /// The add key on the key pad. - /// - KeyPadAdd = 334, - - /// - /// The enter key on the key pad. - /// - KeyPadEnter = 335, - - /// - /// The equal key on the key pad. - /// - KeyPadEqual = 336, - - /// - /// The left shift key. - /// - LeftShift = 340, - - /// - /// The left control key. - /// - LeftControl = 341, - - /// - /// The left alt key. - /// - LeftAlt = 342, - - /// - /// The left super key. - /// - LeftSuper = 343, - - /// - /// The right shift key. - /// - RightShift = 344, - - /// - /// The right control key. - /// - RightControl = 345, - - /// - /// The right alt key. - /// - RightAlt = 346, - - /// - /// The right super key. - /// - RightSuper = 347, - - /// - /// The menu key. - /// - Menu = 348, - - /// - /// The last valid key in this enum. - /// - LastKey = Menu - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/MouseButton.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/MouseButton.cs deleted file mode 100644 index 5fdad15e8..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/MouseButton.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Specifies the buttons of a mouse. - /// - public enum MouseButton : byte - { - /// - /// The first button. - /// - Button1 = 0, - - /// - /// The second button. - /// - Button2 = 1, - - /// - /// The third button. - /// - Button3 = 2, - - /// - /// The fourth button. - /// - Button4 = 3, - - /// - /// The fifth button. - /// - Button5 = 4, - - /// - /// The sixth button. - /// - Button6 = 5, - - /// - /// The seventh button. - /// - Button7 = 6, - - /// - /// The eighth button. - /// - Button8 = 7, - - /// - /// The left mouse button. This corresponds to . - /// - Left = Button1, - - /// - /// The right mouse button. This corresponds to . - /// - Right = Button2, - - /// - /// The middle mouse button. This corresponds to . - /// - Middle = Button3, - - /// - /// The highest mouse button available. - /// - Last = Button8, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/OpenGlProfile.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/OpenGlProfile.cs deleted file mode 100644 index dab4a6df6..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/OpenGlProfile.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// OpenGlProfile.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// The OpenGL context profiles. - /// - public enum OpenGlProfile - { - /// - /// Used for unknown OpenGL profile or OpenGL ES. - /// - Any = 0, - - /// - /// Known OpenGL Core profile. - /// - Core = 0x00032001, - - /// - /// Known OpenGL compatibility profile. - /// - Compat = 0x00032002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/ReleaseBehavior.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/ReleaseBehavior.cs deleted file mode 100644 index 632f98a09..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/ReleaseBehavior.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// ReleaseBehavior.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// The context release behaviors. - /// - /// - public enum ReleaseBehavior - { - /// - /// Use the default release behavior of the platform. - /// - Any = 0, - - /// - /// The pipeline will be flushed whenever the context is released from being the current one. - /// - Flush = 0x00035001, - - /// - /// The pipeline will not be flushed on release. - /// - None = 0x00035002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/Robustness.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/Robustness.cs deleted file mode 100644 index 8499b7ed9..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/Robustness.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// Robustness.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// OpenGL context robustness strategy. - /// - public enum Robustness - { - /// - /// No context robustness strategy. - /// - NoRobustness = 0, - - /// - /// Robust context without a reset notification. - /// - NoResetNotification = 0x00031001, - - /// - /// Lose context on reset. - /// - LoseContextOnReset = 0x00031002 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/StickyAttributes.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/StickyAttributes.cs deleted file mode 100644 index c76a1f435..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/StickyAttributes.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// StickyAttributes.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Attributes related to sticky keys and buttons. - /// - /// - /// - public enum StickyAttributes - { - /// - /// Specify whether keyboard input should be sticky or not. - /// - StickyKeys = 0x00033002, - - /// - /// Specify whether mouse button input should be sticky or not. - /// - StickyMouseButtons = 0x00033003 - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeGetter.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeGetter.cs deleted file mode 100644 index 967dbc100..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeGetter.cs +++ /dev/null @@ -1,96 +0,0 @@ -// -// WindowAttributeSetter.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to get window related attributes. - /// - /// - public enum WindowAttributeGetter - { - /// - /// Indicates whether the specified window has input focus. - /// Initial input focus is controlled by the window hint with the same name - /// - Focused = WindowHintBool.Focused, - - /// - /// Indicates whether the specified window is iconified, - /// whether by the user or with . - /// - Iconified = WindowHintBool.Iconified, - - /// - /// Indicates whether the specified window is resizable by the user. - /// This is set on creation with the window hint with the same name. - /// - Resizable = WindowHintBool.Resizable, - - /// - /// Indicates whether the specified window is visible. - /// Window visibility can be controlled with and - /// and initial visibility is controlled by the window hint with the same name. - /// - Visible = WindowHintBool.Visible, - - /// - /// Indicates whether the specified window has decorations such as a border,a close widget, etc. - /// This is set on creation with the window hint with the same name. - /// - Decorated = WindowHintBool.Decorated, - - /// - /// Specifies whether the full screen window will automatically iconify and restore - /// the previous video mode on input focus loss. - /// Possible values are true and false. This hint is ignored for windowed mode windows. - /// - AutoIconify = WindowHintBool.AutoIconify, - - /// - /// Indicates whether the specified window is floating, also called topmost or always-on-top. - /// This is controlled by the window hint with the same name. - /// - Floating = WindowHintBool.Floating, - - /// - /// Indicates whether the specified window is maximized, - /// whether by the user or with . - /// - Maximized = WindowHintBool.Maximized, - - /// - /// Specifies whether the cursor should be centered over newly created full screen windows. - /// Possible values are true and false. This hint is ignored for windowed mode windows. - /// - CenterCursor = WindowHintBool.CenterCursor, - - /// - /// Specifies whether the window framebuffer will be transparent. - /// If enabled and supported by the system, the window framebuffer alpha channel will be used - /// to combine the framebuffer with the background. - /// This does not affect window decorations. Possible values are true and false. - /// - TransparentFramebuffer = WindowHintBool.TransparentFramebuffer, - - /// - /// indicates whether the cursor is currently directly over the client area of the window, - /// with no other windows between. - /// See Cursor enter/leave events - /// for details. - /// - Hovered = WindowHintBool.Hovered, - - /// - /// Specifies whether the window will be given input focus when is called. - /// Possible values are true and false. - /// - FocusOnShow = WindowHintBool.FocusOnShow, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeSetter.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeSetter.cs deleted file mode 100644 index 4f7b9beaa..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowAttributeSetter.cs +++ /dev/null @@ -1,49 +0,0 @@ -// -// WindowAttributeSetter.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to set window related attributes. - /// - /// - public enum WindowAttributeSetter - { - /// - /// Indicates whether the specified window is resizable by the user. - /// This is set on creation with the window hint with the same name. - /// - Resizable = WindowHintBool.Resizable, - - /// - /// Indicates whether the specified window has decorations such as a border,a close widget, etc. - /// This is set on creation with the window hint with the same name. - /// - Decorated = WindowHintBool.Decorated, - - /// - /// Specifies whether the full screen window will automatically iconify and restore - /// the previous video mode on input focus loss. - /// Possible values are true and false. This hint is ignored for windowed mode windows. - /// - AutoIconify = WindowHintBool.AutoIconify, - - /// - /// Indicates whether the specified window is floating, also called topmost or always-on-top. - /// This is controlled by the window hint with the same name. - /// - Floating = WindowHintBool.Floating, - - /// - /// Specifies whether the window will be given input focus when is called. - /// Possible values are true and false. - /// - FocusOnShow = WindowHintBool.FocusOnShow - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintBool.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintBool.cs deleted file mode 100644 index 5bf895d73..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintBool.cs +++ /dev/null @@ -1,135 +0,0 @@ -// -// WindowHintBool.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Context related boolean attributes. - /// - /// - public enum WindowHintBool - { - /// - /// Indicates whether the specified window has input focus. - /// Initial input focus is controlled by the window hint with the same name - /// - Focused = 0x00020001, - - /// - /// Indicates whether the specified window is iconified, - /// whether by the user or with . - /// - Iconified = 0x00020002, - - /// - /// Indicates whether the specified window is resizable by the user. - /// This is set on creation with the window hint with the same name. - /// - Resizable = 0x00020003, - - /// - /// Indicates whether the specified window is visible. - /// Window visibility can be controlled with and - /// and initial visibility is controlled by the window hint with the same name. - /// - Visible = 0x00020004, - - /// - /// Indicates whether the specified window has decorations such as a border,a close widget, etc. - /// This is set on creation with the window hint with the same name. - /// - Decorated = 0x00020005, - - /// - /// Specifies whether the full screen window will automatically iconify and restore - /// the previous video mode on input focus loss. - /// Possible values are true and false. This hint is ignored for windowed mode windows. - /// - AutoIconify = 0x00020006, - - /// - /// Indicates whether the specified window is floating, also called topmost or always-on-top. - /// This is controlled by the window hint with the same name. - /// - Floating = 0x00020007, - - /// - /// Indicates whether the specified window is maximized, - /// whether by the user or with . - /// - Maximized = 0x00020008, - - /// - /// Specifies whether the cursor should be centered over newly created full screen windows. - /// Possible values are true and false. This hint is ignored for windowed mode windows. - /// - CenterCursor = 0x00020009, - - /// - /// Specifies whether the window framebuffer will be transparent. - /// If enabled and supported by the system, the window framebuffer alpha channel will be used - /// to combine the framebuffer with the background. - /// This does not affect window decorations. Possible values are true and false. - /// - TransparentFramebuffer = 0x0002000A, - - /// - /// Indicates whether the cursor is currently directly over the client area of the window, - /// with no other windows between. - /// See Cursor enter/leave events - /// for details. - /// - Hovered = 0x0002000B, - - /// - /// Specifies whether the window will be given input focus when is called. - /// Possible values are true and false. - /// - FocusOnShow = 0x0002000C, - - /// - /// Specifies whether the window's context is an OpenGL forward-compatible one. - /// Possible values are true and false. - /// - OpenGLForwardCompat = 0x00022006, - - /// - /// Specifies whether the window's context is an OpenGL debug context. - /// Possible values are true and false. - /// - OpenGLDebugContext = 0x00022007, - - /// - /// Specifies whether errors should be generated by the context. - /// If enabled, situations that would have generated errors instead cause undefined behavior. - /// - ContextNoError = 0x0002200A, - - /// - /// Specifies whether to use stereoscopic rendering. This is a hard constraint. - /// - Stereo = 0x0002100C, - - /// - /// Specifies whether the framebuffer should be double buffered. - /// You nearly always want to use double buffering. This is a hard constraint. - /// - DoubleBuffer = 0x00021010, - - /// - /// Specifies whether the framebuffer should be sRGB capable. - /// If supported, a created OpenGL context will support the - /// GL_FRAMEBUFFER_SRGB enable( also called GL_FRAMEBUFFER_SRGB_EXT) - /// for controlling sRGB rendering and a created OpenGL ES context will always have sRGB rendering enabled. - /// - SrgbCapable = 0x0002100E, - - ScaleToMonitor = 0x0002200C, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintClientApi.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintClientApi.cs deleted file mode 100644 index 287d02095..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintClientApi.cs +++ /dev/null @@ -1,26 +0,0 @@ -// -// WindowHintClientApi.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Context related client API attribute. - /// - /// - public enum WindowHintClientApi - { - /// - /// Indicates the client API provided by the window's context; - /// either , - /// or - /// . - /// - ClientApi = 0x00022001, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintContextApi.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintContextApi.cs deleted file mode 100644 index 8eafb8c8e..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintContextApi.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// WindowHintContextApi.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to specify the context creation API. - /// - /// - public enum WindowHintContextApi - { - /// - /// Indicates the context creation API used to create the window's context; - /// either or . - /// - ContextCreationApi = 0x0002200B, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintInt.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintInt.cs deleted file mode 100644 index 0f37424f8..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintInt.cs +++ /dev/null @@ -1,117 +0,0 @@ -// -// WindowHintInt.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Context related attributes. - /// - /// - public enum WindowHintInt - { - /// - /// Indicate the client API version(major part) of the window's context. - /// - ContextVersionMajor = 0x00022002, - - /// - /// Indicate the client API version(minor part) of the window's context. - /// - ContextVersionMinor = 0x00022003, - - /// - /// Indicate the client API version(revision part) of the window's context. - /// - ContextRevision = 0x00022004, - - /// - /// Specify the desired bit depths of the red component of the default framebuffer. - /// means the application has no preference. - /// - RedBits = 0x00021001, - - /// - /// Specify the desired bit depths of the green component of the default framebuffer. - /// means the application has no preference. - /// - GreenBits = 0x00021002, - - /// - /// Specify the desired bit depths of the blue component of the default framebuffer. - /// means the application has no preference. - /// - BlueBits = 0x00021003, - - /// - /// Specify the desired bit depths of the alpha component of the default framebuffer. - /// means the application has no preference. - /// - AlphaBits = 0x00021004, - - /// - /// Specify the desired bit depths of the depth component of the default framebuffer. - /// means the application has no preference. - /// - DepthBits = 0x00021005, - - /// - /// Specify the desired bit depths of the stencil component of the default framebuffer. - /// means the application has no preference. - /// - StencilBits = 0x00021006, - - /// - /// Specify the desired bit depths of the red component of the accumulation buffer. - /// means the application has no preference. - /// - /// Accumulation buffers are a legacy OpenGL feature and should not be used in new code. - AccumRedBits = 0x00021007, - - /// - /// Specify the desired bit depths of the green component of the accumulation buffer. - /// means the application has no preference. - /// - /// Accumulation buffers are a legacy OpenGL feature and should not be used in new code. - AccumGreenBits = 0x00021008, - - /// - /// Specify the desired bit depths of the blue component of the accumulation buffer. - /// means the application has no preference. - /// - /// Accumulation buffers are a legacy OpenGL feature and should not be used in new code. - AccumBlueBits = 0x00021009, - - /// - /// Specify the desired bit depths of the alpha component of the accumulation buffer. - /// means the application has no preference. - /// - /// Accumulation buffers are a legacy OpenGL feature and should not be used in new code. - AccumAlphaBits = 0x0002100A, - - /// - /// Specifies the desired number of auxiliary buffers. - /// means the application has no preference. - /// - /// Auxiliary buffers are a legacy OpenGL feature and should not be used in new code. - AuxBuffers = 0x0002100B, - - /// - /// Specifies the desired number of samples to use for multisampling. Zero disables multisampling. - /// means the application has no preference. - /// - Samples = 0x0002100D, - - /// - /// Specifies the desired refresh rate for full screen windows. - /// If set to , - /// the highest available refresh rate will be used. This hint is ignored for windowed mode windows. - /// - RefreshRate = 0x0002100F, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintOpenGlProfile.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintOpenGlProfile.cs deleted file mode 100644 index 2264beb6a..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintOpenGlProfile.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// WindowHintOpenGlProfile.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to set the OpenGlProfile attribute. - /// - /// - public enum WindowHintOpenGlProfile - { - /// - /// Indicates the OpenGL profile used by the context. - /// This is - /// or - /// if the context uses a known profile, or - /// if the OpenGL profile is unknown or the context is an OpenGL ES context. - /// Note that the returned profile may not match the profile bits of the context flags, - /// as GLFW will try other means of detecting the profile when no bits are set. TODO: enum for missing crefs - /// - OpenGlProfile = 0x00022008, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintReleaseBehavior.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintReleaseBehavior.cs deleted file mode 100644 index 80d2bf8b6..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintReleaseBehavior.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// WindowHintReleaseBehavior.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to specify the release behavior used by the local context. - /// - /// - public enum WindowHintReleaseBehavior - { - /// - /// Specifies the release behavior to be used by the context. - /// Possible values are one of , - /// or . - /// If the behavior is , the default behavior - /// of the context creation API will be used. - /// If the behavior is , the pipeline will be flushed - /// whenever the context is released from being the current one. - /// If the behavior is , the pipeline will not be flushed on release. - /// - ContextReleaseBehavior = 0x00022009, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintRobustness.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintRobustness.cs deleted file mode 100644 index e4da30660..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintRobustness.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -// WindowHintRobustness.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Used to set context robustness attribute. - /// - /// - public enum WindowHintRobustness - { - /// - /// Indicates the robustness strategy used by the context. - /// This is or - /// if the window's context supports robustness, or otherwise. - /// - ContextRobustness = 0x00022005, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintString.cs b/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintString.cs deleted file mode 100644 index 5ddc1c208..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Enums/WindowHintString.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Window hints for the WindowHintString function. - /// - public enum WindowHintString - { - /// - /// Sets the frame name on Cocoa. On any other platform, this does nothing. - /// - CocoaFrameName = 0x00023002, - - /// - /// Sets the class name on X11. On any other platform, this does nothing. - /// - X11ClassName = 0x00024001, - - /// - /// Sets the instance name on X11. on any other platform, this does nothing. - /// - X11InstanceName = 0x00024002, - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GLFW.cs b/OpenToolkit.GraphicsLibraryFramework/GLFW.cs deleted file mode 100644 index 1d9d66200..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GLFW.cs +++ /dev/null @@ -1,5564 +0,0 @@ -// -// GLFW.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System; -using System.Runtime.InteropServices; -using static OpenToolkit.GraphicsLibraryFramework.GLFWNative; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Provides access to the GLFW API. - /// - public static class GLFW - { - // XML-documentation is from https://www.glfw.org/docs/latest/ - // Still missing in documentation - - /// - /// Gets an integer equal to GLFW_DONT_CARE. This can be used for several window hints to use the platform default. - /// - public const int DontCare = -1; - - /// - /// - /// This function initialwizes the GLFW library. Before most GLFW functions can be used, - /// GLFW must be initialized, and before an application terminates GLFW should be terminated in order to - /// free any resources allocated during or after initialization. - /// - /// - /// If this function fails, it calls before returning. - /// - /// - /// If it succeeds, you should call before the application exits. - /// - /// - /// Additional calls to this function after successful initialization - /// but before termination will return true immediately. - /// - /// - /// true if successful, or false if an error occurred. - /// - /// - /// OS X: This function will change the current directory of the application - /// to the Contents/Resources subdirectory of the application's bundle, if present. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static bool Init() => glfwInit() == GLFW_TRUE; - - /// - /// - /// This function destroys all remaining windows and cursors, restores any modified gamma ramps - /// and frees any other allocated resources. Once this function is called, - /// you must again call successfully before you will be able to use most GLFW functions. - /// - /// - /// If GLFW has been successfully initialized, this function should be called before the application exits. - /// - /// - /// If initialization fails, there is no need to call this function, - /// as it is called by before it returns failure. - /// - /// - /// - /// - /// The contexts of any remaining windows must not be current on any other thread when this function is called. - /// - /// - /// This function may be called before . - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static void Terminate() => glfwTerminate(); - - /// - /// - /// This function sets hints for the next initialization of GLFW. - /// - /// - /// The values you set hints to are never reset by GLFW, but they only take effect during initialization. - /// - /// - /// Once GLFW has been initialized, - /// any values you set will be ignored until the library is terminated and initialized again. - /// - /// Some hints are platform specific. - /// These may be set on any platform but they will only affect their specific platform. - /// Other platforms will ignore them. Setting these hints requires no platform specific headers or functions. - /// - /// - /// The to set. - /// The new value of the . - /// - /// - /// This function may be called before . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static void InitHint(InitHintBool hintBool, bool value) => - glfwInitHint((int)hintBool, value ? GLFW_TRUE : GLFW_FALSE); - - /// - /// - /// This function sets hints for the next initialization of GLFW. - /// - /// - /// The values you set hints to are never reset by GLFW, but they only take effect during initialization. - /// - /// - /// Once GLFW has been initialized, - /// any values you set will be ignored until the library is terminated and initialized again. - /// - /// Some hints are platform specific. - /// These may be set on any platform but they will only affect their specific platform. - /// Other platforms will ignore them. Setting these hints requires no platform specific headers or functions. - /// - /// - /// The to set. - /// The new value of the . - /// - /// - /// This function may be called before . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static void InitHint(InitHintInt hintInt, int value) => glfwInitHint((int)hintInt, value); - - /// - /// - /// This function retrieves the major, minor and revision numbers of the GLFW library. - /// It is intended for when you are using GLFW - /// as a shared library and want to ensure that you are using the minimum required version. - /// - /// - /// Any or all of the version arguments may be out _. - /// - /// - /// Where to store the major version number, or out _. - /// Where to store the minor version number, or out _. - /// Where to store the revision number, or out _. - /// - /// - /// This function may be called before . - /// - /// - /// This function may be called from any thread. - /// - /// - public static unsafe void GetVersion(out int major, out int minor, out int revision) - { - int localMajor, localMinor, localRevision; - glfwGetVersion(&localMajor, &localMinor, &localRevision); - major = localMajor; - minor = localMinor; - revision = localRevision; - } - - /// - /// - /// This function returns the compile-time generated version string of the GLFW library binary. - /// It describes the version, platform, compiler and any platform-specific compile-time options. - /// It should not be confused with the OpenGL or OpenGL ES version string, queried with glGetString. - /// - /// - /// Do not use the version string to parse the GLFW library version. - /// The function provides the version of the running library binary in numerical format. - /// - /// - /// The ASCII-encoded GLFW version string. - /// - /// - /// This function may be called before . - /// - /// - /// The returned string is static and compile-time generated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// - public static unsafe string GetVersionString() - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetVersionString()); - } - - /// - /// - /// This function returns the compile-time generated version string of the GLFW library binary. - /// It describes the version, platform, compiler and any platform-specific compile-time options. - /// It should not be confused with the OpenGL or OpenGL ES version string, queried with glGetString. - /// - /// - /// Do not use the version string to parse the GLFW library version. - /// The function provides the version of the running library binary in numerical format. - /// - /// - /// The ASCII-encoded GLFW version string. - /// - /// - /// This function may be called before . - /// - /// - /// The returned string is static and compile-time generated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// - public static unsafe byte* GetVersionStringRaw() - { - return glfwGetVersionString(); - } - - /// - /// - /// This function returns and clears the error code of the last error that occurred on the calling thread, - /// and optionally a UTF-8 encoded human-readable description of it. - /// - /// - /// If no error has occurred since the last call, - /// it returns (zero) and the description pointer is set to null. - /// - /// - /// Where to store the error description pointer, or out _"/>. - /// The last error code for the calling thread, or (zero). - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is only guaranteed to be valid until the next error occurs or the library is terminated. - /// - /// - /// This function may be called before . - /// - /// - /// This function may be called from any thread. - /// - /// - /// - public static unsafe ErrorCode GetError(out string description) - { - byte* desc; - var code = glfwGetError(&desc); - description = Marshal.PtrToStringUTF8((IntPtr) desc); - return code; - } - - /// - /// - /// This function returns and clears the error code of the last error that occurred on the calling thread, - /// and optionally a UTF-8 encoded human-readable description of it. - /// - /// - /// If no error has occurred since the last call, - /// it returns (zero) and the description pointer is set to null. - /// - /// - /// Where to store the error description pointer, or out _"/>. - /// The last error code for the calling thread, or (zero). - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is only guaranteed to be valid until the next error occurs or the library is terminated. - /// - /// - /// This function may be called before . - /// - /// - /// This function may be called from any thread. - /// - /// - /// - public static unsafe ErrorCode GetErrorRaw(out byte* description) - { - byte* desc; - var code = glfwGetError(&desc); - description = desc; - return code; - } - - /// - /// - /// This function returns an array of handles for all currently connected monitors. - /// The primary monitor is always first in the returned array. - /// - /// - /// If no monitors were found, this function returns null. - /// - /// - /// - /// Where to store the number of monitors in the returned array. This is set to zero if an error occurred. - /// - /// - /// An array of monitor handles, or null if no monitors were found or if an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is only guaranteed to be valid until the monitor configuration changes or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - /// - public static unsafe Monitor** GetMonitorsRaw(out int count) - { - fixed (int* ptr = &count) - { - return glfwGetMonitors(ptr); - } - } - - /// - /// - /// This function returns an array of handles for all currently connected monitors. - /// The primary monitor is always first in the returned array. - /// - /// - /// If no monitors were found, this function returns null. - /// - /// - /// - /// Where to store the number of monitors in the returned array. This is set to zero if an error occurred. - /// - /// - /// An array of monitor handles, or null if no monitors were found or if an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is only guaranteed to be valid until the monitor configuration changes or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - /// - public static unsafe Monitor** GetMonitorsRaw(int* count) - { - return glfwGetMonitors(count); - } - - /// - /// - /// This function returns an array of handles for all currently connected monitors. - /// The primary monitor is always first in the returned array. - /// - /// - /// If no monitors were found, this function returns null. - /// - /// - /// - /// An array of monitor handles, or null if no monitors were found or if an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is only guaranteed to be valid until the monitor configuration changes or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - /// - public static unsafe Monitor*[] GetMonitors() - { - var ptr = GetMonitorsRaw(out var count); - - if (ptr == null) - { - return null; - } - - var array = new Monitor*[count]; - - for (var i = 0; i < count; i++) - { - array[i] = ptr[i]; - } - - return array; - } - - /// - /// - /// This function returns the position, in screen coordinates, of the upper-left corner of the specified monitor. - /// - /// - /// The monitor to query. - /// Where to store the monitor x-coordinate, or out _. - /// Where to store the monitor y-coordinate, or out _. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetMonitorPos(Monitor* monitor, out int x, out int y) - { - int localX, localY; - - glfwGetMonitorPos(monitor, &localX, &localY); - - x = localX; - y = localY; - } - - /// - /// - /// This function returns the position, in screen coordinates, of the upper-left corner of the specified monitor. - /// - /// - /// The monitor to query. - /// Where to store the monitor x-coordinate. - /// Where to store the monitor y-coordinate. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetMonitorPos(Monitor* monitor, int* x, int* y) - { - glfwGetMonitorPos(monitor, x, y); - } - - /// - /// - /// This function returns the size, in millimetres, of the display area of the specified monitor. - /// - /// - /// Some systems do not provide accurate monitor size information, - /// either because the monitor EDID(Extended Display Identification Data) data is incorrect - /// or because the driver does not report it accurately. - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The monitor to query. - /// - /// Where to store the width, in millimetres, of the monitor's display area, or out _. - /// - /// - /// Where to store the height, in millimetres, of the monitor's display area, or out _. - /// - /// - /// - /// Windows: calculates the returned physical size from the current resolution - /// and system DPI instead of querying the monitor EDID data. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void GetMonitorPhysicalSize(Monitor* monitor, out int width, out int height) - { - int localWidth, localHeight; - - glfwGetMonitorPhysicalSize(monitor, &localWidth, &localHeight); - - width = localWidth; - height = localHeight; - } - - /// - /// - /// This function retrieves the content scale for the specified monitor. - /// - /// - /// The content scale is the ratio between the current DPI and the platform's default DPI. - /// - /// - /// If you scale all pixel dimensions by this scale then your content should appear at an appropriate size. - /// This is especially important for text and any UI elements. - /// - /// - /// - /// The content scale may depend on both the monitor resolution and pixel density and on user settings. - /// It may be very different from the raw DPI calculated from the physical size and current resolution. - /// - /// - /// The monitor to query. - /// Where to store the x-axis content scale, or out _. - /// Where to store the y-axis content scale, or out _. - public static unsafe void GetMonitorContentScale(Monitor* monitor, out float xScale, out float yScale) - { - float localX, localY; - - glfwGetMonitorContentScale(monitor, &localX, &localY); - - xScale = localX; - yScale = localY; - } - - /// - /// - /// This function retrieves the content scale for the specified monitor. - /// - /// - /// The content scale is the ratio between the current DPI and the platform's default DPI. - /// - /// - /// If you scale all pixel dimensions by this scale then your content should appear at an appropriate size. - /// This is especially important for text and any UI elements. - /// - /// - /// - /// The content scale may depend on both the monitor resolution and pixel density and on user settings. - /// It may be very different from the raw DPI calculated from the physical size and current resolution. - /// - /// - /// The monitor to query. - /// Where to store the x-axis content scale, or out _. - /// Where to store the y-axis content scale, or out _. - public static unsafe void GetMonitorContentScaleRaw(Monitor* monitor, float* xScale, float* yScale) - { - glfwGetMonitorContentScale(monitor, xScale, yScale); - } - - /// - /// - /// This function returns a human-readable name, encoded as UTF-8, of the specified monitor. - /// The name typically reflects the make and model of the monitor - /// and is not guaranteed to be unique among the connected monitors. - /// - /// - /// The monitor to query. - /// The UTF-8 encoded name of the monitor, or null if an error occurred. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified monitor is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe string GetMonitorName(Monitor* monitor) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetMonitorName(monitor)); - } - - /// - /// - /// This function returns a human-readable name, encoded as UTF-8, of the specified monitor. - /// The name typically reflects the make and model of the monitor - /// and is not guaranteed to be unique among the connected monitors. - /// - /// - /// The monitor to query. - /// The UTF-8 encoded name of the monitor, or null if an error occurred. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified monitor is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe byte* GetMonitorNameRaw(Monitor* monitor) - { - return glfwGetMonitorName(monitor); - } - - /// - /// - /// This function sets the user-defined pointer of the specified monitor. - /// The current value is retained until the monitor is disconnected. - /// The initial value is . - /// - /// - /// This function may be called from the monitor callback, even for a monitor that is being disconnected. - /// - /// - /// The monitor whose pointer to set. - /// The new value. - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void SetMonitorUserPointer(Monitor* monitor, void* pointer) - { - glfwSetMonitorUserPointer(monitor, pointer); - } - - /// - /// - /// This function returns the current value of the user-defined pointer of the specified monitor. - /// The initial value is . - /// - /// - /// This function may be called from the monitor callback, even for a monitor that is being disconnected. - /// - /// - /// The monitor whose pointer to return. - /// The user-defined pointer of the given . - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void* GetMonitorUserPointer(Monitor* monitor) - { - return glfwGetMonitorUserPointer(monitor); - } - - /// - /// - /// This function returns an array of all video modes supported by the specified monitor. - /// The returned array is sorted in ascending order, first by color bit depth (the sum of all channel depths) - /// and then by resolution area (the product of width and height). - /// - /// - /// The monitor to query. - /// - /// Where to store the number of video modes in the returned array. - /// This is set to zero if an error occurred. - /// - /// An array of video modes, or null if an error occurred. - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified monitor is disconnected, - /// this function is called again for that monitor, or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe VideoMode* GetVideoModesRaw(Monitor* monitor, out int count) - { - fixed (int* ptr = &count) - { - return glfwGetVideoModes(monitor, ptr); - } - } - - /// - /// - /// This function returns an array of all video modes supported by the specified monitor. - /// The returned array is sorted in ascending order, first by color bit depth (the sum of all channel depths) - /// and then by resolution area (the product of width and height). - /// - /// - /// The monitor to query. - /// - /// Where to store the number of video modes in the returned array. - /// This is set to zero if an error occurred. - /// - /// An array of video modes, or null if an error occurred. - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified monitor is disconnected, - /// this function is called again for that monitor, or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe VideoMode* GetVideoModesRaw(Monitor* monitor, int* count) - { - return glfwGetVideoModes(monitor, count); - } - - /// - /// - /// This function returns an array of all video modes supported by the specified monitor. - /// The returned array is sorted in ascending order, first by color bit depth (the sum of all channel depths) - /// and then by resolution area (the product of width and height). - /// - /// - /// The monitor to query. - /// An array of video modes, or null if an error occurred. - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified monitor is disconnected, - /// this function is called again for that monitor, or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe VideoMode[] GetVideoModes(Monitor* monitor) - { - var ptr = GetVideoModesRaw(monitor, out var count); - - if (ptr == null) - { - return null; - } - - var array = new VideoMode[count]; - for (var i = 0; i < count; i++) - { - array[i] = ptr[i]; - } - - return array; - } - - /// - /// - /// This function generates a 256-element gamma ramp from the specified exponent and then calls - /// with it. The value must be a finite number greater than zero. - /// - /// - /// The monitor whose gamma ramp to set. - /// The desired exponent. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SetGamma(Monitor* monitor, float gamma) - { - glfwSetGamma(monitor, gamma); - } - - /// - /// - /// This function returns the current gamma ramp of the specified monitor. - /// - /// - /// The monitor to query. - /// The current gamma ramp, or null if an error occurred. - /// - /// - /// The returned structure and its arrays are allocated and freed by GLFW. - /// You should not free them yourself. They are valid until the specified monitor is disconnected, - /// this function is called again for that monitor or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe GammaRamp* GetGammaRamp(Monitor* monitor) - { - return glfwGetGammaRamp(monitor); - } - - /// - /// - /// This function sets the current gamma ramp for the specified monitor. - /// - /// - /// The original gamma ramp for that monitor - /// is saved by GLFW the first time this function is called and is restored by . - /// - /// - /// The monitor whose gamma ramp to set. - /// The gamma ramp to use. - /// - /// - /// Gamma ramp sizes other than 256 are not supported by all platforms or graphics hardware. - /// - /// - /// Windows: The gamma ramp size must be 256. - /// - /// - /// The specified gamma ramp is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void SetGammaRamp(Monitor* monitor, GammaRamp* ramp) - { - glfwSetGammaRamp(monitor, ramp); - } - - /// - /// - /// This function resets all window hints to their default values. - /// - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static void DefaultWindowHints() => glfwDefaultWindowHints(); - - /// - /// - /// Sets the specified window hint to the desired value. - /// - /// - /// This function sets hints for the next call to @ref glfwCreateWindow. The - /// hints, once set, retain their values until changed by a call to this - /// function or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported by the next - /// call to . - /// - /// - /// Some hints are platform specific. These may be set on any platform but they - /// will only affect their specific platform. Other platforms will ignore them. - /// Setting these hints requires no platform specific headers or functions. - /// - /// - /// The window hint to set. - /// The new value of the set hint. - /// - /// - /// Possible errors include . - /// - /// - /// The string is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static unsafe void WindowHint(WindowHintString hint, string value) - { - var ptr = Marshal.StringToCoTaskMemUTF8(value); - - try - { - glfwWindowHintString((int)hint, (byte*)ptr); - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// - /// Sets the specified window hint to the desired value. - /// - /// - /// This function sets hints for the next call to @ref glfwCreateWindow. The - /// hints, once set, retain their values until changed by a call to this - /// function or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported by the next - /// call to . - /// - /// - /// Some hints are platform specific. These may be set on any platform but they - /// will only affect their specific platform. Other platforms will ignore them. - /// Setting these hints requires no platform specific headers or functions. - /// - /// - /// The window hint to set. - /// The new value of the set hint. - /// - /// - /// Possible errors include . - /// - /// - /// The string is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static unsafe void WindowHintRaw(WindowHintString hint, byte* value) - { - glfwWindowHintString((int)hint, value); - } - - /// - /// - /// This function sets the size limits of the client area of the specified window. - /// - /// - /// If the window is full screen, the size limits only take effect once it is made windowed. - /// - /// - /// If the window is not resizable, this function does nothing. - /// - /// - /// The size limits are applied immediately to a windowed mode window and may cause it to be resized. - /// - /// - /// The maximum dimensions must be greater than or equal to the minimum dimensions - /// and all must be greater than or equal to zero. - /// - /// - /// The window to set limits for. - /// - /// The minimum width, in screen coordinates, of the client area, or . - /// - /// - /// The minimum height, in screen coordinates, of the client area, or . - /// - /// - /// The maximum width, in screen coordinates, of the client area, or . - /// - /// - /// The maximum height, in screen coordinates, of the client area, or . - /// - /// - /// - /// If you set size limits and an aspect ratio that conflict, the results are undefined. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SetWindowSizeLimits( - Window* window, - int minWidth, - int minHeight, - int maxWidth, - int maxHeight) - { - glfwSetWindowSizeLimits(window, minWidth, minHeight, maxWidth, maxHeight); - } - - /// - /// - /// This function sets the required aspect ratio of the client area of the specified window. - /// - /// - /// If the window is full screen, the aspect ratio only takes effect once it is made windowed. - /// - /// - /// If the window is not resizable, this function does nothing. - /// - /// - /// The aspect ratio is specified as a numerator and a denominator and both values must be greater than zero. - /// For example, the common 16:9 aspect ratio is specified as 16 and 9, respectively. - /// - /// - /// If the numerator and denominator is set to then the aspect ratio limit is disabled. - /// - /// - /// The aspect ratio is applied immediately to a windowed mode window and may cause it to be resized. - /// - /// - /// The window to set limits for. - /// The numerator of the desired aspect ratio, or . - /// The denominator of the desired aspect ratio, or . - /// - /// - /// If you set size limits and an aspect ratio that conflict, the results are undefined. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SetWindowAspectRatio(Window* window, int numer, int denom) - { - glfwSetWindowAspectRatio(window, numer, denom); - } - - /// - /// - /// This function retrieves the size, in screen coordinates, of each edge of the frame of the specified window. - /// - /// - /// This size includes the title bar, if the window has one. - /// The size of the frame may vary depending on the window-related hints used to create it. - /// - /// - /// - /// Because this function retrieves the size of each window frame edge - /// and not the offset along a particular coordinate axis, the retrieved values will always be zero or positive. - /// - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The window whose frame size to query. - /// - /// Where to store the size, in screen coordinates, of the left edge of the window frame, or out _. - /// - /// - /// Where to store the size, in screen coordinates, of the top edge of the window frame, or out _. - /// - /// - /// Where to store the size, in screen coordinates, of the right edge of the window frame, or out _. - /// - /// - /// Where to store the size, in screen coordinates, of the bottom edge of the window frame, or out _. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetWindowFrameSize( - Window* window, - out int left, - out int top, - out int right, - out int bottom) - { - int l, t, r, b; - glfwGetWindowFrameSize(window, &l, &t, &r, &b); - left = l; - top = t; - right = r; - bottom = b; - } - - /// - /// - /// This function retrieves the content scale for the specified window. - /// - /// - /// The content scale is the ratio between the current DPI and the platform's default DPI. - /// This is especially important for text and any UI elements. - /// If the pixel dimensions of your UI scaled by this look appropriate on your machine then it should - /// appear at a reasonable size on other machines regardless of their DPI and scaling settings. - /// This relies on the system DPI and scaling settings being somewhat correct. - /// - /// - /// - /// On systems where each monitors can have its own content scale, - /// the window content scale will depend on which monitor the system considers the window to be on. - /// - /// - /// The window to query. - /// - /// Where to store the x-axis content scale, or out _. - /// - /// - /// Where to store the y-axis content scale, or out _. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetWindowContentScale( - Window* window, - out float xScale, - out float yScale) - { - float x, y; - glfwGetWindowContentScale(window, &x, &y); - xScale = x; - yScale = y; - } - - /// - /// - /// This function returns the opacity of the window, including any decorations. - /// - /// - /// The opacity (or alpha) value is a positive finite number between zero and one, - /// where zero is fully transparent and one is fully opaque. - /// - /// - /// If the system does not support whole window transparency, this function always returns one. - /// - /// - /// The initial opacity value for newly created windows is one. - /// - /// - /// The window to query. - /// The opacity value of the specified window. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe float GetWindowOpacity(Window* window) => glfwGetWindowOpacity(window); - - /// - /// - /// This function sets the opacity of the window, including any decorations. - /// - /// - /// The opacity (or alpha) value is a positive finite number between zero and one, - /// where zero is fully transparent and one is fully opaque. - /// - /// - /// The initial opacity value for newly created windows is one. - /// - /// - /// A window created with framebuffer transparency may not use whole window transparency. - /// The results of doing this are undefined. - /// - /// - /// The window to set the opacity for. - /// The desired opacity of the specified window. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void SetWindowOpacity(Window* window, float opacity) - { - glfwSetWindowOpacity(window, opacity); - } - - /// - /// - /// This function requests user attention to the specified window. - /// On platforms where this is not supported, attention is requested to the application as a whole. - /// - /// - /// Once the user has given attention, usually by focusing the window or application, - /// the system will end the request automatically. - /// - /// - /// The window to request attention to. - /// - /// - /// macOS: Attention is requested to the application as a whole, not the specific window. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void RequestWindowAttention(Window* window) => glfwRequestWindowAttention(window); - - /// - /// - /// This function sets the value of an attribute of the specified window. - /// - /// - /// The supported attributes are , - /// , , - /// and . - /// - /// - /// Some of these attributes are ignored for full screen windows. - /// The new value will take effect if the window is later made windowed. - /// - /// - /// Some of these attributes are ignored for windowed mode windows. - /// The new value will take effect if the window is later made full screen. - /// - /// - /// The window to set the attribute for. - /// A supported window attribute. - /// true or false. - /// - /// - /// Calling will always return the latest value, - /// even if that value is ignored by the current mode of the window. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , , and . - /// - /// - public static unsafe void SetWindowAttrib(Window* window, WindowAttributeSetter attribute, bool value) - { - glfwSetWindowAttrib(window, attribute, value ? GLFW_TRUE : GLFW_FALSE); - } - - /// - /// - /// This function returns whether raw mouse motion is supported on the current system. - /// This status does not change after GLFW has been initialized so you only need to check this once. - /// If you attempt to enable raw motion on a system that does not support it, - /// will be emitted. - /// - /// - /// Raw mouse motion is closer to the actual motion of the mouse across a surface. - /// It is not affected by the scaling and acceleration applied to the motion of the desktop cursor. - /// That processing is suitable for a cursor while raw motion is better for controlling for example a 3D camera. - /// Because of this, raw mouse motion is only provided when the cursor is disabled. - /// - /// - /// - /// true if raw mouse motion is supported on the current machine, or false otherwise. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static bool RawMouseMotionSupported() - { - return glfwRawMouseMotionSupported() == GLFW_TRUE; - } - - /// - /// - /// This function returns the name of the specified printable key, encoded as UTF-8. - /// This is typically the character that key would produce without any modifier keys, - /// intended for displaying key bindings to the user. - /// - /// - /// For dead keys, it is typically the diacritic it would add to a character. - /// - /// - /// Do not use this function for text input. - /// You will break text input for many languages even if it happens to work for yours. - /// - /// - /// If the key is , the scancode is used to identify the key, otherwise the scancode is ignored. - /// If you specify a non-printable key, or and a scancode that maps to a non-printable key, - /// this function returns null but does not emit an error. - /// - /// - /// This behavior allows you to always pass in the arguments in the key callback without modification. - /// - /// - /// The printable keys are: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// to - /// to - /// to - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Names for printable keys depend on keyboard layout, - /// while names for non-printable keys are the same across layouts but depend on the application language - /// and should be localized along with other user interface text. - /// - /// - /// The key to query, or . - /// The scancode of the key to query. - /// The UTF-8 encoded, layout-specific name of the key, or null. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the next call to , or until the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe string GetKeyName(Keys key, int scanCode) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetKeyName(key, scanCode)); - } - - /// - /// - /// This function returns the name of the specified printable key, encoded as UTF-8. - /// This is typically the character that key would produce without any modifier keys, - /// intended for displaying key bindings to the user. - /// - /// - /// For dead keys, it is typically the diacritic it would add to a character. - /// - /// - /// Do not use this function for text input. - /// You will break text input for many languages even if it happens to work for yours. - /// - /// - /// If the key is , the scancode is used to identify the key, otherwise the scancode is ignored. - /// If you specify a non-printable key, or and a scancode that maps to a non-printable key, - /// this function returns null but does not emit an error. - /// - /// - /// This behavior allows you to always pass in the arguments in the key callback without modification. - /// - /// - /// The printable keys are: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// to - /// to - /// to - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Names for printable keys depend on keyboard layout, - /// while names for non-printable keys are the same across layouts but depend on the application language - /// and should be localized along with other user interface text. - /// - /// - /// The key to query, or . - /// The scancode of the key to query. - /// The UTF-8 encoded, layout-specific name of the key, or null. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the next call to , or until the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe byte* GetKeyNameRaw(Keys key, int scancode) - { - return glfwGetKeyName(key, scancode); - } - - /// - /// - /// This function returns the platform-specific scancode of the specified key. - /// - /// - /// If the key is or does not exist on the keyboard this method will return -1. - /// - /// - /// Any named key. - /// The platform-specific scancode for the key, or -1 if an error occurred. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static int GetKeyScancode(Keys key) - { - return glfwGetKeyScancode(key); - } - - /// - /// - /// This function returns the last state reported for the specified key to the specified window. - /// The returned state is one of or . - /// The higher-level action is only reported to the key callback. - /// - /// - /// If the input mode is enabled, this function returns - /// the first time you call it for a key that was pressed, - /// even if that key has already been released. - /// - /// - /// The key functions deal with physical keys, - /// with key tokens named after their use on the standard US keyboard layout. - /// If you want to input text, use the Unicode character callback instead. - /// - /// - /// The modifier key bit masks are not key tokens and cannot be used with this function. - /// - /// - /// Do not use this function to implement text input. - /// - /// - /// The desired window. - /// - /// The desired keyboard key. is not a valid key for this function. - /// - /// One of or . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe InputAction GetKey(Window* window, Keys key) - { - return glfwGetKey(window, key); - } - - /// - /// - /// This function returns the last state reported for the specified mouse button to the specified window. - /// The returned state is one of or . - /// - /// - /// If the input mode is enabled, this function returns - /// the first time you call it for a mouse button that was pressed, - /// even if that mouse button has already been released. - /// - /// - /// The desired window. - /// The desired mouse button. - /// One of or . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe InputAction GetMouseButton(Window* window, MouseButton button) - { - return glfwGetMouseButton(window, button); - } - - /// - /// - /// This function returns the position of the cursor, - /// in screen coordinates, relative to the upper-left corner of the client area of the specified window. - /// - /// - /// If the cursor is disabled (with ) then the cursor position - /// is unbounded and limited only by the minimum and maximum values of a double. - /// - /// - /// The coordinate can be converted to their integer equivalents with the floor function. - /// Casting directly to an integer type works for positive coordinates, but fails for negative ones. - /// - /// - /// Any or all of the position arguments may be out _. - /// If an error occurs, all non-out _ position arguments will be set to zero. - /// - /// - /// The desired window. - /// - /// Where to store the cursor x-coordinate, relative to the left edge of the client area, or out _. - /// - /// - /// Where to store the cursor y-coordinate, relative to the to top edge of the client area, or out _. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetCursorPos(Window* window, out double xPos, out double yPos) - { - double x, y; - glfwGetCursorPos(window, &x, &y); - xPos = x; - yPos = y; - } - - /// - /// - /// This function returns the position of the cursor, - /// in screen coordinates, relative to the upper-left corner of the client area of the specified window. - /// - /// - /// If the cursor is disabled (with ) then the cursor position - /// is unbounded and limited only by the minimum and maximum values of a double. - /// - /// - /// The coordinate can be converted to their integer equivalents with the floor function. - /// Casting directly to an integer type works for positive coordinates, but fails for negative ones. - /// - /// - /// Any or all of the position arguments may be out _. - /// If an error occurs, all non-out _ position arguments will be set to zero. - /// - /// - /// The desired window. - /// - /// Where to store the cursor x-coordinate, relative to the left edge of the client area, or out _. - /// - /// - /// Where to store the cursor y-coordinate, relative to the to top edge of the client area, or out _. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetCursorPosRaw(Window* window, double* xPos, double* yPos) - { - glfwGetCursorPos(window, xPos, yPos); - } - - /// - /// - /// This function sets the position, in screen coordinates, - /// of the cursor relative to the upper-left corner of the client area of the specified window. - /// - /// - /// The window must have input focus. - /// If the window does not have input focus when this function is called, it fails silently. - /// - /// - /// Do not use this function to implement things like camera controls. - /// GLFW already provides the cursor mode that hides the cursor, - /// transparently re-centers it and provides unconstrained cursor motion. - /// See for more information. - /// - /// - /// If the cursor mode is then the cursor position is unconstrained - /// and limited only by the minimum and maximum values of a double. - /// - /// - /// The desired window. - /// The desired x-coordinate, relative to the left edge of the client area. - /// The desired y-coordinate, relative to the top edge of the client area. - /// - /// - /// Wayland: This function will only work when the cursor mode is , - /// otherwise it will do nothing. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void SetCursorPos(Window* window, double xPos, double yPos) - { - glfwSetCursorPos(window, xPos, yPos); - } - - /// - /// - /// Creates a new custom cursor image that can be set for a window with . - /// - /// - /// The cursor can be destroyed with . - /// Any remaining cursors are destroyed by . - /// - /// - /// The pixels are 32-bit, little-endian, non-premultiplied RGBA, - /// i.e. eight bits per channel with the red channel first. - /// They are arranged canonically as packed sequential rows, starting from the top-left corner. - /// - /// - /// The cursor hotspot is specified in pixels, relative to the upper-left corner of the cursor image. - /// Like all other coordinate systems in GLFW, the X-axis points to the right and the Y-axis points down. - /// - /// - /// The desired cursor image. - /// The desired x-coordinate, in pixels, of the cursor hotspot. - /// The desired y-coordinate, in pixels, of the cursor hotspot. - /// The handle of the created cursor, or null if an error occurred. - /// - /// - /// The specified image data is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe Cursor* CreateCursor(in Image image, int xHot, int yHot) - { - fixed (Image* ptr = &image) - { - return CreateCursorRaw(ptr, xHot, yHot); - } - } - - /// - /// - /// Creates a new custom cursor image that can be set for a window with . - /// - /// - /// The cursor can be destroyed with . - /// Any remaining cursors are destroyed by . - /// - /// - /// The pixels are 32-bit, little-endian, non-premultiplied RGBA, - /// i.e. eight bits per channel with the red channel first. - /// They are arranged canonically as packed sequential rows, starting from the top-left corner. - /// - /// - /// The cursor hotspot is specified in pixels, relative to the upper-left corner of the cursor image. - /// Like all other coordinate systems in GLFW, the X-axis points to the right and the Y-axis points down. - /// - /// - /// The desired cursor image. - /// The desired x-coordinate, in pixels, of the cursor hotspot. - /// The desired y-coordinate, in pixels, of the cursor hotspot. - /// The handle of the created cursor, or null if an error occurred. - /// - /// - /// The specified image data is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe Cursor* CreateCursorRaw(Image* image, int xHot, int yHot) - { - return glfwCreateCursor(image, xHot, yHot); - } - - /// - /// - /// Returns a cursor with a standard shape, that can be set for a window with . - /// - /// - /// One of the standard shapes. - /// A new cursor ready to use or null if an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe Cursor* CreateStandardCursor(CursorShape shape) - { - return glfwCreateStandardCursor(shape); - } - - /// - /// - /// This function destroys a cursor previously created with . - /// Any remaining cursors will be destroyed by . - /// - /// - /// If the specified cursor is current for any window, that window will be reverted to the default cursor. - /// This does not affect the cursor mode. - /// - /// - /// The cursor object to destroy. - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void DestroyCursor(Cursor* cursor) => glfwDestroyCursor(cursor); - - /// - /// - /// This function sets the cursor image to be used when the cursor is over the client area - /// of the specified window. - /// - /// - /// The set cursor will only be visible - /// when the cursor mode of the window is . - /// - /// - /// On some platforms, the set cursor may not be visible unless the window also has input focus. - /// - /// - /// The window to set the cursor for. - /// The cursor to set, or null to switch back to the default arrow cursor. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void SetCursor(Window* window, Cursor* cursor) - { - glfwSetCursor(window, cursor); - } - - /// - /// - /// This function returns whether the specified joystick is present. - /// - /// - /// There is no need to call this function before other functions that accept a joystick ID, - /// as they all check for presence before performing any other work. - /// - /// - /// The joystick to query. - /// true if the joystick is present, or false otherwise. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static bool JoystickPresent(int jid) - { - return glfwJoystickPresent(jid) == GLFW_TRUE; - } - - /// - /// - /// This function returns the values of all axes of the specified joystick. - /// Each element in the array is a value between -1.0 and 1.0. - /// - /// - /// If the specified joystick is not present - /// this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// An array of axis values, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. - /// You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe float[] GetJoystickAxes(int jid) - { - var ptr = GetJoystickAxesRaw(jid, out var count); - - if (ptr == null) - { - return null; - } - - var array = new float[count]; - for (var i = 0; i < count; i++) - { - array[i] = ptr[i]; - } - - return array; - } - - /// - /// - /// This function returns the values of all axes of the specified joystick. - /// Each element in the array is a value between -1.0 and 1.0. - /// - /// - /// If the specified joystick is not present - /// this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of axis values in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of axis values, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. - /// You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe float* GetJoystickAxesRaw(int jid, out int count) - { - fixed (int* ptr = &count) - { - return GetJoystickAxesRaw(jid, ptr); - } - } - - /// - /// - /// This function returns the values of all axes of the specified joystick. - /// Each element in the array is a value between -1.0 and 1.0. - /// - /// - /// If the specified joystick is not present - /// this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of axis values in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of axis values, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. - /// You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe float* GetJoystickAxesRaw(int jid, int* count) - { - return glfwGetJoystickAxes(jid, count); - } - - /// - /// - /// This function returns the state of all buttons of the specified joystick. - /// Each element in the array is either or . - /// - /// - /// For backward compatibility with earlier versions that did not have , - /// the button array also includes all hats, each represented as four buttons. - /// - /// - /// The hats are in the same order as returned by and are in the order - /// up, right, down and left. - /// - /// - /// To disable these extra buttons, set the - /// init hint before initialization. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// An array of button states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe InputAction[] GetJoystickButtons(int jid) - { - var ptr = GetJoystickButtonsRaw(jid, out var count); - - if (ptr == null) - { - return null; - } - - var array = new InputAction[count]; - for (var i = 0; i < count; i++) - { - array[i] = ptr[i]; - } - - return array; - } - - /// - /// - /// This function returns the state of all buttons of the specified joystick. - /// Each element in the array is either or . - /// - /// - /// For backward compatibility with earlier versions that did not have , - /// the button array also includes all hats, each represented as four buttons. - /// - /// - /// The hats are in the same order as returned by and are in the order - /// up, right, down and left. - /// - /// - /// To disable these extra buttons, set the - /// init hint before initialization. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of button states in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of button states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe InputAction* GetJoystickButtonsRaw(int jid, out int count) - { - fixed (int* ptr = &count) - { - return GetJoystickButtonsRaw(jid, ptr); - } - } - - /// - /// - /// This function returns the state of all buttons of the specified joystick. - /// Each element in the array is either or . - /// - /// - /// For backward compatibility with earlier versions that did not have , - /// the button array also includes all hats, each represented as four buttons. - /// - /// - /// The hats are in the same order as returned by and are in the order - /// up, right, down and left. - /// - /// - /// To disable these extra buttons, set the - /// init hint before initialization. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of button states in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of button states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe InputAction* GetJoystickButtonsRaw(int jid, int* count) - { - return glfwGetJoystickButtons(jid, count); - } - - /// - /// - /// This function returns the state of all hats of the specified joystick. - /// Each element in the array is one of the . - /// - /// - /// The diagonal directions are bitwise combinations of the primary (up, right, down and left) directions - /// and you can test for these individually by ANDing it with the corresponding direction. - /// - /// if (hats[2].HasFlag(JoystickHats.Right)) - /// { - /// // State of hat 2 could be right-up, right or right-down - /// } - /// - /// - /// - /// If the specified joystick is not present, this function will return NULL but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// An array of hat states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself - /// It is valid until the specified joystick is disconnected, - /// this function is called again for that joystick or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe JoystickHats[] GetJoystickHats(int jid) - { - var ptr = GetJoystickHatsRaw(jid, out var count); - - if (ptr == null) - { - return null; - } - - var array = new JoystickHats[count]; - for (var i = 0; i < count; i++) - { - array[i] = ptr[i]; - } - - return array; - } - - /// - /// - /// This function returns the state of all hats of the specified joystick. - /// Each element in the array is one of the . - /// - /// - /// The diagonal directions are bitwise combinations of the primary (up, right, down and left) directions - /// and you can test for these individually by ANDing it with the corresponding direction. - /// - /// if (hats[2].HasFlag(JoystickHats.Right)) - /// { - /// // State of hat 2 could be right-up, right or right-down - /// } - /// - /// - /// - /// If the specified joystick is not present, this function will return NULL but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of hat states in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of hat states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself - /// It is valid until the specified joystick is disconnected, - /// this function is called again for that joystick or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe JoystickHats* GetJoystickHatsRaw(int jid, out int count) - { - fixed (int* ptr = &count) - { - return glfwGetJoystickHats(jid, ptr); - } - } - - /// - /// - /// This function returns the state of all hats of the specified joystick. - /// Each element in the array is one of the . - /// - /// - /// The diagonal directions are bitwise combinations of the primary (up, right, down and left) directions - /// and you can test for these individually by ANDing it with the corresponding direction. - /// - /// if (hats[2].HasFlag(JoystickHats.Right)) - /// { - /// // State of hat 2 could be right-up, right or right-down - /// } - /// - /// - /// - /// If the specified joystick is not present, this function will return NULL but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// Where to store the number of hat states in the returned array. - /// This is set to zero if the joystick is not present or an error occurred. - /// - /// - /// An array of hat states, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself - /// It is valid until the specified joystick is disconnected, - /// this function is called again for that joystick or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe JoystickHats* GetJoystickHatsRaw(int jid, int* count) - { - return glfwGetJoystickHats(jid, count); - } - - /// - /// - /// This function returns the name, encoded as UTF-8, of the specified joystick. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded name of the joystick, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe string GetJoystickName(int jid) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetJoystickName(jid)); - } - - /// - /// - /// This function returns the name, encoded as UTF-8, of the specified joystick. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded name of the joystick, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe byte* GetJoystickNameRaw(int jid) - { - return glfwGetJoystickName(jid); - } - - /// - /// - /// This function returns the SDL compatible GUID, as a UTF-8 encoded hexadecimal string, - /// of the specified joystick. - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// - /// - /// The GUID is what connects a joystick to a gamepad mapping. - /// A connected joystick will always have a GUID even if there is no gamepad mapping assigned to it. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The GUID uses the format introduced in SDL 2.0.5. - /// This GUID tries to uniquely identify the make and model of a joystick but does not identify a specific unit, - /// e.g. all wired Xbox 360 controllers will have the same GUID on that platform. - /// The GUID for a unit may vary between platforms - /// depending on what hardware information the platform specific APIs provide. - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded GUID of the joystick, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe string GetJoystickGUID(int jid) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetJoystickGUID(jid)); - } - - /// - /// - /// This function returns the SDL compatible GUID, as a UTF-8 encoded hexadecimal string, - /// of the specified joystick. - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// - /// - /// The GUID is what connects a joystick to a gamepad mapping. - /// A connected joystick will always have a GUID even if there is no gamepad mapping assigned to it. - /// - /// - /// If the specified joystick is not present this function will return null but will not generate an error. - /// This can be used instead of first calling . - /// - /// - /// The GUID uses the format introduced in SDL 2.0.5. - /// This GUID tries to uniquely identify the make and model of a joystick but does not identify a specific unit, - /// e.g. all wired Xbox 360 controllers will have the same GUID on that platform. - /// The GUID for a unit may vary between platforms - /// depending on what hardware information the platform specific APIs provide. - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded GUID of the joystick, or null if the joystick is not present or an error occurred. - /// - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe byte* GetJoystickGUIDRaw(int jid) - { - return glfwGetJoystickGUID(jid); - } - - /// - /// - /// This function sets the user-defined pointer of the specified joystick. - /// The current value is retained until the joystick is disconnected. - /// The initial value is . - /// - /// - /// This function may be called from the joystick callback, even for a joystick that is being disconnected. - /// - /// - /// The joystick whose pointer to set. - /// The new value. - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void SetJoystickUserPointer(int jid, void* ptr) - { - glfwSetJoystickUserPointer(jid, ptr); - } - - /// - /// - /// This function returns the current value of the user-defined pointer of the specified joystick. - /// The initial value is . - /// - /// - /// This function may be called from the joystick callback, even for a joystick that is being disconnected. - /// - /// - /// The joystick whose pointer to return. - /// The user-defined pointer of the given . - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void* GetJoystickUserPointer(int jid) - { - return glfwGetJoystickUserPointer(jid); - } - - /// - /// - /// This function returns whether the specified joystick is both present and has a gamepad mapping. - /// - /// - /// If the specified joystick is present but does not have a gamepad mapping - /// this function will return false but will not generate an error. - /// - /// - /// The joystick to query. - /// - /// true if a joystick is both present and has a gamepad mapping, or false otherwise. - /// - /// - /// - /// Call to check if a joystick is present regardless of whether it has a mapping. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// - /// Possible errors include and . - /// - /// - /// - public static bool JoystickIsGamepad(int jid) - { - return glfwJoystickIsGamepad(jid) == GLFW_TRUE; - } - - /// - /// - /// This function parses the specified ASCII encoded string - /// and updates the internal list with any gamepad mappings it finds. - /// - /// - /// This string may contain either a single gamepad mapping or many mappings separated by newlines. - /// - /// - /// The parser supports the full format of the gamecontrollerdb.txt source file - /// including empty lines and comments. - /// - /// - /// See Gamepad mappings - /// for a description of the format. - /// - /// - /// If there is already a gamepad mapping for a given GUID in the internal list, it will be replaced by the one passed to this function. If the library is terminated and re-initialized the internal list will revert to the built-in default. - /// - /// - /// The string containing the gamepad mappings. - /// true if successful, or false if an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe bool UpdateGamepadMappings(string newMapping) - { - var ptr = Marshal.StringToCoTaskMemUTF8(newMapping); - try - { - return glfwUpdateGamepadMappings((byte*)ptr) == GLFW_TRUE; - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// - /// This function parses the specified ASCII encoded string - /// and updates the internal list with any gamepad mappings it finds. - /// - /// - /// This string may contain either a single gamepad mapping or many mappings separated by newlines. - /// - /// - /// The parser supports the full format of the gamecontrollerdb.txt source file - /// including empty lines and comments. - /// - /// - /// See Gamepad mappings - /// for a description of the format. - /// - /// - /// If there is already a gamepad mapping for a given GUID in the internal list, it will be replaced by the one passed to this function. If the library is terminated and re-initialized the internal list will revert to the built-in default. - /// - /// - /// The string containing the gamepad mappings. - /// true if successful, or false if an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe bool UpdateGamepadMappingsRaw(byte* newMapping) - { - return glfwUpdateGamepadMappings(newMapping) == GLFW_TRUE; - } - - /// - /// - /// This function returns the human-readable name of the gamepad - /// from the gamepad mapping assigned to the specified joystick. - /// - /// - /// If the specified joystick is not present or does not have a gamepad mapping - /// this function will return null but will not generate an error. - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded name of the gamepad, or null if the joystick is not present, - /// does not have a mapping or an error occurred. - /// - /// - /// - /// Call to check whether it is present regardless of whether it has a mapping. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected, - /// the gamepad mappings are updated or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static unsafe string GetGamepadName(int jid) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetGamepadName(jid)); - } - - /// - /// - /// This function returns the human-readable name of the gamepad - /// from the gamepad mapping assigned to the specified joystick. - /// - /// - /// If the specified joystick is not present or does not have a gamepad mapping - /// this function will return null but will not generate an error. - /// - /// - /// The joystick to query. - /// - /// The UTF-8 encoded name of the gamepad, or null if the joystick is not present, - /// does not have a mapping or an error occurred. - /// - /// - /// - /// Call to check whether it is present regardless of whether it has a mapping. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// It is valid until the specified joystick is disconnected, - /// the gamepad mappings are updated or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static unsafe byte* GetGamepadNameRaw(int jid) - { - return glfwGetGamepadName(jid); - } - - /// - /// - /// This function retrieves the state of the specified joystick remapped to an Xbox-like gamepad. - /// - /// - /// If the specified joystick is not present or does not have a gamepad mapping - /// this function will return false but will not generate an error. - /// Call to check whether it is present regardless of whether it has a mapping. - /// - /// - /// The Guide button may not be available for input as it is often hooked by the system or the Steam client. - /// - /// - /// Not all devices have all the buttons or axes provided by . - /// Unavailable buttons and axes will always report and 0.0 respectively. - /// - /// - /// The joystick to query. - /// The gamepad input state of the joystick. - /// - /// true if successful, or false if no joystick is connected, - /// it has no gamepad mapping or an error occurred. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe bool GetGamepadState(int jid, out GamepadState state) - { - fixed (GamepadState* ptr = &state) - { - return glfwGetGamepadState(jid, ptr) == GLFW_TRUE; - } - } - - /// - /// - /// This function retrieves the state of the specified joystick remapped to an Xbox-like gamepad. - /// - /// - /// If the specified joystick is not present or does not have a gamepad mapping - /// this function will return false but will not generate an error. - /// Call to check whether it is present regardless of whether it has a mapping. - /// - /// - /// The Guide button may not be available for input as it is often hooked by the system or the Steam client. - /// - /// - /// Not all devices have all the buttons or axes provided by . - /// Unavailable buttons and axes will always report and 0.0 respectively. - /// - /// - /// The joystick to query. - /// The gamepad input state of the joystick. - /// - /// true if successful, or false if no joystick is connected, - /// it has no gamepad mapping or an error occurred. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe bool GetGamepadStateRaw(int jid, GamepadState* state) - { - return glfwGetGamepadState(jid, state) == GLFW_TRUE; - } - - /// - /// - /// This function returns the value of the GLFW timer. - /// - /// - /// Unless the timer has been set using , - /// the timer measures time elapsed since GLFW was initialized. - /// - /// - /// The resolution of the timer is system dependent, but is usually on the order of a few micro- or nanoseconds. - /// It uses the highest-resolution monotonic time source on each supported platform. - /// - /// - /// The current value, in seconds, or zero if an error occurred. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Reading and writing of the internal timer offset is not atomic, - /// so it needs to be externally synchronized with calls to . - /// - /// - /// - /// Possible errors include . - /// - /// - public static double GetTime() => glfwGetTime(); - - /// - /// - /// This function sets the value of the GLFW timer. It then continues to count up from that value. - /// The value must be a positive finite number less than or equal to 18446744073.0, - /// which is approximately 584.5 years. - /// - /// - /// The new value, in seconds. - /// - /// - /// The upper limit of the timer is calculated as floor((2^64 - 1) / 109) and is due to implementations - /// storing nanoseconds in 64 bits. The limit may be increased in the future. - /// - /// - /// This function may be called from any thread. - /// Reading and writing of the internal timer offset is not atomic, - /// so it needs to be externally synchronized with calls to . - /// - /// - /// Possible errors include and . - /// - /// - public static void SetTime(double time) => glfwSetTime(time); - - /// - /// - /// This function returns the current value of the raw timer, measured in 1 / frequency seconds. - /// To get the frequency, call . - /// - /// - /// The value of the timer, or zero if an error occurred. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include . - /// - /// - public static long GetTimerValue() => glfwGetTimerValue(); - - /// - /// - /// This function returns the frequency, in Hz, of the raw timer. - /// - /// - /// he frequency of the timer, in Hz, or zero if an error occurred. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include . - /// - /// - public static long GetTimerFrequency() => glfwGetTimerFrequency(); - - /// - /// - /// This function returns the window whose OpenGL or OpenGL ES context is current on the calling thread. - /// - /// - /// The window whose context is current, or null if no window's context is current. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe Window* GetCurrentContext() => glfwGetCurrentContext(); - - /// - /// - /// This function swaps the front and back buffers of the specified window - /// when rendering with OpenGL or OpenGL ES. - /// - /// - /// If the swap interval is greater than zero, - /// the GPU driver waits the specified number of screen updates before swapping the buffers. - /// - /// - /// The specified window must have an OpenGL or OpenGL ES context. - /// Specifying a window without a context will generate a error. - /// - /// - /// The window whose buffers to swap. - /// - /// - /// EGL: The context of the specified window must be current on the calling thread. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SwapBuffers(Window* window) => glfwSwapBuffers(window); - - /// - /// - /// This function returns whether the specified API extension is supported - /// by the current OpenGL or OpenGL ES context. - /// It searches both for client API extension and context creation API extensions. - /// - /// - /// A context must be current on the calling thread. - /// Calling this function without a current context will cause a error. - /// - /// - /// As this functions retrieves and searches one or more extension strings each call, - /// it is recommended that you cache its results if it is going to be used frequently. - /// The extension strings will not change during the lifetime of a context, so there is no danger in doing this. - /// - /// - /// The ASCII encoded name of the extension. - /// true if the extension is available, or false otherwise. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , , and . - /// - /// - public static unsafe bool ExtensionSupported(string extensionName) - { - var ptr = Marshal.StringToCoTaskMemUTF8(extensionName); - - try - { - return glfwExtensionSupported((byte*)ptr) == GLFW_TRUE; - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// This function returns the address of the specified OpenGL or OpenGL ES core or extension function, if it is supported by the current context. - /// A context must be current on the calling thread. Calling this function without a current context will cause a error. - /// - /// - /// - /// This function does not apply to Vulkan. If you are rendering with Vulkan, see , vkGetInstanceProcAddr and vkGetDeviceProcAddr instead. - /// - /// - /// Possible errors include , and . - /// - /// - /// The address of a given function is not guaranteed to be the same between contexts. - /// This function may return a non-null address despite the associated version or extension not being available. Always check the context version or extension string first. - /// - /// - /// The returned function pointer is valid until the context is destroyed or the library is terminated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// The name of the function. - /// The address of the function, or null if an error occurred. - /// - public static unsafe IntPtr GetProcAddress(string procName) - { - var ptr = Marshal.StringToCoTaskMemUTF8(procName); - - try - { - return glfwGetProcAddress((byte*)ptr); - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// This function returns the address of the specified OpenGL or OpenGL ES core or extension function, if it is supported by the current context. - /// A context must be current on the calling thread. Calling this function without a current context will cause a error. - /// - /// - /// - /// This function does not apply to Vulkan. If you are rendering with Vulkan, see , vkGetInstanceProcAddr and vkGetDeviceProcAddr instead. - /// - /// - /// Possible errors include , and . - /// - /// - /// The address of a given function is not guaranteed to be the same between contexts. - /// This function may return a non-null address despite the associated version or extension not being available. Always check the context version or extension string first. - /// - /// - /// The returned function pointer is valid until the context is destroyed or the library is terminated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// The ASCII-encoded name of the function. - /// The address of the function, or null if an error occurred. - /// - public static unsafe IntPtr GetProcAddressRaw(byte* procName) - { - return glfwGetProcAddress(procName); - } - - /// - /// - /// This function returns whether the specified API extension is supported - /// by the current OpenGL or OpenGL ES context. - /// It searches both for client API extension and context creation API extensions. - /// - /// - /// A context must be current on the calling thread. - /// Calling this function without a current context will cause a error. - /// - /// - /// As this functions retrieves and searches one or more extension strings each call, - /// it is recommended that you cache its results if it is going to be used frequently. - /// The extension strings will not change during the lifetime of a context, so there is no danger in doing this. - /// - /// - /// The ASCII encoded name of the extension. - /// true if the extension is available, or false otherwise. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , , and . - /// - /// - public static unsafe bool ExtensionSupportedRaw(byte* extensionName) - { - return glfwExtensionSupported(extensionName) == GLFW_TRUE; - } - - /// - /// - /// This function creates a window and its associated OpenGL or OpenGL ES context. - /// Most of the options controlling how the window and its context should be created - /// are specified with window hints. - /// - /// - /// Successful creation does not change which context is current. - /// Before you can use the newly created context, you need to make it current. - /// For information about the share parameter, see - /// Context object sharing. - /// - /// - /// The created window, framebuffer and context may differ from what you requested, - /// as not all parameters and hints are - /// hard constraints. - /// This includes the size of the window, especially for full screen windows. - /// To query the actual attributes of the created window, framebuffer and context, - /// see , and . - /// - /// - /// To create a full screen window, you need to specify the monitor the window will cover. - /// If no monitor is specified, the window will be windowed mode. - /// Unless you have a way for the user to choose a specific monitor, - /// it is recommended that you pick the primary monitor. - /// For more information on how to query connected monitors, see - /// Retrieving monitors. - /// - /// - /// For full screen windows, the specified size becomes the resolution of the window's desired video mode. - /// As long as a full screen window is not iconified, - /// the supported video mode most closely matching the desired video mode is set for the specified monitor. - /// For more information about full screen windows, including the creation of so called windowed full screen - /// or borderless full screen windows, see - /// - /// "Windowed full screen" windows - /// . - /// - /// - /// Once you have created the window, you can switch it between windowed and full screen mode - /// with . If the window has an OpenGL or OpenGL ES context, it will be unaffected. - /// - /// - /// By default, newly created windows use the placement recommended by the window system. - /// To create the window at a specific position, - /// make it initially invisible using the window hint, - /// set its position(see ) and then show it - /// (see ). - /// - /// - /// As long as at least one full screen window is not iconified, the screensaver is prohibited from starting. - /// - /// - /// Window systems put limits on window sizes. - /// Very large or very small window dimensions may be overridden by the window system on creation. - /// Check the actual size after creation(see or . - /// - /// - /// The swap interval - /// is not set during window creation and the initial value may vary - /// depending on driver settings and defaults. - /// - /// - /// - /// The desired width, in screen coordinates, of the window. This must be greater than zero. - /// - /// - /// The desired height, in screen coordinates, of the window. This must be greater than zero. - /// - /// The initial, UTF-8 encoded window title. - /// The monitor to use for full screen mode, or null for windowed mode. - /// - /// The window whose context to share resources with, or null to not share resources. - /// - /// The handle of the created window, or null if an error occurred. - /// - /// - /// Windows: Window creation will fail if the Microsoft GDI software OpenGL implementation is the only one available. - /// - /// - /// Windows: If the executable has an icon resource named GLFW_ICON, it will be set as the initial icon for the window. - /// If no such icon is present, the IDI_WINLOGO icon will be used instead. To set a different icon, see . - /// - /// - /// Windows: The context to share resources with must not be current on any other thread. - /// - /// - /// OS X: The GLFW window has no icon, as it is not a document window, but the dock icon will be the same as the application bundle's icon. - /// For more information on bundles, see the Bundle Programming Guide in the Mac Developer Library. - /// - /// - /// OS X: The first time a window is created the menu bar is populated with common commands like Hide, Quit and About. - /// The About entry opens a minimal about dialog with information from the application's bundle. - /// The menu bar can be disabled with a compile-time option. - /// - /// - /// OS X: On OS X 10.10 and later the window frame will not be rendered at full resolution on Retina displays - /// unless the NSHighResolutionCapable key is enabled in the application bundle's Info.plist. - /// For more information, see High Resolution Guidelines for OS X in the Mac Developer Library. - /// The GLFW test and example programs use a custom Info.plist template for this, which can be found as CMake/MacOSXBundleInfo.plist.in in the source tree. - /// - /// - /// X11: Some window managers will not respect the placement of initially hidden windows. - /// X11: Due to the asynchronous nature of X11, it may take a moment for a window to reach its requested state. - /// This means you may not be able to query the final size, position or other attributes directly after window creation. - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , , , , - /// , and . - /// - /// - public static unsafe Window* CreateWindow(int width, int height, string title, Monitor* monitor, Window* share) - { - var ptr = Marshal.StringToCoTaskMemUTF8(title); - - try - { - return glfwCreateWindow(width, height, (byte*)ptr, monitor, share); - } - finally - { - if (ptr != IntPtr.Zero) - { - Marshal.FreeCoTaskMem(ptr); - } - } - } - - /// - /// - /// This function creates a window and its associated OpenGL or OpenGL ES context. - /// Most of the options controlling how the window and its context should be created - /// are specified with window hints. - /// - /// - /// Successful creation does not change which context is current. - /// Before you can use the newly created context, you need to make it current. - /// For information about the share parameter, see - /// Context object sharing. - /// - /// - /// The created window, framebuffer and context may differ from what you requested, - /// as not all parameters and hints are - /// hard constraints. - /// This includes the size of the window, especially for full screen windows. - /// To query the actual attributes of the created window, framebuffer and context, - /// see , and . - /// - /// - /// To create a full screen window, you need to specify the monitor the window will cover. - /// If no monitor is specified, the window will be windowed mode. - /// Unless you have a way for the user to choose a specific monitor, - /// it is recommended that you pick the primary monitor. - /// For more information on how to query connected monitors, see - /// Retrieving monitors. - /// - /// - /// For full screen windows, the specified size becomes the resolution of the window's desired video mode. - /// As long as a full screen window is not iconified, - /// the supported video mode most closely matching the desired video mode is set for the specified monitor. - /// For more information about full screen windows, including the creation of so called windowed full screen - /// or borderless full screen windows, see - /// - /// "Windowed full screen" windows - /// . - /// - /// - /// Once you have created the window, you can switch it between windowed and full screen mode - /// with . If the window has an OpenGL or OpenGL ES context, it will be unaffected. - /// - /// - /// By default, newly created windows use the placement recommended by the window system. - /// To create the window at a specific position, - /// make it initially invisible using the window hint, - /// set its position(see ) and then show it - /// (see ). - /// - /// - /// As long as at least one full screen window is not iconified, the screensaver is prohibited from starting. - /// - /// - /// Window systems put limits on window sizes. - /// Very large or very small window dimensions may be overridden by the window system on creation. - /// Check the actual size after creation(see or . - /// - /// - /// The swap interval - /// is not set during window creation and the initial value may vary - /// depending on driver settings and defaults. - /// - /// - /// - /// The desired width, in screen coordinates, of the window. This must be greater than zero. - /// - /// - /// The desired height, in screen coordinates, of the window. This must be greater than zero. - /// - /// The initial, UTF-8 encoded window title. - /// The monitor to use for full screen mode, or null for windowed mode. - /// - /// The window whose context to share resources with, or null to not share resources. - /// - /// The handle of the created window, or null if an error occurred. - /// - /// - /// Windows: Window creation will fail if the Microsoft GDI software OpenGL implementation is the only one available. - /// - /// - /// Windows: If the executable has an icon resource named GLFW_ICON, it will be set as the initial icon for the window. - /// If no such icon is present, the IDI_WINLOGO icon will be used instead. To set a different icon, see . - /// - /// - /// Windows: The context to share resources with must not be current on any other thread. - /// - /// - /// OS X: The GLFW window has no icon, as it is not a document window, but the dock icon will be the same as the application bundle's icon. - /// For more information on bundles, see the Bundle Programming Guide in the Mac Developer Library. - /// - /// - /// OS X: The first time a window is created the menu bar is populated with common commands like Hide, Quit and About. - /// The About entry opens a minimal about dialog with information from the application's bundle. - /// The menu bar can be disabled with a compile-time option. - /// - /// - /// OS X: On OS X 10.10 and later the window frame will not be rendered at full resolution on Retina displays - /// unless the NSHighResolutionCapable key is enabled in the application bundle's Info.plist. - /// For more information, see High Resolution Guidelines for OS X in the Mac Developer Library. - /// The GLFW test and example programs use a custom Info.plist template for this, which can be found as CMake/MacOSXBundleInfo.plist.in in the source tree. - /// - /// - /// X11: Some window managers will not respect the placement of initially hidden windows. - /// X11: Due to the asynchronous nature of X11, it may take a moment for a window to reach its requested state. - /// This means you may not be able to query the final size, position or other attributes directly after window creation. - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , , , , - /// , and . - /// - /// - public static unsafe Window* CreateWindowRaw( - int width, - int height, - byte* title, - Monitor* monitor, - Window* share) - { - return glfwCreateWindow(width, height, title, monitor, share); - } - - /// - /// - /// This function destroys the specified window and its context. On calling this function, - /// no further callbacks will be called for that window. - /// - /// - /// If the context of the specified window is current on the main thread, it is detached before being destroyed. - /// - /// - /// The window to destroy. - /// - /// - /// The context of the specified window must not be current on any other thread when this function is called. - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void DestroyWindow(Window* window) => glfwDestroyWindow(window); - - /// - /// - /// This function brings the specified window to front and sets input focus. - /// The window should already be visible and not iconified. - /// - /// - /// By default, both windowed and full screen mode windows are focused when initially created. - /// Set the to disable this behavior. - /// - /// - /// Do not use this function to steal focus from other applications unless you are certain - /// that is what the user wants. - /// Focus stealing can be extremely disruptive. - /// - /// - /// The window to give input focus. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void FocusWindow(Window* window) => glfwFocusWindow(window); - - /// - /// - /// This function returns the contents of the system clipboard, - /// if it contains or is convertible to a UTF-8 encoded string. - /// - /// - /// The window that will request the clipboard contents. - /// - /// The contents of the clipboard as a UTF-8 encoded string, or null if an error occurred. - /// - /// - /// - /// This function may only be called from the main thread. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// The returned string is valid only until the next call to or - /// . - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe string GetClipboardString(Window* window) - { - return Marshal.PtrToStringUTF8((IntPtr) glfwGetClipboardString(window)); - } - - /// - /// - /// This function returns the contents of the system clipboard, - /// if it contains or is convertible to a UTF-8 encoded string. - /// - /// - /// The window that will request the clipboard contents. - /// - /// The contents of the clipboard as a UTF-8 encoded string, or null if an error occurred. - /// - /// - /// - /// This function may only be called from the main thread. - /// - /// - /// The returned string is allocated and freed by GLFW. You should not free it yourself. - /// The returned string is valid only until the next call to or - /// . - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe byte* GetClipboardStringRaw(Window* window) - { - return glfwGetClipboardString(window); - } - - /// - /// - /// This function retrieves the size, in pixels, of the framebuffer of the specified window. - /// If you wish to retrieve the size of the window in screen coordinates, see . - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The window whose framebuffer to query. - /// Where to store the width, in pixels, of the framebuffer. - /// Where to store the height, in pixels, of the framebuffer. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetFramebufferSize(Window* window, out int width, out int height) - { - int w, h; - glfwGetFramebufferSize(window, &w, &h); - width = w; - height = h; - } - - /// - /// - /// This function retrieves the size, in pixels, of the framebuffer of the specified window. - /// If you wish to retrieve the size of the window in screen coordinates, see . - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The window whose framebuffer to query. - /// Where to store the width, in pixels, of the framebuffer. - /// Where to store the height, in pixels, of the framebuffer. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void GetFramebufferSizeRaw(Window* window, int* width, int* height) - { - glfwGetFramebufferSize(window, width, height); - } - - /// - /// - /// This function returns the value of an input option for the specified window. - /// The mode must be or . - /// - /// - /// The window to query. - /// - /// Either or . - /// - /// TODO: return value is either InputModeValue or bool dependant on . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe bool GetInputMode(Window* window, StickyAttributes mode) - { - return glfwGetInputMode(window, mode) == GLFW_TRUE; - } - - /// - /// - /// This function returns the value of an input option for the specified window. - /// The mode must be . - /// - /// - /// The window to query. - /// - /// . - /// - /// TODO: return value is either InputModeValue or bool dependant on . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe CursorModeValue GetInputMode(Window* window, CursorStateAttribute mode) - { - return glfwGetInputMode(window, mode); - } - - /// - /// - /// This function returns the primary monitor. - /// - /// - /// This is usually the monitor where elements like the task bar or global menu bar are located. - /// - /// - /// The primary monitor, or null if no monitors were found or if an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// The primary monitor is always first in the array returned by . - /// - /// - /// Possible errors include . - /// - /// - public static unsafe Monitor* GetPrimaryMonitor() => glfwGetPrimaryMonitor(); - - /// - /// - /// This function returns the current video mode of the specified monitor. - /// - /// - /// If you have created a full screen window for that monitor, - /// the return value will depend on whether that window is iconified. - /// - /// - /// The monitor to query. - /// The current mode of the monitor, or null if an error occurred. - /// - /// - /// The returned array is allocated and freed by GLFW - /// You should not free it yourself. - /// It is valid until the specified monitor is disconnected or the library is terminated. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe VideoMode* GetVideoMode(Monitor* monitor) => glfwGetVideoMode(monitor); - - /// - /// - /// This function returns the value of an attribute of the specified window or its OpenGL or OpenGL ES context. - /// - /// - /// The window to query. - /// The window attribute whose value to return. - /// The value of the attribute, or zero if an error occurred. - /// - /// - /// Framebuffer-related hints are not window attributes. See - /// - /// Framebuffer related attributes - /// - /// for more information. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe bool GetWindowAttrib(Window* window, WindowAttributeGetter attribute) - { - return glfwGetWindowAttrib(window, attribute) == GLFW_TRUE; - } - - /// - /// - /// This function retrieves the size, in screen coordinates, of the client area of the specified window. - /// If you wish to retrieve the size of the framebuffer of the window in pixels, see . - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The window whose size to retrieve. - /// Where to store the width, in screen coordinates, of the client area. - /// Where to store the height, in screen coordinates, of the client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void GetWindowSize(Window* window, out int width, out int height) - { - int w, h; - glfwGetWindowSize(window, &w, &h); - width = w; - height = h; - } - - /// - /// - /// This function retrieves the size, in screen coordinates, of the client area of the specified window. - /// If you wish to retrieve the size of the framebuffer of the window in pixels, see . - /// - /// - /// Any or all of the size arguments may be out _. - /// If an error occurs, all non-out _ size arguments will be set to zero. - /// - /// - /// The window whose size to retrieve. - /// Where to store the width, in screen coordinates, of the client area. - /// Where to store the height, in screen coordinates, of the client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void GetWindowSizeRaw(Window* window, int* width, int* height) - { - glfwGetWindowSize(window, width, height); - } - - /// - /// - /// This function retrieves the position, in screen coordinates, - /// of the upper-left corner of the client area of the specified window. - /// - /// - /// Any or all of the position arguments may be out _. - /// If an error occurs, all non-out _ position arguments will be set to zero. - /// - /// - /// The window to query. - /// Where to store the x-coordinate of the upper-left corner of the client area. - /// Where to store the y-coordinate of the upper-left corner of the client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void GetWindowPos(Window* window, out int x, out int y) - { - int lX, lY; - glfwGetWindowPos(window, &lX, &lY); - x = lX; - y = lY; - } - - /// - /// - /// This function retrieves the position, in screen coordinates, - /// of the upper-left corner of the client area of the specified window. - /// - /// - /// Any or all of the position arguments may be out _. - /// If an error occurs, all non-out _ position arguments will be set to zero. - /// - /// - /// The window to query. - /// Where to store the x-coordinate of the upper-left corner of the client area. - /// Where to store the y-coordinate of the upper-left corner of the client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void GetWindowPosRaw(Window* window, int* x, int* y) - { - glfwGetWindowPos(window, x, y); - } - - /// - /// - /// This function returns the handle of the monitor that the specified window is in full screen on. - /// - /// - /// The window to query. - /// The monitor, or null if the window is in windowed mode or an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - /// - public static unsafe Monitor* GetWindowMonitor(Window* window) => glfwGetWindowMonitor(window); - - /// - /// - /// This function hides the specified window if it was previously visible. - /// If the window is already hidden or is in full screen mode, this function does nothing. - /// - /// - /// The window to hide. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void HideWindow(Window* window) => glfwHideWindow(window); - - /// - /// - /// This function iconifies (minimizes) the specified window if it was previously restored. - /// If the window is already iconified, this function does nothing. - /// - /// - /// If the specified window is a full screen window, - /// the original monitor resolution is restored until the window is restored. - /// - /// - /// The window to iconify. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void IconifyWindow(Window* window) => glfwIconifyWindow(window); - - /// - /// - /// This function makes the OpenGL or OpenGL ES context of the specified window current on the calling thread. - /// - /// - /// A context can only be made current on a single thread at a time - /// and each thread can have only a single current context at a time. - /// - /// - /// By default, making a context non-current implicitly forces a pipeline flush. - /// - /// - /// On machines that support GL_KHR_context_flush_control, - /// you can control whether a context performs this flush - /// by setting the window hint. - /// - /// - /// The specified window must have an OpenGL or OpenGL ES context. - /// Specifying a window without a context will generate a error. - /// - /// - /// - /// The window whose context to make current, or null to detach the current context. - /// - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , and . - /// - /// - /// - public static unsafe void MakeContextCurrent(Window* window) => glfwMakeContextCurrent(window); - - /// - /// - /// This function maximizes the specified window if it was previously not maximized. - /// If the window is already maximized, this function does nothing. - /// - /// - /// If the specified window is a full screen window, this function does nothing. - /// - /// - /// The window to maximize. - /// - /// - /// This function may only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void MaximizeWindow(Window* window) => glfwMaximizeWindow(window); - - /// - /// - /// This function processes only those events that are already in the event queue and then returns immediately. - /// Processing events will cause the window and input callbacks associated with those events to be called. - /// - /// - /// On some platforms, a window move, resize or menu operation will cause event processing to block. - /// This is due to how event processing is designed on those platforms. - /// You can use the - /// window refresh callback - /// to redraw the contents of your window when necessary during such operations. - /// - /// - /// On some platforms, certain events are sent directly to the application without going through the event queue, - /// causing callbacks to be called outside of a call to one of the event processing functions. - /// - /// - /// Event processing is not required for joystick input to work. - /// - /// - /// - /// - /// This function must not be called from a callback. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static void PollEvents() => glfwPollEvents(); - - /// - /// - /// This function posts an empty event from the current thread to the event queue, - /// causing or to return. - /// - /// - /// If no windows exist, this function returns immediately. - /// For synchronization of threads in applications that do not create windows, use your threading library of choice. - /// - /// - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include and . - /// - /// - public static void PostEmptyEvent() => glfwPostEmptyEvent(); - - /// - /// - /// This function restores the specified window if it was previously iconified (minimized) or maximized. - /// If the window is already restored, this function does nothing. - /// - /// - /// If the specified window is a full screen window, the resolution chosen for the window is restored on the selected monitor. - /// - /// - /// The window to restore. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void RestoreWindow(Window* window) => glfwRestoreWindow(window); - - /// - /// - /// This function sets the character callback of the specified window, which is called when a Unicode character is input. - /// - /// - /// The character callback is intended for Unicode text input. As it deals with characters, - /// it is keyboard layout dependent, whereas the key callback is not. Characters do not map 1:1 to physical keys - /// as a key may produce zero, one, or more characters. - /// - /// - /// If you want to know whether a specific physical key was pressed or released, see the key callback instead. - /// - /// - /// The character callback behaves as system text input normally does - /// and will not be called if modifier keys are held down that would prevent normal text input on that platform, - /// for example a Super (Command) key on OS X or Alt key on Windows. - /// - /// - /// There is a character with modifiers callback() that receives these events. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetCharCallback( - Window* window, - GLFWCallbacks.CharCallback callback) - { - return glfwSetCharCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the character with modifiers callback of the specified window, - /// which is called when a Unicode character is input regardless of what modifier keys are used. - /// - /// - /// The character with modifiers callback is intended for implementing custom Unicode character input. - /// For regular Unicode text input, see the character callback. - /// - /// - /// Like the character callback(), - /// the character with modifiers callback deals with characters and is keyboard layout dependent. - /// Characters do not map 1:1 to physical keys, as a key may produce zero, one, or more characters. - /// - /// - /// If you want to know whether a specific physical key was pressed or released, - /// see the key callback() instead. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// The previously set callback, or null if no callback was set or an error occurred. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetCharModsCallback( - Window* window, - GLFWCallbacks.CharModsCallback callback) - { - return glfwSetCharCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the system clipboard to the specified, UTF-8 encoded string. - /// - /// - /// The window that will own the clipboard contents. - /// A UTF-8 encoded string. - /// - /// - /// The specified string is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void SetClipboardString(Window* window, string data) - { - var ptr = Marshal.StringToCoTaskMemUTF8(data); - - try - { - glfwSetClipboardString(window, (byte*)ptr); - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// - /// This function sets the system clipboard to the specified, UTF-8 encoded string. - /// - /// - /// The window that will own the clipboard contents. - /// A UTF-8 encoded string. - /// - /// - /// The specified string is copied before this function returns. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void SetClipboardStringRaw(Window* window, byte* data) - { - glfwSetClipboardString(window, data); - } - - /// - /// - /// This function sets the cursor boundary crossing callback of the specified window - /// which is called when the cursor enters or leaves the client area of the window. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetCursorEnterCallback( - Window* window, - GLFWCallbacks.CursorEnterCallback callback) - { - return glfwSetCursorEnterCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the cursor position callback of the specified window, - /// which is called when the cursor is moved. - /// - /// - /// The callback is provided with the position, in screen coordinates, - /// relative to the upper-left corner of the client area of the window. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetCursorPosCallback( - Window* window, - GLFWCallbacks.CursorPosCallback callback) - { - return glfwSetCursorPosCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the file drop callback of the specified window, - /// which is called when one or more dragged files are dropped on the window. - /// - /// - /// Because the path array and its strings may have been generated specifically for that event, - /// they are not guaranteed to be valid after the callback has returned. - /// If you wish to use them after the callback returns, you need to make a deep copy. - /// - /// - /// The window whose callback to set. - /// The new file drop callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetDropCallback( - Window* window, - GLFWCallbacks.DropCallback callback) - { - return glfwSetDropCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the error callback, which is called with an error code - /// and a human-readable description each time a GLFW error occurs. - /// - /// - /// The error callback is called on the thread where the error occurred. - /// If you are using GLFW from multiple threads, your error callback needs to be written accordingly. - /// - /// - /// Because the description string may have been generated specifically for that error, - /// it is not guaranteed to be valid after the callback has returned. - /// If you wish to use it after the callback returns, you need to make a deep copy. - /// - /// - /// Once set, the error callback remains set even after the library has been terminated. - /// - /// - /// The new callback, or null to remove the currently set callback. - /// The previously set callback, or null if no callback was set. - /// - /// - /// This function may be called before . - /// - /// - /// This function must only be called from the main thread. - /// - /// - public static IntPtr SetErrorCallback(GLFWCallbacks.ErrorCallback callback) - { - return glfwSetErrorCallback(Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets an input mode option for the specified window. - /// The mode must be . - /// - /// - /// If the mode is , the value must be one of the following cursor modes: - /// - makes the cursor visible and behaving normally. - /// - makes the cursor invisible when it is over the client area of - /// the window but does not restrict the cursor from leaving. - /// - hides and grabs the cursor, providing virtual - /// and unlimited cursor movement. This is useful for implementing for example 3D camera controls. - /// - /// - /// The window whose input mode to set. - /// . - /// The new value of the specified input mode. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SetInputMode(Window* window, CursorStateAttribute mode, CursorModeValue value) - { - glfwSetInputMode(window, mode, value); - } - - /// - /// - /// This function sets an input mode option for the specified window. - /// The mode must be - /// or . - /// - /// - /// If the mode is , the value must be either true - /// to enable sticky keys, or false to disable it. - /// - /// - /// If sticky keys are enabled, a key press will ensure that - /// returns the next time it is called even if the key had been released before the call. - /// This is useful when you are only interested in whether keys have been pressed but not when or in which order. - /// - /// - /// If the mode is , the value must be either true - /// to enable sticky mouse buttons, or false to disable it. - /// If sticky mouse buttons are enabled, a mouse button press will ensure that - /// returns the next time it is called even if the mouse button had been released before the call. - /// This is useful when you are only interested in whether mouse buttons have been pressed but not when or in which order. - /// - /// - /// The window whose input mode to set. - /// - /// Either or . - /// - /// The new value of the specified input mode. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include , and . - /// - /// - public static unsafe void SetInputMode(Window* window, StickyAttributes mode, bool value) - { - glfwSetInputMode(window, mode, value ? GLFW_TRUE : GLFW_FALSE); - } - - /// - /// - /// This function sets the joystick configuration callback, or removes the currently set callback. - /// This is called when a joystick is connected to or disconnected from the system. - /// - /// - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static IntPtr SetJoystickCallback(GLFWCallbacks.JoystickCallback callback) - { - return glfwSetJoystickCallback(Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the key callback of the specified window, which is called when a key is pressed, repeated or released. - /// - /// - /// The key functions deal with physical keys, with layout independent - /// key tokens() named after their values in the standard US keyboard layout. - /// If you want to input text, use the character callback() instead. - /// - /// - /// When a window loses input focus, it will generate synthetic key release events for all pressed keys. - /// You can tell these events from user-generated events by the fact that the synthetic ones are generated - /// after the focus loss event has been processed, - /// i.e. after the window focus callback() has been called. - /// - /// - /// The scancode of a key is specific to that platform or sometimes even to that machine. - /// Scancodes are intended to allow users to bind keys that don't have a GLFW key token. - /// Such keys have key set to , their state is not saved - /// and so it cannot be queried with . - /// - /// - /// Sometimes GLFW needs to generate synthetic key events, in which case the scancode may be zero. - /// - /// - /// The window whose callback to set. - /// The new key callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetKeyCallback( - Window* window, - GLFWCallbacks.KeyCallback callback) - { - return glfwSetKeyCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the scroll callback of the specified window, - /// which is called when a scrolling device is used, such as a mouse wheel or scrolling area of a touchpad. - /// - /// - /// The scroll callback receives all scrolling input, like that from a mouse wheel or a touchpad scrolling area. - /// - /// - /// The window whose callback to set. - /// The new scroll callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetScrollCallback( - Window* window, - GLFWCallbacks.ScrollCallback callback) - { - return glfwSetScrollCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the monitor configuration callback, or removes the currently set callback. - /// This is called when a monitor is connected to or disconnected from the system. - /// - /// - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static IntPtr SetMonitorCallback(GLFWCallbacks.MonitorCallback callback) - { - return glfwSetMonitorCallback(Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the mouse button callback of the specified window, - /// which is called when a mouse button is pressed or released. - /// - /// - /// When a window loses input focus, - /// it will generate synthetic mouse button release events for all pressed mouse buttons. - /// You can tell these events from user-generated events by the fact that the synthetic ones are generated after - /// the focus loss event has been processed, - /// i.e. after the window focus callback() has been called. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetMouseButtonCallback( - Window* window, - GLFWCallbacks.MouseButtonCallback callback) - { - return glfwSetMouseButtonCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the close callback of the specified window, - /// which is called when the user attempts to close the window, - /// for example by clicking the close widget in the title bar. - /// - /// - /// The close flag is set before this callback is called, - /// but you can modify it at any time with . - /// - /// - /// The close callback is not triggered by . - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// OS X: Selecting Quit from the application menu will trigger the close callback for all windows. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowCloseCallback( - Window* window, - GLFWCallbacks.WindowCloseCallback callback) - { - return glfwSetWindowCloseCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the focus callback of the specified window, - /// which is called when the window gains or loses input focus. - /// - /// - /// After the focus callback is called for a window that lost input focus, - /// synthetic key and mouse button release events will be generated for all such that had been pressed. - /// For more information, see and . - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowFocusCallback( - Window* window, - GLFWCallbacks.WindowFocusCallback callback) - { - return glfwSetWindowFocusCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the icon of the specified window. - /// - /// - /// If passed an array of candidate images, those of or closest to the sizes desired by the system are selected. - /// - /// - /// If no images are specified, the window reverts to its default icon. - /// - /// - /// The desired image sizes varies depending on platform and system settings. - /// The selected images will be rescaled as needed. Good sizes include 16x16, 32x32 and 48x48. - /// - /// - /// The window whose icon to set. - /// The images to create the icon from. If zero images are passed, the window is reset to the default icon. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// The specified image data is copied before this function returns. - /// - /// - /// OS X: The GLFW window has no icon, as it is not a document window, so this function does nothing. - /// The dock icon will be the same as the application bundle's icon. For more information on bundles, - /// see the Bundle Programming Guide in the Mac Developer Library. - /// - /// - public static unsafe void SetWindowIcon(Window* window, ReadOnlySpan images) - { - fixed (Image* ptr = images) - { - glfwSetWindowIcon(window, images.Length, ptr); - } - } - - /// - /// - /// This function sets the icon of the specified window. - /// - /// - /// If passed an array of candidate images, those of or closest to the sizes desired by the system are selected. - /// - /// - /// If no images are specified, the window reverts to its default icon. - /// - /// - /// The desired image sizes varies depending on platform and system settings. - /// The selected images will be rescaled as needed. Good sizes include 16x16, 32x32 and 48x48. - /// - /// - /// The window whose icon to set. - /// The number of images in the specified array, or zero to revert to the default window icon. - /// The images to create the icon from. This is ignored if count is zero. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// The specified image data is copied before this function returns. - /// - /// - /// OS X: The GLFW window has no icon, as it is not a document window, so this function does nothing. - /// The dock icon will be the same as the application bundle's icon. For more information on bundles, - /// see the Bundle Programming Guide in the Mac Developer Library. - /// - /// - public static unsafe void SetWindowIconRaw(Window* window, int count, Image* images) - { - glfwSetWindowIcon(window, count, images); - } - - /// - /// - /// This function sets the iconification callback of the specified window, - /// which is called when the window is iconified or restored. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowIconifyCallback( - Window* window, - GLFWCallbacks.WindowIconifyCallback callback) - { - return glfwSetWindowIconifyCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the window content scale callback of the specified window, - /// which is called when the content scale of the specified window changes. - /// - /// - /// The window whose content scale changed. - /// The new x-axis content scale of the window. - /// The new y-axis content scale of the window. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - /// - public static unsafe IntPtr SetWindowContentScaleCallback( - Window* window, - GLFWCallbacks.WindowContentScaleCallback callback) - { - return glfwSetWindowContentScaleCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the monitor that the window uses for full screen mode or, - /// if the monitor is null, makes it windowed mode. - /// - /// - /// When setting a monitor, this function updates the width, height and refresh rate - /// of the desired video mode and switches to the video mode closest to it. - /// - /// - /// The window position is ignored when setting a monitor. - /// - /// - /// When the monitor is null, the position, width and height are used to place the window client area. - /// The refresh rate is ignored when no monitor is specified. - /// - /// - /// If you only wish to update the resolution of a full screen window or the size of a windowed mode window, - /// see . - /// - /// - /// When a window transitions from full screen to windowed mode, - /// this function restores any previous window settings such as whether it is decorated, - /// floating, resizable, has size or aspect ratio limits, etc.. - /// - /// - /// The window whose monitor, size or video mode to set. - /// The desired monitor, or null to set windowed mode. - /// The desired x-coordinate of the upper-left corner of the client area. - /// The desired y-coordinate of the upper-left corner of the client area. - /// The desired with, in screen coordinates, of the client area or video mode. - /// The desired height, in screen coordinates, of the client area or video mode. - /// The desired refresh rate, in Hz, of the video mode, or . - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - /// - public static unsafe void SetWindowMonitor( - Window* window, - Monitor* monitor, - int x, - int y, - int width, - int height, - int refreshRate) - { - glfwSetWindowMonitor(window, monitor, x, y, width, height, refreshRate); - } - - /// - /// - /// This function sets the position, in screen coordinates, - /// of the upper-left corner of the client area of the specified windowed mode window. - /// - /// - /// If the window is a full screen window, this function does nothing. - /// - /// - /// Do not use this function to move an already visible window - /// unless you have very good reasons for doing so, as it will confuse and annoy the user. - /// - /// - /// The window manager may put limits on what positions are allowed. - /// GLFW cannot and should not override these limits. - /// - /// - /// The window to query. - /// The x-coordinate of the upper-left corner of the client area. - /// The y-coordinate of the upper-left corner of the client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void SetWindowPos(Window* window, int x, int y) - { - glfwSetWindowPos(window, x, y); - } - - /// - /// - /// This function sets the position callback of the specified window, which is called when the window is moved. - /// - /// - /// The callback is provided with the screen position of the upper-left corner of the client area of the window. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowPosCallback( - Window* window, - GLFWCallbacks.WindowPosCallback callback) - { - return glfwSetWindowPosCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// Sets the refresh callback for the specified window. - /// - /// - /// This function sets the refresh callback of the specified window, which is - /// called when the content area of the window needs to be redrawn, for example - /// if the window has been exposed after having been covered by another window. - /// - /// - /// On compositing window systems such as Aero, Compiz, Aqua or Wayland, where - /// the window contents are saved off-screen, this callback may be called only - /// very infrequently or never at all. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowRefreshCallback( - Window* window, - GLFWCallbacks.WindowRefreshCallback callback) - { - return glfwSetWindowRefreshCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the size, in screen coordinates, of the client area of the specified window. - /// - /// - /// For full screen windows, this function updates the resolution of its desired video mode - /// and switches to the video mode closest to it, without affecting the window's context. - /// - /// - /// As the context is unaffected, the bit depths of the framebuffer remain unchanged. - /// - /// - /// If you wish to update the refresh rate of the desired video mode in addition to its resolution, - /// see . - /// - /// - /// The window manager may put limits on what sizes are allowed. - /// GLFW cannot and should not override these limits. - /// - /// - /// The window to resize. - /// The desired width, in screen coordinates, of the window client area. - /// The desired height, in screen coordinates, of the window client area. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - /// - public static unsafe void SetWindowSize(Window* window, int width, int height) - { - glfwSetWindowSize(window, width, height); - } - - /// - /// - /// This function sets the size callback of the specified window, which is called when the window is resized. - /// - /// - /// The callback is provided with the size, in screen coordinates, of the client area of the window. - /// - /// - /// The window whose callback to set. - /// The new callback, or null to remove the currently set callback. - /// - /// The previously set callback, or null if no callback was set or the library had not been initialized. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe IntPtr SetWindowSizeCallback( - Window* window, - GLFWCallbacks.WindowSizeCallback callback) - { - return glfwSetWindowSizeCallback(window, Marshal.GetFunctionPointerForDelegate(callback)); - } - - /// - /// - /// This function sets the value of the close flag of the specified window. - /// - /// - /// This can be used to override the user's attempt to close the window, or to signal that it should be closed. - /// - /// - /// The window whose flag to change. - /// The new value. - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe void SetWindowShouldClose(Window* window, bool value) - { - glfwSetWindowShouldClose(window, value ? GLFW_TRUE : GLFW_FALSE); - } - - /// - /// - /// This function sets the window title, encoded as UTF-8, of the specified window. - /// - /// - /// The window whose title to change. - /// The UTF-8 encoded window title. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// OS X: The window title will not be updated until the next time you process events. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void SetWindowTitle(Window* window, string title) - { - var ptr = Marshal.StringToCoTaskMemUTF8(title); - - try - { - glfwSetWindowTitle(window, (byte*)ptr); - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// - /// This function sets the window title, encoded as UTF-8, of the specified window. - /// - /// - /// The window whose title to change. - /// The UTF-8 encoded window title. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// OS X: The window title will not be updated until the next time you process events. - /// - /// - /// Possible errors include and . - /// - /// - public static unsafe void SetWindowTitleRaw(Window* window, byte* title) - { - glfwSetWindowTitle(window, title); - } - - /// - /// - /// This function makes the specified window visible if it was previously hidden. - /// - /// - /// If the window is already visible or is in full screen mode, this function does nothing. - /// - /// - /// The window to make visible. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static unsafe void ShowWindow(Window* window) => glfwShowWindow(window); - - /// - /// - /// This function sets the swap interval for the current OpenGL or OpenGL ES context, - /// i.e. the number of screen updates to wait from the time was called - /// before swapping the buffers and returning. - /// This is sometimes called vertical synchronization, vertical retrace synchronization or just vsync. - /// - /// - /// A context that supports either of the WGL_EXT_swap_control_tear - /// and GLX_EXT_swap_control_tear extensions also accepts negative swap intervals, - /// which allows the driver to swap immediately even if a frame arrives a little bit late. - /// You can check for these extensions with . - /// - /// - /// A context must be current on the calling thread. - /// Calling this function without a current context will cause a error. - /// - /// - /// - /// The minimum number of screen updates to wait for until the buffers are swapped by . - /// - /// - /// - /// This function is not called during context creation, - /// leaving the swap interval set to whatever is the default on that platform. - /// This is done because some swap interval extensions used by GLFW - /// do not allow the swap interval to be reset to zero once it has been set to a non-zero value. - /// - /// - /// Some GPU drivers do not honor the requested swap interval, - /// either because of a user setting that overrides the application's request or due to bugs in the driver. - /// - /// - /// This function may be called from any thread. - /// - /// - /// Possible errors include , and . - /// - /// - /// - public static void SwapInterval(int interval) => glfwSwapInterval(interval); - - /// - /// - /// This function puts the calling thread to sleep until at least one event is available in the event queue. - /// - /// - /// Once one or more events are available, it behaves exactly like , - /// i.e. the events in the queue are processed and the function then returns immediately. - /// - /// - /// Processing events will cause the window and input callbacks associated with those events to be called. - /// - /// - /// Since not all events are associated with callbacks, - /// this function may return without a callback having been called even if you are monitoring all callbacks. - /// - /// - /// On some platforms, a window move, resize or menu operation will cause event processing to block. - /// This is due to how event processing is designed on those platforms. - /// You can use the window refresh callback () - /// to redraw the contents of your window when necessary during such operations. - /// - /// - /// On some platforms, - /// certain callbacks may be called outside of a call to one of the event processing functions. - /// - /// - /// If no windows exist, this function returns immediately. - /// For synchronization of threads in applications that do not create windows, - /// use your threading library of choice. - /// - /// - /// Event processing is not required for joystick input to work. - /// - /// - /// - /// This function must only be called from the main thread. - /// - /// This function must not be called from a callback. - /// - /// Possible errors include and . - /// - /// - /// - public static void WaitEvents() => glfwWaitEvents(); - - /// - /// - /// This function puts the calling thread to sleep until at least one event is available in the event queue, - /// or until the specified timeout is reached. - /// - /// - /// If one or more events are available, it behaves exactly like , - /// i.e. the events in the queue are processed and the function then returns immediately. - /// - /// - /// Processing events will cause the window and input callbacks associated with those events to be called. - /// - /// - /// The timeout value must be a positive finite number. - /// - /// - /// Since not all events are associated with callbacks, - /// this function may return without a callback having been called even if you are monitoring all callbacks. - /// - /// - /// On some platforms, a window move, resize or menu operation will cause event processing to block. - /// This is due to how event processing is designed on those platforms. - /// - /// - /// You can use the window refresh callback () - /// to redraw the contents of your window when necessary during such operations. - /// - /// - /// On some platforms, - /// certain callbacks may be called outside of a call to one of the event processing functions. - /// - /// - /// If no windows exist, this function returns immediately. - /// - /// - /// For synchronization of threads in applications that do not create windows, - /// use your threading library of choice. - /// - /// - /// Event processing is not required for joystick input to work. - /// - /// - /// The maximum amount of time, in seconds, to wait. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// This function must not be called from a callback. - /// - /// - /// - /// - public static void WaitEventsTimeout(double timeout) => glfwWaitEventsTimeout(timeout); - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// The to set. - /// The new value of the framebuffer attribute hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintInt hint, int value) => glfwWindowHint(hint, value); - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// The to set. - /// The new value of the framebuffer attribute hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintBool hint, bool value) - { - glfwWindowHint(hint, value ? GLFW_TRUE : GLFW_FALSE); - } - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// . - /// The new value of the window hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintClientApi hint, ClientApi value) => glfwWindowHint(hint, value); - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// . - /// The new value of the window hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintReleaseBehavior hint, ReleaseBehavior value) - { - glfwWindowHint(hint, value); - } - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// . - /// The new value of the window hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintContextApi hint, ContextApi value) - { - glfwWindowHint(hint, value); - } - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// . - /// The new value of the window hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintRobustness hint, Robustness value) - { - glfwWindowHint(hint, value); - } - - /// - /// - /// This function sets hints for the next call to . - /// The hints, once set, retain their values - /// until changed by a call to - /// or , or until the library is terminated. - /// - /// - /// This function does not check whether the specified hint values are valid. - /// If you set hints to invalid values this will instead be reported - /// by the next call to . - /// - /// - /// . - /// The new value of the window hint. - /// - /// - /// This function must only be called from the main thread. - /// - /// - /// Possible errors include and . - /// - /// - /// - public static void WindowHint(WindowHintOpenGlProfile hint, OpenGlProfile value) - { - glfwWindowHint(hint, value); - } - - /// - /// - /// This function returns the value of the close flag of the specified window. - /// - /// - /// The window to query. - /// The value of the close flag. - /// - /// - /// This function may be called from any thread. Access is not synchronized. - /// - /// - /// Possible errors include . - /// - /// - public static unsafe bool WindowShouldClose(Window* window) => glfwWindowShouldClose(window) == GLFW_TRUE; - - /// - /// Returns whether the Vulkan loader and an ICD have been found. - /// - /// - /// - /// This function returns whether the Vulkan loader and any minimally functional ICD have been found. - /// - /// - /// The availability of a Vulkan loader and even an ICD does not by itself - /// guarantee that surface creation or even instance creation is possible. - /// For example, on Fermi systems Nvidia will install an ICD that provides no actual Vulkan support. - /// Call to check whether the extensions necessary - /// for Vulkan surface creation are available and - /// to check whether a queue family of a physical device supports image presentation. - /// - /// - /// Possible errors include . - /// - /// - /// This function may be called from any thread. - /// - /// - /// - /// true if Vulkan is minimally available, or false otherwise. - /// - public static bool VulkanSupported() => glfwVulkanSupported() == GLFW_TRUE; - - /// - /// Returns the Vulkan instance extensions required by GLFW. - /// - /// - /// - /// This function returns an array of names of Vulkan instance extensions required by GLFW for - /// creating Vulkan surfaces for GLFW windows. If successful, the list will always contains - /// VK_KHR_surface, so if you don't require any additional extensions you can - /// pass this list directly to the VkInstanceCreateInfo struct. - /// - /// - /// If Vulkan is not available on the machine, this function returns null and generates - /// a error. Call to check - /// whether Vulkan is at least minimally available. - /// - /// - /// If Vulkan is available but no set of extensions allowing window surface creation was found, - /// this function returns null. You may still use Vulkan for off-screen rendering and compute work. - /// - /// - /// Additional extensions may be required by future versions of GLFW. - /// You should check if any extensions you wish to enable are already in the returned array, - /// as it is an error to specify an extension more than once in the VkInstanceCreateInfo struct. - /// - /// - /// macOS: This function currently only supports the VK_MVK_macos_surface extension from MoltenVK. - /// - /// - /// Possible errors include and . - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is guaranteed to be valid only until the library is terminated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// - /// Where to store the number of extensions in the returned array. - /// This is set to zero if an error occurred. - /// - /// - /// An array of ASCII encoded extension names, or null if an error occurred. - /// - public static unsafe byte** GetRequiredInstanceExtensionsRaw(out uint count) - { - fixed (uint* ptr = &count) - { - return glfwGetRequiredInstanceExtensions(ptr); - } - } - - /// - /// Returns the Vulkan instance extensions required by GLFW. - /// - /// - /// - /// This function returns an array of names of Vulkan instance extensions required by GLFW for - /// creating Vulkan surfaces for GLFW windows. If successful, the list will always contains - /// VK_KHR_surface, so if you don't require any additional extensions you can - /// pass this list directly to the VkInstanceCreateInfo struct. - /// - /// - /// If Vulkan is not available on the machine, this function returns null and generates - /// a error. Call to check - /// whether Vulkan is at least minimally available. - /// - /// - /// If Vulkan is available but no set of extensions allowing window surface creation was found, - /// this function returns null. You may still use Vulkan for off-screen rendering and compute work. - /// - /// - /// Additional extensions may be required by future versions of GLFW. - /// You should check if any extensions you wish to enable are already in the returned array, - /// as it is an error to specify an extension more than once in the VkInstanceCreateInfo struct. - /// - /// - /// macOS: This function currently only supports the VK_MVK_macos_surface extension from MoltenVK. - /// - /// - /// Possible errors include and . - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is guaranteed to be valid only until the library is terminated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// - /// Where to store the number of extensions in the returned array. - /// This is set to zero if an error occurred. - /// - /// - /// An array of ASCII encoded extension names, or null if an error occurred. - /// - public static unsafe byte** GetRequiredInstanceExtensionsRaw(uint* count) - { - return glfwGetRequiredInstanceExtensions(count); - } - - /// - /// Returns the Vulkan instance extensions required by GLFW. - /// - /// - /// - /// This function returns an array of names of Vulkan instance extensions required by GLFW for - /// creating Vulkan surfaces for GLFW windows. If successful, the list will always contains - /// VK_KHR_surface, so if you don't require any additional extensions you can - /// pass this list directly to the VkInstanceCreateInfo struct. - /// - /// - /// If Vulkan is not available on the machine, this function returns null and generates - /// a error. Call to check - /// whether Vulkan is at least minimally available. - /// - /// - /// If Vulkan is available but no set of extensions allowing window surface creation was found, - /// this function returns null. You may still use Vulkan for off-screen rendering and compute work. - /// - /// - /// Additional extensions may be required by future versions of GLFW. - /// You should check if any extensions you wish to enable are already in the returned array, - /// as it is an error to specify an extension more than once in the VkInstanceCreateInfo struct. - /// - /// - /// macOS: This function currently only supports the VK_MVK_macos_surface extension from MoltenVK. - /// - /// - /// Possible errors include and . - /// - /// - /// The returned array is allocated and freed by GLFW. You should not free it yourself. - /// It is guaranteed to be valid only until the library is terminated. - /// - /// - /// This function may be called from any thread. - /// - /// - /// - /// An array of ASCII encoded extension names, or null if an error occurred. - /// - public static unsafe string[] GetRequiredInstanceExtensions() - { - var ptr = GetRequiredInstanceExtensionsRaw(out var count); - - var array = new string[count]; - for (var i = 0; i < count; i++) - { - array[i] = Marshal.PtrToStringUTF8((IntPtr) ptr[i]); - } - - return array; - } - - /// - /// Returns the address of the specified Vulkan instance function. - /// - /// - /// - /// This function returns the address of the specified Vulkan core or extension function for - /// the specified instance. If instance is set to null it can return any function exported - /// from the Vulkan loader, including at least the following functions: - /// - /// - /// - /// vkEnumerateInstanceExtensionProperties - /// vkEnumerateInstanceLayerProperties - /// vkCreateInstance - /// vkGetInstanceProcAddr - /// - /// - /// - /// If Vulkan is not available on the machine, this function returns null and generates - /// a error. Call to check - /// whether Vulkan is at least minimally available. - /// - /// - /// This function is equivalent to calling vkGetInstanceProcAddr with a platform-specific - /// query of the Vulkan loader as a fallback. - /// - /// - /// Possible errors include and . - /// - /// - /// The returned function pointer is valid until the library is terminated. - /// - /// - /// - /// The Vulkan instance to query, or null to retrieve functions related to instance creation. - /// - /// The ASCII encoded name of the function. - /// The address of the function, or null if an error occurred. - public static unsafe IntPtr GetInstanceProcAddress(VkHandle instance, string procName) - { - var ptr = Marshal.StringToCoTaskMemUTF8(procName); - - try - { - return glfwGetInstanceProcAddress(instance, (byte*)ptr); - } - finally - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// - /// Returns the address of the specified Vulkan instance function. - /// - /// - /// - /// This function returns the address of the specified Vulkan core or extension function for - /// the specified instance. If instance is set to null it can return any function exported - /// from the Vulkan loader, including at least the following functions: - /// - /// - /// - /// vkEnumerateInstanceExtensionProperties - /// vkEnumerateInstanceLayerProperties - /// vkCreateInstance - /// vkGetInstanceProcAddr - /// - /// - /// - /// If Vulkan is not available on the machine, this function returns null and generates - /// a error. Call to check - /// whether Vulkan is at least minimally available. - /// - /// - /// This function is equivalent to calling vkGetInstanceProcAddr with a platform-specific - /// query of the Vulkan loader as a fallback. - /// - /// - /// Possible errors include and . - /// - /// - /// The returned function pointer is valid until the library is terminated. - /// - /// - /// - /// The Vulkan instance to query, or null to retrieve functions related to instance creation. - /// - /// The ASCII encoded name of the function. - /// The address of the function, or null if an error occurred. - public static unsafe IntPtr GetInstanceProcAddressRaw(VkHandle instance, byte* procName) - { - return glfwGetInstanceProcAddress(instance, procName); - } - - /// - /// Returns whether the specified queue family can present images. - /// - /// - /// - /// This function returns whether the specified queue family of the specified physical device - /// supports presentation to the platform GLFW was built for. - /// - /// - /// If Vulkan or the required window surface creation instance extensions are not available - /// on the machine, or if the specified instance was not created with the required extensions, - /// this function returns false and generates a error. - /// Call to check whether Vulkan is at least minimally available and - /// to check what instance extensions are required. - /// - /// - /// Possible errors include and . - /// - /// - /// macOS: This function currently always returns true, as the VK_MVK_macos_surface - /// extension does not provide a vkGetPhysicalDevice*PresentationSupport type function. - /// - /// - /// This function may be called from any thread. - /// For synchronization details of Vulkan objects, see the Vulkan specification. - /// - /// - /// The instance that the physical device belongs to. - /// The physical device that the queue family belongs to. - /// The index of the queue family to query. - /// true if the queue family supports presentation, or false otherwise. - public static bool GetPhysicalDevicePresentationSupport(VkHandle instance, VkHandle device, int queueFamily) - { - return glfwGetPhysicalDevicePresentationSupport(instance, device, queueFamily) == GLFW_TRUE; - } - - /// - /// Creates a Vulkan surface for the specified window. - /// - /// - /// - /// This function creates a Vulkan surface for the specified window. - /// - /// - /// If the Vulkan loader or at least one minimally functional ICD were not found, - /// this function returns VK_ERROR_INITIALIZATION_FAILED and generates a - /// error. - /// Call to check whether Vulkan is at least minimally available. - /// - /// - /// If the required window surface creation instance extensions are not available or - /// if the specified instance was not created with these extensions enabled, - /// this function returns VK_ERROR_EXTENSION_NOT_PRESENT and generates a - /// error. - /// Call to check what instance extensions are required. - /// - /// - /// The window surface cannot be shared with another API so the window must have been created with - /// the client api hint set to otherwise it generates a - /// error and returns VK_ERROR_NATIVE_WINDOW_IN_USE_KHR. - /// - /// - /// The window surface must be destroyed before the specified Vulkan instance. - /// It is the responsibility of the caller to destroy the window surface. - /// GLFW does not destroy it for you. Call vkDestroySurfaceKHR to destroy the surface. - /// - /// - /// Possible errors include , , - /// and . - /// - /// - /// If an error occurs before the creation call is made, GLFW returns the Vulkan error code most - /// appropriate for the error. Appropriate use of and - /// should eliminate almost all occurrences of these errors. - /// - /// - /// macOS: This function currently only supports the VK_MVK_macos_surface extension from MoltenVK. - /// - /// - /// macOS: This function creates and sets a CAMetalLayer instance for the window content view, - /// which is required for MoltenVK to function. - /// - /// - /// This function may be called from any thread. - /// For synchronization details of Vulkan objects, see the Vulkan specification. - /// - /// - /// The Vulkan instance to create the surface in. - /// The window to create the surface for. - /// The allocator to use, or null to use the default allocator. - /// - /// Where to store the handle of the surface. - /// This is set to VK_NULL_HANDLE if an error occurred. - /// - /// - /// VK_SUCCESS if successful, or a Vulkan error code if an error occurred. - /// - public static unsafe int CreateWindowSurface( - VkHandle instance, - Window* window, - void* allocator, - VkHandle surface) - { - return glfwCreateWindowSurface(instance, window, allocator, surface); - } - - public static unsafe uint GetX11Window(Window* window) - { - return glfwGetX11Window(window); - } - - public static unsafe IntPtr GetX11Display(Window* window) - { - return glfwGetX11Display(window); - } - - public static unsafe IntPtr GetWin32Window(Window* window) - { - return glfwGetWin32Window(window); - } - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GLFWCallbacks.cs b/OpenToolkit.GraphicsLibraryFramework/GLFWCallbacks.cs deleted file mode 100644 index ebf7b7c7d..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GLFWCallbacks.cs +++ /dev/null @@ -1,180 +0,0 @@ -// -// GLFWCallbacks.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Class containing GLFW related callbacks. - /// - public static unsafe class GLFWCallbacks - { - /// - /// The function signature for Unicode character callback functions. - /// - /// The window that received the event. - /// The Unicode code point of the character. - /// - public delegate void CharCallback(Window* window, uint codepoint); - - /// - /// The function signature for Unicode character with modifiers callback functions. - /// It is called for each input character, regardless of what modifier keys are held down. - /// - /// The window that received the event. - /// The Unicode code point of the character. - /// Bit field describing which modifier keys were held down. - /// - public delegate void CharModsCallback(Window* window, uint codepoint, KeyModifiers modifiers); - - /// - /// The function signature for cursor enter/leave callback functions. - /// - /// The window that received the event. - /// true if the cursor entered the window's client area, or false if it left it. - /// - public delegate void CursorEnterCallback(Window* window, bool entered); - - /// - /// The function signature for cursor position callback functions. - /// - /// The window that received the event. - /// The new cursor x-coordinate, relative to the left edge of the client area. - /// The new cursor y-coordinate, relative to the top edge of the client area. - /// - public delegate void CursorPosCallback(Window* window, double x, double y); - - /// - /// The function signature for file drop callbacks. - /// - /// The window that received the event. - /// The number of dropped files. - /// The UTF-8 encoded file and/or directory path names. - /// - public delegate void DropCallback(Window* window, int count, byte** paths); - - /// - /// The function signature for joystick configuration callback functions. - /// - /// The joystick that was connected or disconnected. - /// - /// One of or . - /// - /// - public delegate void JoystickCallback(int joystick, ConnectedState state); - - /// - /// The function signature for keyboard key callback functions. - /// - /// The window that received the event. - /// The keyboard key that was pressed or released. - /// The system-specific scancode of the key. - /// The for that . - /// Bit field describing which modifier keys were held down. - /// - public delegate void KeyCallback(Window* window, Keys key, int scanCode, InputAction action, KeyModifiers mods); - - /// - /// The function signature for mouse button callback functions. - /// - /// The window that received the event. - /// The mouse button that was pressed or released. - /// One of or . - /// Bit field describing which modifier keys were held down. - /// - public delegate void MouseButtonCallback(Window* window, MouseButton button, InputAction action, KeyModifiers mods); // TODO: Make enums for int params in callback - - /// - /// The function signature for scroll callback functions. - /// - /// The window that received the event. - /// The scroll offset along the x-axis. - /// The scroll offset along the y-axis. - /// - public delegate void ScrollCallback(Window* window, double offsetX, double offsetY); - - /// - /// The function signature for monitor configuration callback functions. - /// - /// The monitor that was connected or disconnected. - /// - /// One of or . - /// - /// - public delegate void MonitorCallback(Monitor* monitor, ConnectedState state); - - /// - /// The function signature for window close callback functions. - /// - /// The window that the user attempted to close. - /// - public delegate void WindowCloseCallback(Window* window); - - /// - /// The function signature for window focus callback functions. - /// - /// The window that gained or lost input focus. - /// true if the window was given input focus, or false if it lost it. - /// - public delegate void WindowFocusCallback(Window* window, bool focused); - - /// - /// The function signature for window iconify/restore callback functions. - /// - /// The window that was iconified or restored. - /// true if the window was iconified(minimized), or false if it was restored. - /// - public delegate void WindowIconifyCallback(Window* window, bool iconified); - - /// - /// The function signature for window position callback functions. - /// - /// The window that was moved. - /// - /// The new x-coordinate, in screen coordinates, of the upper-left corner of the client area of the window. - /// - /// - /// The new y-coordinate, in screen coordinates, of the upper-left corner of the client area of the window. - /// - /// - public delegate void WindowPosCallback(Window* window, int x, int y); - - /// - /// The function signature for window size callback functions. - /// - /// The window that was resized. - /// The new width, in screen coordinates, of the window. - /// The new height, in screen coordinates, of the window. - /// - public delegate void WindowSizeCallback(Window* window, int width, int height); - - /// - /// The function signature for error callback functions. - /// - /// An error code. - /// A UTF-8 encoded string describing the error. - public delegate void ErrorCallback(ErrorCode error, string description); - - /// - /// The function signature for window refresh functions. - /// - /// The window that needs to be refreshed. - public delegate void WindowRefreshCallback(Window* window); - - /// - /// This is the function pointer type for window content scale callbacks. - /// - /// The window whose content scale changed. - /// The new x-axis content scale of the window. - /// The new y-axis content scale of the window. - /// - public delegate void WindowContentScaleCallback(Window* window, float xscale, float yscale); - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GLFWException.cs b/OpenToolkit.GraphicsLibraryFramework/GLFWException.cs deleted file mode 100644 index e723e61b3..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GLFWException.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// GLFWException.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System; -using System.Runtime.Serialization; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Represents errors that occur within GLFW. - /// - [Serializable] - public class GLFWException : Exception - { - /// - /// Gets the underlying GLFW-error code. - /// - public ErrorCode ErrorCode { get; } - - /// - /// Initializes a new instance of the class. - /// - public GLFWException() - { - } - - /// - /// Initializes a new instance of the class with the specified detailed description. - /// - /// A detailed description of the error. - public GLFWException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class - /// with the specified detailed description and GLFW error code. - /// - /// A detailed description of the error. - /// The GLFW error code causing the exception. - public GLFWException(string message, ErrorCode errorCode) - : base(message) - { - ErrorCode = errorCode; - } - - /// - /// Initializes a new instance of the class with the specified detailed description - /// and the specified exception. - /// - /// A detailed description of the error. - /// A reference to the inner exception that is the cause of this exception. - public GLFWException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GLFWNative.cs b/OpenToolkit.GraphicsLibraryFramework/GLFWNative.cs deleted file mode 100644 index e8673ba28..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GLFWNative.cs +++ /dev/null @@ -1,414 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - internal static unsafe class GLFWNative - { - private const string LibraryName = "glfw3.dll"; - - public const int GLFW_TRUE = 1; - public const int GLFW_FALSE = 0; - -#if NETCOREAPP - static GLFWNative() - { - // Register DllImport resolver so that the correct dynamic library is loaded on all platforms. - // On net472, we rely on Mono's DllMap for this. See the .dll.config file. - NativeLibrary.SetDllImportResolver(typeof(GLFWNative).Assembly, (name, assembly, path) => - { - // Please keep in sync with what Robust.Shared/DllMapHelper.cs does. - - if (name != "glfw3.dll") - { - return IntPtr.Zero; - } - - string rName = null; - if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD()) rName = "libglfw.so.3"; - else if (OperatingSystem.IsMacOS()) rName = "libglfw.3.dylib"; - - if ((rName != null) && NativeLibrary.TryLoad(rName, assembly, path, out var handle)) - return handle; - - return IntPtr.Zero; - }); - } -#endif - - [DllImport(LibraryName)] - public static extern int glfwInit(); - - [DllImport(LibraryName)] - public static extern void glfwTerminate(); - - [DllImport(LibraryName)] - public static extern void glfwInitHint(int hint, int value); - - [DllImport(LibraryName)] - public static extern void glfwGetVersion(int* major, int* minor, int* revision); - - [DllImport(LibraryName)] - public static extern byte* glfwGetVersionString(); - - [DllImport(LibraryName)] - public static extern ErrorCode glfwGetError(byte** description); - - [DllImport(LibraryName)] - public static extern Monitor** glfwGetMonitors(int* count); - - [DllImport(LibraryName)] - public static extern void glfwGetMonitorPos(Monitor* monitor, int* x, int* y); - - [DllImport(LibraryName)] - public static extern void glfwGetMonitorPhysicalSize(Monitor* monitor, int* width, int* height); - - [DllImport(LibraryName)] - public static extern void glfwGetMonitorContentScale(Monitor* monitor, float* xscale, float* yscale); - - [DllImport(LibraryName)] - public static extern byte* glfwGetMonitorName(Monitor* monitor); - - [DllImport(LibraryName)] - public static extern void glfwSetMonitorUserPointer(Monitor* monitor, void* pointer); - - [DllImport(LibraryName)] - public static extern void* glfwGetMonitorUserPointer(Monitor* monitor); - - [DllImport(LibraryName)] - public static extern VideoMode* glfwGetVideoModes(Monitor* monitor, int* count); - - [DllImport(LibraryName)] - public static extern void glfwSetGamma(Monitor* monitor, float gamma); - - [DllImport(LibraryName)] - public static extern GammaRamp* glfwGetGammaRamp(Monitor* monitor); - - [DllImport(LibraryName)] - public static extern void glfwSetGammaRamp(Monitor* monitor, GammaRamp* ramp); - - [DllImport(LibraryName)] - public static extern void glfwDefaultWindowHints(); - - [DllImport(LibraryName)] - public static extern void glfwWindowHintString(int hint, byte* value); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowSizeLimits(Window* window, int minwidth, int minheight, int maxwidth, int maxheight); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowAspectRatio(Window* window, int numer, int denom); - - [DllImport(LibraryName)] - public static extern void glfwGetWindowFrameSize(Window* window, int* left, int* top, int* right, int* bottom); - - [DllImport(LibraryName)] - public static extern void glfwGetWindowContentScale(Window* window, float* xscale, float* yscale); - - [DllImport(LibraryName)] - public static extern float glfwGetWindowOpacity(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowOpacity(Window* window, float opacity); - - [DllImport(LibraryName)] - public static extern void glfwRequestWindowAttention(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowAttrib(Window* window, WindowAttributeSetter attrib, int value); - - [DllImport(LibraryName)] - public static extern int glfwRawMouseMotionSupported(); - - [DllImport(LibraryName)] - public static extern byte* glfwGetKeyName(Keys key, int scancode); - - [DllImport(LibraryName)] - public static extern int glfwGetKeyScancode(Keys key); - - [DllImport(LibraryName)] - public static extern InputAction glfwGetKey(Window* window, Keys key); - - [DllImport(LibraryName)] - public static extern InputAction glfwGetMouseButton(Window* window, MouseButton button); - - [DllImport(LibraryName)] - public static extern void glfwGetCursorPos(Window* window, double* xpos, double* ypos); - - [DllImport(LibraryName)] - public static extern void glfwSetCursorPos(Window* window, double xpos, double ypos); - - [DllImport(LibraryName)] - public static extern Cursor* glfwCreateCursor(Image* image, int xhot, int yhot); - - [DllImport(LibraryName)] - public static extern Cursor* glfwCreateStandardCursor(CursorShape shape); - - [DllImport(LibraryName)] - public static extern void glfwDestroyCursor(Cursor* cursor); - - [DllImport(LibraryName)] - public static extern void glfwSetCursor(Window* window, Cursor* cursor); - - [DllImport(LibraryName)] - public static extern int glfwJoystickPresent(int jid); - - [DllImport(LibraryName)] - public static extern float* glfwGetJoystickAxes(int jid, int* count); - - [DllImport(LibraryName)] - public static extern InputAction* glfwGetJoystickButtons(int jid, int* count); - - [DllImport(LibraryName)] - public static extern JoystickHats* glfwGetJoystickHats(int jid, int* count); - - [DllImport(LibraryName)] - public static extern byte* glfwGetJoystickName(int jid); - - [DllImport(LibraryName)] - public static extern byte* glfwGetJoystickGUID(int jid); - - [DllImport(LibraryName)] - public static extern void glfwSetJoystickUserPointer(int jid, void* ptr); - - [DllImport(LibraryName)] - public static extern void* glfwGetJoystickUserPointer(int jid); - - [DllImport(LibraryName)] - public static extern int glfwJoystickIsGamepad(int jid); - - [DllImport(LibraryName)] - public static extern int glfwUpdateGamepadMappings(byte* newMapping); - - [DllImport(LibraryName)] - public static extern byte* glfwGetGamepadName(int jid); - - [DllImport(LibraryName)] - public static extern int glfwGetGamepadState(int jid, GamepadState* state); - - [DllImport(LibraryName)] - public static extern double glfwGetTime(); - - [DllImport(LibraryName)] - public static extern void glfwSetTime(double time); - - [DllImport(LibraryName)] - public static extern long glfwGetTimerValue(); - - [DllImport(LibraryName)] - public static extern long glfwGetTimerFrequency(); - - [DllImport(LibraryName)] - public static extern Window* glfwGetCurrentContext(); - - [DllImport(LibraryName)] - public static extern void glfwSwapBuffers(Window* window); - - [DllImport(LibraryName)] - public static extern int glfwExtensionSupported(byte* extensionName); - - [DllImport(LibraryName)] - public static extern IntPtr glfwGetProcAddress(byte* procame); - - [DllImport(LibraryName)] - public static extern Window* glfwCreateWindow(int width, int height, byte* title, Monitor* monitor, Window* share); - - [DllImport(LibraryName)] - public static extern Monitor* glfwGetPrimaryMonitor(); - - [DllImport(LibraryName)] - public static extern void glfwDestroyWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwFocusWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwGetFramebufferSize(Window* window, int* width, int* height); - - [DllImport(LibraryName)] - public static extern CursorModeValue glfwGetInputMode(Window* window, CursorStateAttribute mode); - - [DllImport(LibraryName)] - public static extern int glfwGetInputMode(Window* window, StickyAttributes mode); - - [DllImport(LibraryName)] - public static extern void glfwRestoreWindow(Window* window); - - [DllImport(LibraryName)] - public static extern VideoMode* glfwGetVideoMode(Monitor* monitor); - - [DllImport(LibraryName)] - public static extern int glfwGetWindowAttrib(Window* window, WindowAttributeGetter attribute); - - [DllImport(LibraryName)] - public static extern void glfwGetWindowSize(Window* window, int* width, int* height); - - [DllImport(LibraryName)] - public static extern void glfwGetWindowPos(Window* window, int* x, int* y); - - [DllImport(LibraryName)] - public static extern Monitor* glfwGetWindowMonitor(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwHideWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwIconifyWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwMakeContextCurrent(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwMaximizeWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwPollEvents(); - - [DllImport(LibraryName)] - public static extern void glfwPostEmptyEvent(); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintInt hint, int value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintBool hint, int value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintClientApi hint, ClientApi value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintReleaseBehavior hint, ReleaseBehavior value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintContextApi hint, ContextApi value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintRobustness hint, Robustness value); - - [DllImport(LibraryName)] - public static extern void glfwWindowHint(WindowHintOpenGlProfile hint, OpenGlProfile value); - - [DllImport(LibraryName)] - public static extern int glfwWindowShouldClose(Window* window); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetCharCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetCharModsCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetCursorEnterCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetCursorPosCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetDropCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetErrorCallback(IntPtr callback); - - [DllImport(LibraryName)] - public static extern void glfwSetInputMode(Window* window, CursorStateAttribute mode, CursorModeValue value); - - [DllImport(LibraryName)] - public static extern void glfwSetInputMode(Window* window, StickyAttributes mode, int value); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetJoystickCallback(IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetKeyCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetScrollCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetMonitorCallback(IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetMouseButtonCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowCloseCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowFocusCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowIcon(Window* window, int count, Image* images); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowIconifyCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowContentScaleCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowTitle(Window* window, byte* title); - - [DllImport(LibraryName)] - public static extern void glfwShowWindow(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowSize(Window* window, int width, int height); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowSizeCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowShouldClose(Window* window, int value); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowMonitor(Window* window, Monitor* monitor, int x, int y, int width, int height, int refreshRate); - - [DllImport(LibraryName)] - public static extern void glfwSetWindowPos(Window* window, int x, int y); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowPosCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern IntPtr glfwSetWindowRefreshCallback(Window* window, IntPtr callback); - - [DllImport(LibraryName)] - public static extern void glfwSwapInterval(int interval); - - [DllImport(LibraryName)] - public static extern void glfwWaitEvents(); - - [DllImport(LibraryName)] - public static extern void glfwWaitEventsTimeout(double timeout); - - [DllImport(LibraryName)] - public static extern byte* glfwGetClipboardString(Window* window); - - [DllImport(LibraryName)] - public static extern void glfwSetClipboardString(Window* window, byte* data); - - [DllImport(LibraryName)] - public static extern int glfwVulkanSupported(); - - [DllImport(LibraryName)] - public static extern byte** glfwGetRequiredInstanceExtensions(uint* count); - - [DllImport(LibraryName)] - public static extern IntPtr glfwGetInstanceProcAddress(VkHandle instance, byte* procName); - - [DllImport(LibraryName)] - public static extern int glfwGetPhysicalDevicePresentationSupport(VkHandle instance, VkHandle device, int queueFamily); - - [DllImport(LibraryName)] - public static extern int glfwCreateWindowSurface(VkHandle instance, Window* window, void* allocator, VkHandle surface); - - [DllImport(LibraryName)] - public static extern uint glfwGetX11Window(Window* window); - - [DllImport(LibraryName)] - public static extern IntPtr glfwGetX11Display(Window* window); - - [DllImport(LibraryName)] - public static extern IntPtr glfwGetWin32Window(Window* window); - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GamepadState.cs b/OpenToolkit.GraphicsLibraryFramework/GamepadState.cs deleted file mode 100644 index ab27b455b..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GamepadState.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// GamepadState.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// This describes the input state of a gamepad. - /// - public struct GamepadState - { - /// - /// State of each of the 15 gamepad buttons, equal to or . - /// - public unsafe fixed byte Buttons[15]; - - /// - /// State of each of the 6 gamepad axes, ranging from -1.0 to 1.0. - /// - public unsafe fixed float Axes[6]; - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/GammaRamp.cs b/OpenToolkit.GraphicsLibraryFramework/GammaRamp.cs deleted file mode 100644 index 2104442cb..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/GammaRamp.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// GammaRamp.cs -// -// Copyright (C) 2019 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Gamma ramp for a . - /// - public unsafe struct GammaRamp - { - /// - /// Red components of the gamma ramp. - /// - public ushort* Red; - - /// - /// Green components of the gamma ramp. - /// - public ushort* Green; - - /// - /// Blue components of the gamma ramp. - /// - public ushort* Blue; - - /// - /// Length of the arrays. - /// - public uint Size; - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Image.cs b/OpenToolkit.GraphicsLibraryFramework/Image.cs deleted file mode 100644 index 50f109040..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Image.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// Image.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Contains GLFW Image data. - /// - public unsafe struct Image - { - /// - /// Initializes a new instance of the struct. - /// - /// The width of the image in pixels. - /// The height of the image in pixels. - /// pointing to the RGBA pixel data of the image. - public Image(int width, int height, byte* pixels) - { - Width = width; - Height = height; - Pixels = pixels; - } - - /// - /// The width, in pixels, of this . - /// - public int Width; - - /// - /// The height, in pixels, of this . - /// - public int Height; - - /// - /// A pointer pointing to the RGBA pixel data. - /// - public byte* Pixels; - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/LICENSE.md b/OpenToolkit.GraphicsLibraryFramework/LICENSE.md deleted file mode 100644 index dac589199..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/LICENSE.md +++ /dev/null @@ -1,23 +0,0 @@ -# MIT License - -Copyright (c) 2006-2019 Stefanos Apostolopoulos for the Open Toolkit project. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -- The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -#### Third party licenses may be applicable. These have been disclosed in [THIRD_PARTIES.md](THIRD_PARTIES.md) diff --git a/OpenToolkit.GraphicsLibraryFramework/Monitor.cs b/OpenToolkit.GraphicsLibraryFramework/Monitor.cs deleted file mode 100644 index bf47bab90..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Monitor.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// Monitor.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Opaque handle to a GLFW monitor. - /// - public struct Monitor - { - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/OpenToolkit.GraphicsLibraryFramework.csproj b/OpenToolkit.GraphicsLibraryFramework/OpenToolkit.GraphicsLibraryFramework.csproj deleted file mode 100644 index 035122c52..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/OpenToolkit.GraphicsLibraryFramework.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - true - 9.0 - true - - - - diff --git a/OpenToolkit.GraphicsLibraryFramework/THIRD_PARTIES.md b/OpenToolkit.GraphicsLibraryFramework/THIRD_PARTIES.md deleted file mode 100644 index 3d1aafe13..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/THIRD_PARTIES.md +++ /dev/null @@ -1,20 +0,0 @@ -# Third parties - -## AdvancedDLSupport -> OpenTK uses AdvancedDLSupport for native interoperability. To enable compatibility with the LGPLv3 License, Firwood has given us a licensing exception. - -* Read the [license grant](AdvancedDLSupport-LICENSE.pdf). -* Read the [license summary](Short-LICENSE.md) for an easy-to-understand version. - -## OpenEXR - -> OpenTK.Half offers Half-to-Single and Single-to-Half conversions based on OpenEXR source code, which is covered by the following license: - -Copyright (c) 2002, Industrial Light & Magic, a division of Lucas Digital Ltd. LLC. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of Industrial Light & Magic nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/OpenToolkit.GraphicsLibraryFramework/VideoMode.cs b/OpenToolkit.GraphicsLibraryFramework/VideoMode.cs deleted file mode 100644 index 91e4a382a..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/VideoMode.cs +++ /dev/null @@ -1,50 +0,0 @@ -// -// VideoMode.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -using System.Runtime.InteropServices; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Replicated handle to a GLFW VideoMode. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct VideoMode - { - /// - /// The width, in screen coordinates, of the . - /// - public int Width; - - /// - /// The height, in screen coordinates, of the . - /// - public int Height; - - /// - /// The bit depth of the red channel of the . - /// - public int RedBits; - - /// - /// The bit depth of the green channel of the . - /// - public int GreenBits; - - /// - /// The bit depth of the blue channel of the . - /// - public int BlueBits; - - /// - /// The refresh rate, in Hz, of the . - /// - public int RefreshRate; - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/VkHandle.cs b/OpenToolkit.GraphicsLibraryFramework/VkHandle.cs deleted file mode 100644 index 251ab4ef8..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/VkHandle.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// A handle to a Vulkan object. - /// - [StructLayout(LayoutKind.Sequential)] - public struct VkHandle - { - /// - /// The actual value of the Vulkan handle. - /// - public IntPtr Handle; - - /// - /// Initializes a new instance of the struct. - /// - /// - /// The native Vulkan handle. - /// This is NOT a pointer to a field containing the handle, this is the actual handle itself. - /// - public VkHandle(IntPtr handle) - { - Handle = handle; - } - } -} diff --git a/OpenToolkit.GraphicsLibraryFramework/Window.cs b/OpenToolkit.GraphicsLibraryFramework/Window.cs deleted file mode 100644 index 83e5e2723..000000000 --- a/OpenToolkit.GraphicsLibraryFramework/Window.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// Window.cs -// -// Copyright (C) 2018 OpenTK -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -namespace OpenToolkit.GraphicsLibraryFramework -{ - /// - /// Opaque handle to a GLFW window. - /// - public struct Window - { - } -} diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e173e421f..a7cd4182a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,4 +1,4 @@ -# Release notes for RobustToolbox. +# Release notes for RobustToolbox. *None yet* +## 270.0.0 + +### Breaking changes + +* Fixed `IClydeWindowInternal` erroneously being public. +* Added a new `[NotContentImplementable]` attribute and made many interfaces in the engine have it. This attribute marks that we may add members to these interfaces in the future, so content should not implement them. +* Removed unused `IRenderableComponent`, `IRand`, and `IPlayerInput` interfaces. + +### New features + +* Added `IsUiOpen` and `IsAnyUiOpen` to `SharedUserInterfaceSystem`. (was in previous engine release, missed in changelog) +* Added `game.time_scale` CVar. + +### Bugfixes + +* Fix a fake error being logged every time when setting the clipboard. +* Fixed audio loading by reverting dependency update to `VorbisPizza`. + +### Other + +* The size of the serializer string map is now logged. + + +## 269.0.1 + +### Bugfixes + +* Fixed transitive project dependencies in content triggering "no direct project reference" detection. + + +## 269.0.0 + +### Breaking changes + +* The project now targets .NET 10. You will have to install the new runtime on game servers when updating. +* We have adopted a new "solution management" system for games. + * This enables us to add new projects to RT (e.g. split stuff up) without causing breaking changes. + * Games must move to `.slnx` solutions and run `dotnet run --project ./RobustToolbox/Tools/Robust.SolutionGen/ -- update` after updating RT. This should be done after *every* RT feature update. +* Games may no longer directly reference RT projects. To depend on these, import the various `.props` files in the `Imports/` folder. +* We've tidied up all the transitive dependencies RT projects used to expose, meaning packages used by *Robust* aren't automatically visible to content projects anymore. You will likely have both accidental usages that are now erroring, or valid usages that you will need to add a `` for. +* `OutputPanel` and `RichTextLabel` now set a default set of "safe" markup tags when using overloads that don't take in a `Type[]? allowedTags`. These tags are formatting only, so dangerous stuff like `[cmdlink]` is blocked by default. +* The constructor of `EntityQuery` has been made internal. + +### New features + +* Added `ExtensionMarkerAttribute`, used by the new C# 14 extension members, for the sandbox. +* Added `CommandWhenUIFocused` property to `Command` keybinds, to make them not fire when a UI control is focused. +* Startup logging now lists total memory and AVX10 intrinsics. +* Added new `FormattedString` type that represents a plain `string` that has markup formatting. +* Added an analyzer to detect redundant `[Prototype("foobar")]` strings. +* Added an analyzer to detect `DirtyField()` calls with incorrect field names. + +### Bugfixes + +* Fixed `FormattedMessage` not escaping plain text content properly with `.ToMarkup()`. +* Fixed wrapping on inline rich text controls like links. +* Fixed some native libs getting packaged for Linux clients when they shouldn't. +* Fixed `TilesEnumerator` being able to stack overflow due to the recursive implementation. +* Fixed some typos in `EntityDeserializer` log messages. +* Fixed WebView control resizing being fucky. +* Fixed `DataDefinitionAnalyzer` to recognize `[MeansDataDefinition]` attributes. + +### Other + +* Updated NuGet package dependencies. +* Prototype loading now tries to do some basic interning to avoid duplicate string objects being stored. This saves some memory. +* Avoid redundant texture uploads on WebView controls. +* Updated and added a lot of documentation to various parts of the engine. +* Moved to `.slnx`, and changed the default marker filename for hot reload to `.slnx` too. +* Removed GLFW windowing implementation. +* `EntityQuery.Resolve` now logs more info on error. +* Disabled some unnecessary .NET SDK source generators that slowed down build. +* Removed kdialog/nfd file dialog implementation. + +### Internal + +* Added a prototype `AspectRatioPanel` control. Not stabilized yet. +* Added gay colors to uitest. +* "Test content master" RT workflow now replaces `global.json` in SS14. +* Updated `Robust.LoaderApi` and `NetSerializer` to .NET 10. +* Fixed all the configurations in `RobustToolbox.sln`. +* Split up `Robust.UnitTesting` into many more projects. +* Internal warning fixes. + + +## 268.1.0 + +### New features + +* Added `IReplayFileWriter.WriteYaml()`, for writing yaml documents to a replay zip file. +* Added Caps Lock as a proper bindable key. +* Added `IParallelBulkRobustJob` as an alternative to `IParallelRobustJob`, taking ranges instead of indices. +* Allow content to override `ProcessStream` and `GetOcclusion` in `AudioSystem` + +### Bugfixes + +* `ActorComponent` now has the `UnsavedComponentAttribute` + * Previously it was unintentionally get serialized to yaml, which could result in NREs when deserializing. +* Don't spam error messages on startup trying to draw splash logos for projects that don't have one. +* Fix `SpriteSystem.LayerExists` saying that layer 0 is invalid. +* Fix `ButtonGroup`s unpressing buttons in an edge case with UI rebuilding. +* Added `CreatedTime` to `NetUserData`. +* Fix loading of `WebView`. + +### Other + +* Reverted undocumented change from 268.0.0 which obsoleted many `IoCManager` methods. +* Fix .NET 10 serializer compatibility of `BitArray`. (backported to older engines). +* Revert performance change to physics due to issues (double-buffered contact events). +* Audio entities are marked as `HideSpawnMenu` now. +* Make `SharedAudioSystem.Stop` not do nothing when the current tick has already been predicted. +* Warning cleanup. + +### Internal + +* Consolidated and updated physics benchmarks. + + +## 268.0.0 + +### Breaking changes + +* Events that are raised via `IEventBus.RaiseComponentEvent()` now **must** be annotated with the `ComponentEventAttribute`. + * By default, events annotated with this attribute can **only** be raised via `IEventBus.RaiseComponentEvent()`. This can be configured via `ComponentEventAttribute.Exclusive` +* StartCollide and EndCollide events are now buffered until the end of physics substeps instead of being raised during the CollideContacts step. EndCollide events are double-buffered and any new ones raised while the events are being dispatched will now go out on the next tick / substep. + +### New features + +* Added `IUserInterfaceManager.ControlSawmill` and `Control.Log` properties so that controls can easily use logging without using static methods. + + +## 267.4.0 + +### New features + +* Added two new custom yaml serializers `CustomListSerializer` and `CustomArraySerializer`. +* CVars defined in `[CVarDefs]` can now be private or internal. +* Added config rollback system to `IConfigurationManager`. This enables CVars to be snapshot and rolled back, even in the event of client crash. +* `OptionButton` now has a `Filterable` property that gives it a text box to filter options. +* Added `FontTagHijackHolder` to replace fonts resolved by `FontTag`. +* Sandbox: + * Exposed `System.Reflection.Metadata.MetadataUpdateHandlerAttribute`. + * Exposed more overloads on `StringBuilder`. +* The engine can now load system fonts. + * At the moment only available on Windows. + * See `ISystemFontManager` for API. +* The client now display a loading screen during startup. + +### Bugfixes + +* Fix `Menu` and `NumpadDecimal` key codes on SDL3. +* client-side predicted entity deletion ( `EntityManager.PredictedQueueDeleteEntity`) now behaves more like it does on the server. In particular, entities will be deleted on the same tick after all system have been updated. Previously, it would process deletions at the beginning of the next tick. +* Fix modifying `Label.FontOverride` not causing a layout update. +* Controls created by rich-text tags now get arranged to a proper size. +* Fix `OutputPanel` scrollbar breaking if a style update changes the font size. + +### Other + +* ComponentNameSerializer will now ignore any components that have been ignored via `IComponentFactory.RegisterIgnore`. +* Add pure to some SharedTransformSystem methods. +* Significantly optimised collision detection in SharedBroadphaseSystem. +* `Control.Stylesheet` does not do any work if assigning the value it already has. +* XAML hot reload now JITs UIs when first opened rather than doing every single one at client startup. This reduces dev startup overhead significantly and probably helps with memory usage too. + +### Internal + +* The `dmetamem` command now sorts its output, and doesn't output to log anymore to avoid output interleaving. + + ## 267.3.0 ### New features diff --git a/Resources/EnginePrototypes/Audio/audio_entities.yml b/Resources/EnginePrototypes/Audio/audio_entities.yml index 5de22856c..1a8a8a08f 100644 --- a/Resources/EnginePrototypes/Audio/audio_entities.yml +++ b/Resources/EnginePrototypes/Audio/audio_entities.yml @@ -2,7 +2,8 @@ id: Audio name: Audio description: Audio entity used by engine - save: false + save: false # TODO PERSISTENCE what about looping or long sounds? + categories: [ HideSpawnMenu ] components: - type: Transform gridTraversal: false diff --git a/Resources/Locale/en-US/controls.ftl b/Resources/Locale/en-US/controls.ftl index 6aed00b21..f23a6e14c 100644 --- a/Resources/Locale/en-US/controls.ftl +++ b/Resources/Locale/en-US/controls.ftl @@ -8,3 +8,5 @@ color-selector-sliders-alpha = A color-selector-sliders-rgb = RGB color-selector-sliders-hsv = HSV + +option-button-filter = Filter diff --git a/Resources/Locale/en-US/input.ftl b/Resources/Locale/en-US/input.ftl index f67bd2c4a..de7d1f832 100644 --- a/Resources/Locale/en-US/input.ftl +++ b/Resources/Locale/en-US/input.ftl @@ -68,6 +68,7 @@ input-key-MouseButton6 = Mouse 6 input-key-MouseButton7 = Mouse 7 input-key-MouseButton8 = Mouse 8 input-key-MouseButton9 = Mouse 9 +input-key-CapsLock = Caps Lock input-key-LSystem-win = Left Win input-key-RSystem-win = Right Win diff --git a/Resources/Locale/pt-BR/input.ftl b/Resources/Locale/pt-BR/input.ftl index ce061acb4..90c5ea153 100644 --- a/Resources/Locale/pt-BR/input.ftl +++ b/Resources/Locale/pt-BR/input.ftl @@ -43,6 +43,7 @@ input-key-MouseButton6 = Mouse 6 input-key-MouseButton7 = Mouse 7 input-key-MouseButton8 = Mouse 8 input-key-MouseButton9 = Mouse 9 +input-key-CapsLock = Caps Lock input-key-LSystem-win = Left Win input-key-RSystem-win = Right Win diff --git a/Robust.Analyzers.Tests/PrototypeAnalyzerTest.cs b/Robust.Analyzers.Tests/PrototypeAnalyzerTest.cs new file mode 100644 index 000000000..5a6b676d0 --- /dev/null +++ b/Robust.Analyzers.Tests/PrototypeAnalyzerTest.cs @@ -0,0 +1,177 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using VerifyCS = + Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier; + +namespace Robust.Analyzers.Tests; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture] +[TestOf(typeof(PrototypeAnalyzer))] +public sealed class PrototypeAnalyzerTest +{ + private static Task Verifier(string code, params DiagnosticResult[] expected) + { + var test = new RTAnalyzerTest() + { + TestState = + { + Sources = { code } + }, + }; + + TestHelper.AddEmbeddedSources( + test.TestState, + "Robust.Shared.Prototypes.Attributes.cs", + "Robust.Shared.Prototypes.IPrototype.cs", + "Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs" + ); + + // ExpectedDiagnostics cannot be set, so we need to AddRange here... + test.TestState.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(); + } + + [Test] + public async Task RedundantTypeTest() + { + const string code = """ + using Robust.Shared.Prototypes; + + [Prototype] + public sealed partial class GoodAutoPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype("someOtherName")] + public sealed partial class GoodUnmatchedPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype("badMatched")] + public sealed partial class BadMatchedPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype(ProtoName)] + public sealed partial class GoodNonLiteralMatchedPrototype : IPrototype + { + public const string ProtoName = "goodNonLiteralMatched"; + + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype(ProtoName)] + public sealed partial class GoodNonLiteralUnmatchedPrototype : IPrototype + { + public const string ProtoName = "someOtherNameEntirely"; + + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype("goodDoesNotEndWithPrototypeWord")] + public sealed partial class GoodDoesNotEndWithPrototypeWord : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + """; + + await Verifier(code, + // /0/Test0.cs(9,2): warning RA0033: Prototype BadMatchedPrototype has explicitly set type "badMatched" that matches autogenerated value + VerifyCS.Diagnostic(PrototypeAnalyzer.PrototypeRedundantTypeRule).WithSpan(17, 12, 17, 24).WithArguments("BadMatchedPrototype", "badMatched") + ); + } + + [Test] + public async Task AliasTest() + { + const string code = """ + using Robust.Shared.Prototypes; + using PPrototypeAttribute = Robust.Shared.Prototypes.PrototypeAttribute; + + [PPrototype("badMatched")] + public sealed partial class BadMatchedPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + """; + + await Verifier(code, + // /0/Test0.cs(4,2): warning RA0042: Prototype BadMatchedPrototype has explicitly set type "badMatched" that matches autogenerated value + VerifyCS.Diagnostic(PrototypeAnalyzer.PrototypeRedundantTypeRule).WithSpan(4, 13, 4, 25).WithArguments("BadMatchedPrototype", "badMatched") + ); + } + + + [Test] + public async Task MoreAttributesTest() + { + const string code = """ + using System; + using Robust.Shared.Prototypes; + using PPrototypeAttribute = Robust.Shared.Prototypes.PrototypeAttribute; + + [FooBarAttribute] + [PPrototype("badMatched")] + public sealed partial class BadMatchedPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class FooBarAttribute : Attribute; + """; + + await Verifier(code, + // /0/Test0.cs(4,2): warning RA0042: Prototype BadMatchedPrototype has explicitly set type "badMatched" that matches autogenerated value + VerifyCS.Diagnostic(PrototypeAnalyzer.PrototypeRedundantTypeRule).WithSpan(6, 13, 6, 25).WithArguments("BadMatchedPrototype", "badMatched") + ); + } + + [Test] + public async Task NameEndsWithPrototypeTest() + { + const string code = """ + using Robust.Shared.Prototypes; + + [Prototype] + public sealed partial class GoodAutoPrototype : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype("ThisIsFine")] + public sealed partial class GoodManual : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + + [Prototype] + public sealed partial class BadAuto : IPrototype + { + [IdDataField] + public string ID { get; private set; } = default!; + } + """; + + await Verifier(code, + // /0/Test0.cs(18,29): error RA0043: Prototype BadAuto does not end with the word Prototype + VerifyCS.Diagnostic(PrototypeAnalyzer.PrototypeEndsWithPrototypeRule).WithSpan(18, 29, 18, 36).WithArguments("BadAuto") + ); + } +} diff --git a/Robust.Analyzers.Tests/PrototypeFixerTest.cs b/Robust.Analyzers.Tests/PrototypeFixerTest.cs new file mode 100644 index 000000000..9200d36a0 --- /dev/null +++ b/Robust.Analyzers.Tests/PrototypeFixerTest.cs @@ -0,0 +1,95 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using VerifyCS = + Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier; + + +namespace Robust.Analyzers.Tests; + +public sealed class PrototypeFixerTest +{ + private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected) + { + var test = new CSharpCodeFixTest() + { + TestState = + { + Sources = { code }, + }, + FixedState = + { + Sources = { fixedCode }, + } + }; + + test.TestState.Sources.Add(("PrototypeAttribute.cs", PrototypeAttributeDef)); + test.FixedState.Sources.Add(("PrototypeAttribute.cs", PrototypeAttributeDef)); + + test.TestState.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(); + } + + private const string PrototypeAttributeDef = """ + using System; + + namespace Robust.Shared.Prototypes + { + public class PrototypeAttribute : Attribute + { + public string? Type { get; internal set; } + public readonly int LoadPriority = 1; + + public PrototypeAttribute(string? type = null, int loadPriority = 1) + { + Type = type; + LoadPriority = loadPriority; + } + + public PrototypeAttribute(int loadPriority) + { + Type = null; + LoadPriority = loadPriority; + } + } + public interface IPrototype; + } + """; + + [Test] + public async Task Test() + { + const string code = """ + using Robust.Shared.Prototypes; + + [Prototype] + public sealed partial class GoodAutoPrototype : IPrototype; + + [Prototype("someOtherName")] + public sealed partial class GoodUnmatchedPrototype : IPrototype; + + [Prototype("badMatched")] + public sealed partial class BadMatchedPrototype : IPrototype; + """; + + const string fixedCode = """ + using Robust.Shared.Prototypes; + + [Prototype] + public sealed partial class GoodAutoPrototype : IPrototype; + + [Prototype("someOtherName")] + public sealed partial class GoodUnmatchedPrototype : IPrototype; + + [Prototype] + public sealed partial class BadMatchedPrototype : IPrototype; + """; + + await Verifier(code, fixedCode, + // /0/Test0.cs(9,2): warning RA0033: Prototype BadMatchedPrototype has explicitly set type "badMatched" that matches autogenerated value + VerifyCS.Diagnostic(PrototypeAnalyzer.PrototypeRedundantTypeRule).WithSpan(9, 12, 9, 24).WithArguments("BadMatchedPrototype", "badMatched") + ); + } +} diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj index a6d59100b..48e416191 100644 --- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj +++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/Robust.Analyzers.Tests/ValidateMemberAnalyzerTest.cs b/Robust.Analyzers.Tests/ValidateMemberAnalyzerTest.cs new file mode 100644 index 000000000..4bd34af22 --- /dev/null +++ b/Robust.Analyzers.Tests/ValidateMemberAnalyzerTest.cs @@ -0,0 +1,96 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using VerifyCS = + Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier; + + +namespace Robust.Analyzers.Tests; + +public sealed class ValidateMemberAnalyzerTest +{ + private static Task Verifier(string code, params DiagnosticResult[] expected) + { + var test = new CSharpAnalyzerTest() + { + TestState = + { + Sources = { code }, + }, + }; + + TestHelper.AddEmbeddedSources( + test.TestState, + "Robust.Shared.Analyzers.ValidateMemberAttribute.cs" + ); + + // ExpectedDiagnostics cannot be set, so we need to AddRange here... + test.TestState.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(); + } + + [Test] + public async Task Test() + { + const string code = """ + using System; + using Robust.Shared.Analyzers; + + public sealed class TestComponent + { + public int IntField; + public bool BoolField; + } + + public sealed class OtherComponent + { + public float FloatField; + public double DoubleField; + } + + public sealed class TestManager + { + public static void DirtyField(T comp, [ValidateMember]string fieldName) { } + public static void DirtyTwoFields(T comp, [ValidateMember]string first, [ValidateMember]string second) { } + } + + public sealed class TestCaller + { + public void Test() + { + var testComp = new TestComponent(); + var otherComp = new OtherComponent(); + + TestManager.DirtyField(testComp, nameof(TestComponent.IntField)); + + TestManager.DirtyField(testComp, nameof(OtherComponent.FloatField)); + + TestManager.DirtyField(otherComp, nameof(TestComponent.IntField)); + + TestManager.DirtyField(otherComp, nameof(OtherComponent.FloatField)); + + TestManager.DirtyTwoFields(testComp, nameof(TestComponent.IntField), nameof(TestComponent.BoolField)); + + TestManager.DirtyTwoFields(testComp, nameof(TestComponent.IntField), nameof(OtherComponent.FloatField)); + + TestManager.DirtyTwoFields(testComp, nameof(OtherComponent.FloatField), nameof(OtherComponent.DoubleField)); + } + } + """; + + await Verifier(code, + // /0/Test0.cs(31,42): error RA0033: FloatField is not a member of TestComponent + VerifyCS.Diagnostic().WithSpan(31, 42, 31, 75).WithArguments("FloatField", "TestComponent"), + // /0/Test0.cs(33,43): error RA0033: IntField is not a member of OtherComponent + VerifyCS.Diagnostic().WithSpan(33, 43, 33, 73).WithArguments("IntField", "OtherComponent"), + // /0/Test0.cs(39,78): error RA0033: FloatField is not a member of TestComponent + VerifyCS.Diagnostic().WithSpan(39, 78, 39, 111).WithArguments("FloatField", "TestComponent"), + // /0/Test0.cs(41,46): error RA0033: FloatField is not a member of TestComponent + VerifyCS.Diagnostic().WithSpan(41, 46, 41, 79).WithArguments("FloatField", "TestComponent"), + // /0/Test0.cs(41,81): error RA0033: DoubleField is not a member of TestComponent + VerifyCS.Diagnostic().WithSpan(41, 81, 41, 115).WithArguments("DoubleField", "TestComponent") + ); + } +} diff --git a/Robust.Analyzers/DataDefinitionAnalyzer.cs b/Robust.Analyzers/DataDefinitionAnalyzer.cs index 747d1fbe5..d5f3fd622 100644 --- a/Robust.Analyzers/DataDefinitionAnalyzer.cs +++ b/Robust.Analyzers/DataDefinitionAnalyzer.cs @@ -16,6 +16,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer { private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; + private const string MeansDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataDefinitionAttribute"; private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute"; private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute"; @@ -270,6 +271,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer return false; return HasAttribute(type, DataDefinitionNamespace) || + MeansDataDefinition(type) || IsImplicitDataDefinition(type); } @@ -425,6 +427,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer return (VVAccess)accessByte == VVAccess.ReadWrite; } + private static bool MeansDataDefinition(ITypeSymbol type) + { + foreach (var attribute in type.GetAttributes()) + { + if (attribute.AttributeClass is null) + continue; + + if (HasAttribute(attribute.AttributeClass, MeansDataDefinitionNamespace)) + return true; + } + return false; + } + private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type) { return HasAttribute(type, NotYamlSerializableName); diff --git a/Robust.Analyzers/ITypeSymbolExtensions.cs b/Robust.Analyzers/ITypeSymbolExtensions.cs new file mode 100644 index 000000000..5fcf614d5 --- /dev/null +++ b/Robust.Analyzers/ITypeSymbolExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace Robust.Analyzers; + +public static class ITypeSymbolExtensions +{ + public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type) + { + var current = type; + while (current != null) + { + yield return current; + current = current.BaseType; + } + } +} diff --git a/Robust.Analyzers/PrototypeAnalyzer.cs b/Robust.Analyzers/PrototypeAnalyzer.cs new file mode 100644 index 000000000..d9f801547 --- /dev/null +++ b/Robust.Analyzers/PrototypeAnalyzer.cs @@ -0,0 +1,139 @@ +#nullable enable +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; +using Robust.Shared.Prototypes; + +namespace Robust.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PrototypeAnalyzer : DiagnosticAnalyzer +{ + public static readonly DiagnosticDescriptor PrototypeRedundantTypeRule = new( + Diagnostics.IdPrototypeRedundantType, + "Redundant Prototype Type specification", + "Prototype {0} has explicitly set type \"{1}\" that matches autogenerated value", + "Usage", + DiagnosticSeverity.Warning, + true, + "Remove the redundant type specification." + ); + + public static readonly DiagnosticDescriptor PrototypeEndsWithPrototypeRule = new( + Diagnostics.IdPrototypeEndsWithPrototype, + "Prototype name must end with the word Prototype", + "Prototype {0} does not end with the word Prototype", + "Usage", + DiagnosticSeverity.Error, + true, + "Add the word Prototype to the end of the class name or manually specify a name in the Prototype attribute." + ); + + public override ImmutableArray SupportedDiagnostics => + [PrototypeRedundantTypeRule, PrototypeEndsWithPrototypeRule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | + GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static ctx => + { + var prototypeAttribute = + ctx.Compilation.GetTypeByMetadataName("Robust.Shared.Prototypes.PrototypeAttribute"); + + // No attribute, no analyzer. + if (prototypeAttribute is null) + return; + + ctx.RegisterSyntaxNodeAction( + symCtx => AnalyzePrototype(symCtx, prototypeAttribute), + SyntaxKind.ClassDeclaration); + }); + } + + private static void AnalyzePrototype(SyntaxNodeAnalysisContext context, INamedTypeSymbol prototypeAttributeSymbol) + { + if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax) + return; + + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (classSymbol is null) + return; + + var className = classSymbol.Name; + + if (!AttributeHelper.HasAttribute(classSymbol, prototypeAttributeSymbol, out var attributeData)) + return; + + var prototypeAttribute = GetAttributeSyntax(attributeData, classDeclarationSyntax); + if (prototypeAttribute == null) + return; + + // Check for autogenerated type + if (prototypeAttribute.ArgumentList?.Arguments[0] is not { } argumentSyntax) + { + if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding)) + { + context.ReportDiagnostic(Diagnostic.Create(PrototypeEndsWithPrototypeRule, + classDeclarationSyntax.Identifier.GetLocation(), + className)); + } + + return; + } + + // We only care about redundancy if the argument is a string literal. + // Passing in a value that resolves to a redundant string is fine. + if (argumentSyntax.Expression is not LiteralExpressionSyntax literalSyntax) + return; + + var literalValue = context.SemanticModel.GetConstantValue(literalSyntax); + if (literalValue.Value is not string specifiedName) + return; + + var autoName = PrototypeUtility.CalculatePrototypeName(className); + + // Check for name redundancy + if (autoName == specifiedName) + { + // If the class name does not end with "Prototype", allow the redundancy + if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding)) + return; + + var location = argumentSyntax.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(PrototypeRedundantTypeRule, + location, + className, + specifiedName)); + } + } + + private static AttributeSyntax? GetAttributeSyntax( + AttributeData attributeData, + ClassDeclarationSyntax classSyntax) + { + if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) + return null; + + foreach (var attributeList in classSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (syntaxReference.SyntaxTree != attribute.SyntaxTree) + continue; + + if (!syntaxReference.Span.OverlapsWith(attribute.Span)) + continue; + + return attribute; + } + } + + return null; + } +} diff --git a/Robust.Analyzers/PrototypeFixer.cs b/Robust.Analyzers/PrototypeFixer.cs new file mode 100644 index 000000000..bb027197b --- /dev/null +++ b/Robust.Analyzers/PrototypeFixer.cs @@ -0,0 +1,77 @@ +#nullable enable +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Robust.Roslyn.Shared.Diagnostics; + +namespace Robust.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public sealed class PrototypeFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => [IdPrototypeRedundantType]; + + public override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + switch (diagnostic.Id) + { + case IdPrototypeRedundantType: + return RegisterRemoveType(context, diagnostic); + } + } + + return Task.CompletedTask; + } + + private static async Task RegisterRemoveType(CodeFixContext context, Diagnostic diagnostic) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + var span = diagnostic.Location.SourceSpan; + var token = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (token == null) + return; + + context.RegisterCodeFix(CodeAction.Create( + "Remove explicitly set type", + c => RemoveType(context.Document, token, c), + "Remove explicitly set type" + ), diagnostic); + } + + private static async Task RemoveType(Document document, AttributeArgumentSyntax syntax, CancellationToken cancellation) + { + var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation); + + if (syntax.Parent is not AttributeArgumentListSyntax argListSyntax) + return document; + + if (argListSyntax.Arguments.Count == 1) + { + // If this is the only argument, remove the whole argument list so we don't leave empty parentheses + if (argListSyntax.Parent is not AttributeSyntax attributeSyntax) + return document; + + var newAttributeSyntax = attributeSyntax.RemoveNode(argListSyntax, SyntaxRemoveOptions.KeepNoTrivia); + root = root!.ReplaceNode(attributeSyntax, newAttributeSyntax!); + } + else + { + // Otherwise just remove the argument + var newArgListSyntax = argListSyntax.WithArguments(argListSyntax.Arguments.Remove(syntax)); + root = root!.ReplaceNode(argListSyntax, newArgListSyntax); + } + + + return document.WithSyntaxRoot(root); + } +} diff --git a/Robust.Analyzers/Robust.Analyzers.csproj b/Robust.Analyzers/Robust.Analyzers.csproj index f6f5023a1..082dca23e 100644 --- a/Robust.Analyzers/Robust.Analyzers.csproj +++ b/Robust.Analyzers/Robust.Analyzers.csproj @@ -33,6 +33,11 @@ + + + + + diff --git a/Robust.Analyzers/ValidateMemberAnalyzer.cs b/Robust.Analyzers/ValidateMemberAnalyzer.cs new file mode 100644 index 000000000..235b230b8 --- /dev/null +++ b/Robust.Analyzers/ValidateMemberAnalyzer.cs @@ -0,0 +1,95 @@ +#nullable enable +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Robust.Roslyn.Shared; + +namespace Robust.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer +{ + private const string ValidateMemberType = "Robust.Shared.Analyzers.ValidateMemberAttribute"; + + private static readonly DiagnosticDescriptor ValidateMemberDescriptor = new( + Diagnostics.IdValidateMember, + "Invalid member name", + "{0} is not a member of {1}", + "Usage", + DiagnosticSeverity.Error, + true, + "Be sure the type and member name are correct."); + + public override ImmutableArray SupportedDiagnostics => [ValidateMemberDescriptor]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterSyntaxNodeAction(AnalyzeExpression, SyntaxKind.InvocationExpression); + } + + private void AnalyzeExpression(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax node) + return; + + if (context.SemanticModel.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol methodSymbol) + return; + + // We need at least one type argument for context + if (methodSymbol.TypeArguments.Length < 1) + return; + + // We'll be checking members of the first type argument + if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol targetType) + return; + + // We defer building this set until we need it later, so we don't have to build it for every single method invocation! + ImmutableHashSet? members = null; + + // Check each parameter of the method + foreach (var parameterContext in node.ArgumentList.Arguments) + { + + // Get the symbol for this parameter + if (context.SemanticModel.GetOperation(parameterContext) is not IArgumentOperation op || op.Parameter is null) + continue; + var parameterSymbol = op.Parameter.OriginalDefinition; + + // Make sure the parameter has the ValidateMember attribute + if (!AttributeHelper.HasAttribute(parameterSymbol, ValidateMemberType, out _)) + continue; + + // Find the value passed for this parameter. + // We use GetConstantValue to resolve compile-time values - i.e. the result of nameof() + if (context.SemanticModel.GetConstantValue(parameterContext.Expression).Value is not string fieldName) + continue; + + // Get a set containing all the members of the target type and its ancestors + members ??= targetType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).ToImmutableHashSet(SymbolEqualityComparer.Default); + + // Check each member of the target type to see if it matches our passed in value + var found = false; + foreach (var member in members) + { + if (member.Name == fieldName) + { + found = true; + break; + } + } + // If we didn't find it, report the violation + if (!found) + context.ReportDiagnostic(Diagnostic.Create( + ValidateMemberDescriptor, + parameterContext.GetLocation(), + fieldName, + targetType.Name + )); + } + } +} diff --git a/Robust.Benchmarks/Configs/DefaultSQLConfig.cs b/Robust.Benchmarks/Configs/DefaultSQLConfig.cs index 2b6eb552b..49beaaa50 100644 --- a/Robust.Benchmarks/Configs/DefaultSQLConfig.cs +++ b/Robust.Benchmarks/Configs/DefaultSQLConfig.cs @@ -59,5 +59,6 @@ public sealed class DefaultSQLConfig : IConfig public CultureInfo CultureInfo => DefaultConfig.Instance.CultureInfo!; public ConfigOptions Options => DefaultConfig.Instance.Options; public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout; + public WakeLockType WakeLock => DefaultConfig.Instance.WakeLock; public IReadOnlyList ConfigAnalysisConclusion => DefaultConfig.Instance.ConfigAnalysisConclusion; } diff --git a/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs new file mode 100644 index 000000000..4fc4f9f61 --- /dev/null +++ b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs @@ -0,0 +1,19 @@ +using System.Numerics; +using BenchmarkDotNet.Attributes; +using Robust.Shared.Analyzers; +using Robust.Shared.Maths; + +namespace Robust.Benchmarks.NumericsHelpers; + +[Virtual, DisassemblyDiagnoser] +public class Box2Benchmark +{ + public Box2 Box = new(); + public Matrix3x2 Matrix = new(); + + [Benchmark] + public Box2 Transform() + { + return Matrix.TransformBox(Box); + } +} diff --git a/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs new file mode 100644 index 000000000..3515af670 --- /dev/null +++ b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs @@ -0,0 +1,18 @@ +using System.Numerics; +using BenchmarkDotNet.Attributes; +using Robust.Shared.Analyzers; +using Robust.Shared.Maths; + +namespace Robust.Benchmarks.NumericsHelpers; + +[Virtual, DisassemblyDiagnoser] +public class Box2RotatedBenchmark +{ + public Box2Rotated Box = new(); + + [Benchmark] + public Matrix3x2 GetTransform() + { + return Box.Transform; + } +} diff --git a/Robust.Benchmarks/NumericsHelpers/GetAABBBenchmark.cs b/Robust.Benchmarks/NumericsHelpers/GetAABBBenchmark.cs new file mode 100644 index 000000000..3325ede4f --- /dev/null +++ b/Robust.Benchmarks/NumericsHelpers/GetAABBBenchmark.cs @@ -0,0 +1,25 @@ +using System.Runtime.Intrinsics; +using BenchmarkDotNet.Attributes; +using Robust.Shared.Analyzers; +using Robust.Shared.Maths; + +namespace Robust.Benchmarks.NumericsHelpers; + +[Virtual, DisassemblyDiagnoser] +public class GetAABBBenchmark +{ + public Vector128 X; + public Vector128 Y; + + [Benchmark(Baseline = true)] + public Vector128 GetAABB_NoAvx() + { + return SimdHelpers.GetAABBSlow(X, Y); + } + + [Benchmark] + public Vector128 GetAABB_Avx() + { + return SimdHelpers.GetAABBAvx(X, Y); + } +} diff --git a/Robust.Benchmarks/NumericsHelpers/SlimPolygonBenchmark.cs b/Robust.Benchmarks/NumericsHelpers/SlimPolygonBenchmark.cs new file mode 100644 index 000000000..e62aaaee8 --- /dev/null +++ b/Robust.Benchmarks/NumericsHelpers/SlimPolygonBenchmark.cs @@ -0,0 +1,21 @@ +using BenchmarkDotNet.Attributes; +using Robust.Shared.Analyzers; +using Robust.Shared.Maths; +using Robust.Shared.Physics.Shapes; + +namespace Robust.Benchmarks.NumericsHelpers; + +// SlimPolyon is internal, so this won't compile without changes. +/* +[Virtual, DisassemblyDiagnoser] +public class SlimPolygonBenchmark +{ + public Box2Rotated RotBox = new(); + + [Benchmark] + public SlimPolygon RotatedBox() + { + return new SlimPolygon(in RotBox); + } +} +*/ diff --git a/Robust.Benchmarks/Physics/BoxStackBenchmark.cs b/Robust.Benchmarks/Physics/BoxStackBenchmark.cs deleted file mode 100644 index 406264f54..000000000 --- a/Robust.Benchmarks/Physics/BoxStackBenchmark.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Numerics; -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.Configuration; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Physics.Systems; -using Robust.UnitTesting.Server; - -namespace Robust.Benchmarks.Physics; - -[Virtual] -[MediumRunJob] -public class PhysicsBoxStackBenchmark -{ - private ISimulation _sim = default!; - - [GlobalSetup] - public void Setup() - { - _sim = RobustServerSimulation.NewSimulation().InitializeInstance(); - - var entManager = _sim.Resolve(); - entManager.System().CreateMap(out var mapId); - SetupTumbler(entManager, mapId); - - for (var i = 0; i < 30; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - [Benchmark] - public void BoxStack() - { - var entManager = _sim.Resolve(); - - for (var i = 0; i < 10000; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - private void SetupTumbler(IEntityManager entManager, MapId mapId) - { - var physics = entManager.System(); - var fixtures = entManager.System(); - - var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId)); - var ground = entManager.AddComponent(groundUid); - - var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0)); - fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground); - - var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10)); - fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground); - - var xs = new[] - { - 0.0f, -10.0f, -5.0f, 5.0f, 10.0f - }; - - var columnCount = 1; - var rowCount = 15; - PolygonShape shape; - - for (var j = 0; j < columnCount; j++) - { - for (var i = 0; i < rowCount; i++) - { - var x = 0.0f; - - var boxUid = entManager.SpawnEntity(null, - new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 1.1f * i), mapId)); - var box = entManager.AddComponent(boxUid); - - physics.SetBodyType(boxUid, BodyType.Dynamic, body: box); - - shape = new PolygonShape(); - shape.SetAsBox(0.5f, 0.5f); - physics.SetFixedRotation(boxUid, false, body: box); - fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true), body: box); - - physics.WakeBody(boxUid, body: box); - physics.SetSleepingAllowed(boxUid, box, false); - } - } - - physics.WakeBody(groundUid, body: ground); - } -} diff --git a/Robust.Benchmarks/Physics/CircleStackBenchmark.cs b/Robust.Benchmarks/Physics/CircleStackBenchmark.cs deleted file mode 100644 index eb79a6b9a..000000000 --- a/Robust.Benchmarks/Physics/CircleStackBenchmark.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Numerics; -using BenchmarkDotNet.Attributes; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Physics.Systems; -using Robust.UnitTesting.Server; - -namespace Robust.Benchmarks.Physics; - -[Virtual] -public class PhysicsCircleStackBenchmark -{ - private ISimulation _sim = default!; - - [GlobalSetup] - public void Setup() - { - _sim = RobustServerSimulation.NewSimulation().InitializeInstance(); - - var entManager = _sim.Resolve(); - entManager.System().CreateMap(out var mapId); - SetupTumbler(entManager, mapId); - - for (var i = 0; i < 30; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - [Benchmark] - public void CircleStack() - { - var entManager = _sim.Resolve(); - - for (var i = 0; i < 10000; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - private void SetupTumbler(IEntityManager entManager, MapId mapId) - { - var physics = entManager.System(); - var fixtures = entManager.System(); - - var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId)); - var ground = entManager.AddComponent(groundUid); - - var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0)); - fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground); - - var vertical = new EdgeShape(new Vector2(20, 0), new Vector2(20, 20)); - fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground); - - var xs = new[] - { - 0.0f, -10.0f, -5.0f, 5.0f, 10.0f - }; - - var columnCount = 1; - var rowCount = 15; - PhysShapeCircle shape; - - for (var j = 0; j < columnCount; j++) - { - for (var i = 0; i < rowCount; i++) - { - var x = 0.0f; - - var boxUid = entManager.SpawnEntity(null, - new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId)); - var box = entManager.AddComponent(boxUid); - - physics.SetBodyType(boxUid, BodyType.Dynamic, body: box); - shape = new PhysShapeCircle(0.5f); - physics.SetFixedRotation(boxUid, false, body: box); - // TODO: Need to detect shape and work out if we need to use fixedrotation - - fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f)); - physics.WakeBody(boxUid, body: box); - physics.SetSleepingAllowed(boxUid, box, false); - } - } - - physics.WakeBody(groundUid, body: ground); - } -} diff --git a/Robust.Benchmarks/Physics/PhysicsBenchmark.cs b/Robust.Benchmarks/Physics/PhysicsBenchmark.cs new file mode 100644 index 000000000..170b58e3d --- /dev/null +++ b/Robust.Benchmarks/Physics/PhysicsBenchmark.cs @@ -0,0 +1,312 @@ +using System; +using System.Numerics; +using BenchmarkDotNet.Attributes; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Dynamics; +using Robust.Shared.Physics.Systems; +using Robust.UnitTesting.Server; + +namespace Robust.Benchmarks.Physics; + +[Virtual, MediumRunJob] +public class PhysicsBenchmark +{ + // TODO: Rain + // Large pyramid + // Joint Grid + // Spinner + // Washer + + const float frameTime = 0.016f; + + #region Many Pyramids + + private ISimulation _manyPyramidSim = default!; + + [GlobalSetup(Target = nameof(ManyPyramids))] + public void PyramidSetup() + { + _manyPyramidSim = RobustServerSimulation.NewSimulation().InitializeInstance(); + + var entManager = _manyPyramidSim.Resolve(); + entManager.System().CreateMap(out var mapId); + SetupManyPyramids(entManager, mapId); + } + + [Benchmark] + public void ManyPyramids() + { + var entManager = _manyPyramidSim.Resolve(); + + for (var i = 0; i < (1f / frameTime) * 10; i++) + { + entManager.TickUpdate(frameTime, false); + } + } + + private void SetupManyPyramids(IEntityManager entManager, MapId mapId) + { + int baseCount = 10; + float extent = 0.5f; + int rowCount = 20; // 5 + int columnCount = 5; + + // Setup ground + var physics = entManager.System(); + var fixtures = entManager.System(); + physics.SetGravity(new Vector2(0f, -9.8f)); + + // Setup boxes + float a = 0.5f; + PolygonShape shape = new(); + shape.SetAsBox(a, a); + + float groundDeltaY = 2.0f * extent * ( baseCount + 1.0f ); + float groundWidth = 2.0f * extent * columnCount * ( baseCount + 1.0f ); + + float groundY = 0.0f; + + for ( int i = 0; i < rowCount; ++i ) + { + var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId)); + var ground = entManager.AddComponent(groundUid); + + var horizontal = new EdgeShape(new Vector2(-0.5f * 2.0f * groundWidth, groundY), new Vector2(0.5f * 2.0f * groundWidth, groundY)); + fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground); + physics.WakeBody(groundUid, body: ground); + groundY += groundDeltaY; + } + + float baseWidth = 2.0f * extent * baseCount; + float baseY = 0.0f; + + for ( int i = 0; i < rowCount; ++i ) + { + for ( int j = 0; j < columnCount; ++j ) + { + float centerX = -0.5f * groundWidth + j * ( baseWidth + 2.0f * extent ) + extent; + CreateSmallPyramid(entManager, mapId, baseCount, extent, centerX, baseY); + } + + baseY += groundDeltaY; + } + } + + private void CreateSmallPyramid(IEntityManager entManager, MapId mapId, int baseCount, float extent, float centerX, float baseY) + { + var physics = entManager.System(); + var fixtures = entManager.System(); + var shape = new PolygonShape(); + shape.SetAsBox(extent, extent); + + for ( int i = 0; i < baseCount; ++i ) + { + float y = ( 2.0f * i + 1.0f ) * extent + baseY; + + for ( int j = i; j < baseCount; ++j ) + { + float x = ( i + 1.0f ) * extent + 2.0f * ( j - i ) * extent + centerX - 0.5f; + + var boxUid = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(x, y), mapId)); + var box = entManager.AddComponent(boxUid); + physics.SetBodyType(boxUid, BodyType.Dynamic, body: box); + + fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f), body: box); + physics.WakeBody(boxUid); + } + } + } + + #endregion + + #region Smash + + private ISimulation _smashSim = default!; + + [GlobalSetup(Target = nameof(Smash))] + public void SmashSetup() + { + _smashSim = RobustServerSimulation.NewSimulation().InitializeInstance(); + + var entManager = _smashSim.Resolve(); + + entManager.System().CreateMap(out var mapId); + + var physics = entManager.System(); + var fixtures = entManager.System(); + var joints = entManager.System(); + var xformSystem = entManager.System(); + physics.SetGravity(new Vector2(0f, -9.8f)); + + { + var smashBox = new PolygonShape(); + smashBox.SetAsBox(4f, 4f); + + var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId)); + var body = entManager.AddComponent(bodyUid); + + physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body); + physics.SetSleepingAllowed(bodyUid, body, false); + physics.SetFixedRotation(bodyUid, false, body: body); + xformSystem.SetLocalPosition(bodyUid, new Vector2(-20f, 0f)); + physics.SetLinearVelocity(bodyUid, new Vector2(40f, 0f)); + + fixtures.TryCreateFixture(bodyUid, smashBox, "fix1", density: 8f, hard: true); + } + + float d = 0.4f; + var box = new PolygonShape(); + box.SetAsBox(0.5f * d, 0.5f * d); + + int columns = 120; // 20 + int rows = 80; // 10 + + for ( int i = 0; i < columns; ++i ) + { + for ( int j = 0; j < rows; ++j ) + { + var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(i * d + 30f, ( j - rows / 2.0f ) * d, mapId)); + var body = entManager.AddComponent(bodyUid); + + physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body); + physics.SetSleepingAllowed(bodyUid, body, false); + physics.SetFixedRotation(bodyUid, false, body: body); + xformSystem.SetLocalPosition(bodyUid, new Vector2(-20f, 0f)); + physics.SetLinearVelocity(bodyUid, new Vector2(40f, 0f)); + + fixtures.TryCreateFixture(bodyUid, box, "fix1", hard: true); + physics.WakeBody(bodyUid); + } + } + } + + [Benchmark] + public void Smash() + { + var entManager = _smashSim.Resolve(); + + for (var i = 0; i < (1f / frameTime) * 10; i++) + { + entManager.TickUpdate(frameTime, false); + } + } + + #endregion + + #region Tumbler + + private ISimulation _tumblerSim = default!; + + [GlobalSetup(Target = nameof(Tumbler))] + public void TumblerSetup() + { + _tumblerSim = RobustServerSimulation.NewSimulation().InitializeInstance(); + + var entManager = _tumblerSim.Resolve(); + + entManager.System().CreateMap(out var mapId); + SetupTumbler(entManager, mapId); + } + + [Benchmark] + public void Tumbler() + { + var entManager = _tumblerSim.Resolve(); + + for (var i = 0; i < 1 / frameTime * 10; i++) + { + entManager.TickUpdate(frameTime, false); + } + } + + private void SetupTumbler(IEntityManager entManager, MapId mapId) + { + var physics = entManager.System(); + var fixtures = entManager.System(); + var joints = entManager.System(); + physics.SetGravity(new Vector2(0f, -9.8f)); + + { + var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId)); + var ground = entManager.AddComponent(groundUid); + // Due to lookup changes fixtureless bodies are invalid, so + var cShape = new PhysShapeCircle(1f); + fixtures.CreateFixture(groundUid, "fix1", new Fixture(cShape, 0, 0, false)); + + var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId)); + var body = entManager.AddComponent(bodyUid); + + physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body); + physics.SetSleepingAllowed(bodyUid, body, false); + physics.SetFixedRotation(bodyUid, false, body: body); + + + // TODO: Box2D just deref, bleh shape structs someday + var shape1 = new PolygonShape(); + shape1.SetAsBox(0.5f, 10.0f, new Vector2(10.0f, 0.0f), 0.0f); + fixtures.CreateFixture(bodyUid, "fix1", new Fixture(shape1, 2, 0, true, 50f)); + + var shape2 = new PolygonShape(); + shape2.SetAsBox(0.5f, 10.0f, new Vector2(-10.0f, 0.0f), 0f); + fixtures.CreateFixture(bodyUid, "fix2", new Fixture(shape2, 2, 0, true, 50f)); + + var shape3 = new PolygonShape(); + shape3.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, 10.0f), 0f); + fixtures.CreateFixture(bodyUid, "fix3", new Fixture(shape3, 2, 0, true, 50f)); + + var shape4 = new PolygonShape(); + shape4.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, -10.0f), 0f); + fixtures.CreateFixture(bodyUid, "fix4", new Fixture(shape4, 2, 0, true, 50f)); + + physics.WakeBody(groundUid, body: ground); + physics.WakeBody(bodyUid, body: body); + var revolute = joints.CreateRevoluteJoint(groundUid, bodyUid); + + var motorSpeed = 25f; + + revolute.LocalAnchorA = new Vector2(0f, 10f); + revolute.LocalAnchorB = new Vector2(0f, 0f); + revolute.ReferenceAngle = 0f; + revolute.MotorSpeed = MathF.PI / 180f * motorSpeed; + revolute.MaxMotorTorque = 100000000f; + revolute.EnableMotor = true; + } + + // Make boxes + { + var gridCount = 20; // 45 + var y = -0.2f * gridCount + 10f; + + var a = 0.125f; + PolygonShape shape = new(); + shape.SetAsBox(a, a); + + for (var i = 0; i < gridCount; i++) + { + var x = -0.2f * gridCount; + + for (var j = 0; j < gridCount; j++) + { + var boxUid = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(x, y), mapId)); + var body = entManager.AddComponent(boxUid); + physics.SetBodyType(boxUid, BodyType.Dynamic, body: body); + + fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f), body: body); + x += 0.4f; + + physics.WakeBody(boxUid, body: body); + physics.SetSleepingAllowed(boxUid, body, false); + } + + y += 0.4f; + } + } + } + + #endregion +} diff --git a/Robust.Benchmarks/Physics/PyramidBenchmark.cs b/Robust.Benchmarks/Physics/PyramidBenchmark.cs deleted file mode 100644 index 505176dfa..000000000 --- a/Robust.Benchmarks/Physics/PyramidBenchmark.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Numerics; -using BenchmarkDotNet.Attributes; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Physics.Systems; -using Robust.UnitTesting.Server; - -namespace Robust.Benchmarks.Physics; - -[Virtual] -public class PhysicsPyramidBenchmark -{ - private ISimulation _sim = default!; - - [GlobalSetup] - public void Setup() - { - _sim = RobustServerSimulation.NewSimulation().InitializeInstance(); - - var entManager = _sim.Resolve(); - entManager.System().CreateMap(out var mapId); - SetupTumbler(entManager, mapId); - - for (var i = 0; i < 300; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - [Benchmark] - public void Pyramid() - { - var entManager = _sim.Resolve(); - - for (var i = 0; i < 5000; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - private void SetupTumbler(IEntityManager entManager, MapId mapId) - { - const byte count = 20; - - // Setup ground - var physics = entManager.System(); - var fixtures = entManager.System(); - var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId)); - var ground = entManager.AddComponent(groundUid); - - var horizontal = new EdgeShape(new Vector2(40, 0), new Vector2(-40, 0)); - fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground); - physics.WakeBody(groundUid, body: ground); - - // Setup boxes - float a = 0.5f; - PolygonShape shape = new(); - shape.SetAsBox(a, a); - - var x = new Vector2(-7.0f, 0.75f); - Vector2 y; - Vector2 deltaX = new Vector2(0.5625f, 1.25f); - Vector2 deltaY = new Vector2(1.125f, 0.0f); - - for (var i = 0; i < count; ++i) - { - y = x; - - for (var j = i; j < count; ++j) - { - var boxUid = entManager.SpawnEntity(null, new MapCoordinates(y, mapId)); - var box = entManager.AddComponent(boxUid); - physics.SetBodyType(boxUid, BodyType.Dynamic, body: box); - - fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f), body: box); - y += deltaY; - - physics.WakeBody(boxUid, body: box); - physics.SetSleepingAllowed(boxUid, box, false); - } - - x += deltaX; - } - } -} diff --git a/Robust.Benchmarks/Physics/TumblerBenchmark.cs b/Robust.Benchmarks/Physics/TumblerBenchmark.cs deleted file mode 100644 index 89a50f2ae..000000000 --- a/Robust.Benchmarks/Physics/TumblerBenchmark.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Numerics; -using BenchmarkDotNet.Attributes; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Physics.Systems; -using Robust.UnitTesting.Server; - -namespace Robust.Benchmarks.Physics; - -[Virtual] -public class PhysicsTumblerBenchmark -{ - private ISimulation _sim = default!; - - [GlobalSetup] - public void Setup() - { - _sim = RobustServerSimulation.NewSimulation().InitializeInstance(); - - var entManager = _sim.Resolve(); - var physics = entManager.System(); - var fixtures = entManager.System(); - var mapUid = entManager.System().CreateMap(out var mapId); - SetupTumbler(entManager, mapId); - - for (var i = 0; i < 300; i++) - { - entManager.TickUpdate(0.016f, false); - var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId)); - var box = entManager.AddComponent(boxUid); - physics.SetBodyType(boxUid, BodyType.Dynamic, body: box); - physics.SetFixedRotation(boxUid, false, body: box); - var shape = new PolygonShape(); - shape.SetAsBox(0.125f, 0.125f); - fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 0.0625f), body: box); - physics.WakeBody(boxUid, body: box); - physics.SetSleepingAllowed(boxUid, box, false); - } - - if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase)) - entManager.System().RebuildBottomUp(mapBroadphase); - } - - [Benchmark] - public void Tumbler() - { - var entManager = _sim.Resolve(); - - for (var i = 0; i < 1000; i++) - { - entManager.TickUpdate(0.016f, false); - } - } - - private void SetupTumbler(IEntityManager entManager, MapId mapId) - { - var physics = entManager.System(); - var fixtures = entManager.System(); - var joints = entManager.System(); - - var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId)); - var ground = entManager.AddComponent(groundUid); - // Due to lookup changes fixtureless bodies are invalid, so - var cShape = new PhysShapeCircle(1f); - fixtures.CreateFixture(groundUid, "fix1", new Fixture(cShape, 0, 0, false)); - - var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId)); - var body = entManager.AddComponent(bodyUid); - - physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body); - physics.SetSleepingAllowed(bodyUid, body, false); - physics.SetFixedRotation(bodyUid, false, body: body); - - - // TODO: Box2D just deref, bleh shape structs someday - var shape1 = new PolygonShape(); - shape1.SetAsBox(0.5f, 10.0f, new Vector2(10.0f, 0.0f), 0.0f); - fixtures.CreateFixture(bodyUid, "fix1", new Fixture(shape1, 2, 0, true, 20f)); - - var shape2 = new PolygonShape(); - shape2.SetAsBox(0.5f, 10.0f, new Vector2(-10.0f, 0.0f), 0f); - fixtures.CreateFixture(bodyUid, "fix2", new Fixture(shape2, 2, 0, true, 20f)); - - var shape3 = new PolygonShape(); - shape3.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, 10.0f), 0f); - fixtures.CreateFixture(bodyUid, "fix3", new Fixture(shape3, 2, 0, true, 20f)); - - var shape4 = new PolygonShape(); - shape4.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, -10.0f), 0f); - fixtures.CreateFixture(bodyUid, "fix4", new Fixture(shape4, 2, 0, true, 20f)); - - physics.WakeBody(groundUid, body: ground); - physics.WakeBody(bodyUid, body: body); - var revolute = joints.CreateRevoluteJoint(groundUid, bodyUid); - revolute.LocalAnchorA = new Vector2(0f, 10f); - revolute.LocalAnchorB = new Vector2(0f, 0f); - revolute.ReferenceAngle = 0f; - revolute.MotorSpeed = 0.05f * MathF.PI; - revolute.MaxMotorTorque = 100000000f; - revolute.EnableMotor = true; - } -} diff --git a/Robust.Benchmarks/Robust.Benchmarks.csproj b/Robust.Benchmarks/Robust.Benchmarks.csproj index 99f07a48d..1d9454f41 100644 --- a/Robust.Benchmarks/Robust.Benchmarks.csproj +++ b/Robust.Benchmarks/Robust.Benchmarks.csproj @@ -6,14 +6,20 @@ ../bin/Benchmarks Exe RA0003 + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Robust.Client.Injectors/Robust.Client.Injectors.csproj b/Robust.Client.Injectors/Robust.Client.Injectors.csproj index b252642f1..9a4a33ce9 100644 --- a/Robust.Client.Injectors/Robust.Client.Injectors.csproj +++ b/Robust.Client.Injectors/Robust.Client.Injectors.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/Robust.UnitTesting/Client/GameObjects/Components/TransformComponentTests.cs b/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs similarity index 100% rename from Robust.UnitTesting/Client/GameObjects/Components/TransformComponentTests.cs rename to Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs diff --git a/Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs b/Robust.Client.IntegrationTests/GameStates/GameStateProcessor_Tests.cs similarity index 100% rename from Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs rename to Robust.Client.IntegrationTests/GameStates/GameStateProcessor_Tests.cs diff --git a/Robust.UnitTesting/Client/Graphics/EyeManagerTest.cs b/Robust.Client.IntegrationTests/Graphics/EyeManagerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/Graphics/EyeManagerTest.cs rename to Robust.Client.IntegrationTests/Graphics/EyeManagerTest.cs diff --git a/Robust.Client.IntegrationTests/Robust.Client.IntegrationTests.csproj b/Robust.Client.IntegrationTests/Robust.Client.IntegrationTests.csproj new file mode 100644 index 000000000..dbaed80ba --- /dev/null +++ b/Robust.Client.IntegrationTests/Robust.Client.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + + enable + false + ../bin/Client.IntegrationTests + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Client/Sprite/SpriteBoundsTest.cs b/Robust.Client.IntegrationTests/Sprite/SpriteBoundsTest.cs similarity index 100% rename from Robust.UnitTesting/Client/Sprite/SpriteBoundsTest.cs rename to Robust.Client.IntegrationTests/Sprite/SpriteBoundsTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/ControlTest.cs b/Robust.Client.IntegrationTests/UserInterface/ControlTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/ControlTest.cs rename to Robust.Client.IntegrationTests/UserInterface/ControlTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/BaseButtonTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/BaseButtonTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/BaseButtonTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/BaseButtonTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/BoxContainerTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/BoxContainerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/BoxContainerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/BoxContainerTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/CenterContainerTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/CenterContainerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/CenterContainerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/CenterContainerTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/GridContainerTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/GridContainerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/GridContainerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/GridContainerTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/LayoutContainerTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/LayoutContainerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/LayoutContainerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/LayoutContainerTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/LineEditTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/LineEditTest.cs similarity index 99% rename from Robust.UnitTesting/Client/UserInterface/Controls/LineEditTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/LineEditTest.cs index 825b146b1..061b5f0a8 100644 --- a/Robust.UnitTesting/Client/UserInterface/Controls/LineEditTest.cs +++ b/Robust.Client.IntegrationTests/UserInterface/Controls/LineEditTest.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; -using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Client.UserInterface.Controls { diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/MultiselectOptionButtonTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/MultiselectOptionButtonTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/MultiselectOptionButtonTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/MultiselectOptionButtonTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/PopupContainerTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/PopupContainerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/PopupContainerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/PopupContainerTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/RadioOptionsTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/RadioOptionsTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/RadioOptionsTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/RadioOptionsTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/TextEditSharedTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/TextEditSharedTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/TextEditSharedTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/TextEditSharedTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/Controls/TextEditTest.cs b/Robust.Client.IntegrationTests/UserInterface/Controls/TextEditTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/Controls/TextEditTest.cs rename to Robust.Client.IntegrationTests/UserInterface/Controls/TextEditTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/StylesheetTest.cs b/Robust.Client.IntegrationTests/UserInterface/StylesheetTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/StylesheetTest.cs rename to Robust.Client.IntegrationTests/UserInterface/StylesheetTest.cs diff --git a/Robust.UnitTesting/Client/UserInterface/UserInterfaceManagerTest.cs b/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs similarity index 100% rename from Robust.UnitTesting/Client/UserInterface/UserInterfaceManagerTest.cs rename to Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs diff --git a/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs b/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs index 5c5e07333..828fd5438 100644 --- a/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs +++ b/Robust.Client.NameGenerator/XamlUiPartialClassGenerator.cs @@ -25,7 +25,9 @@ namespace Robust.Client.NameGenerator /// Adjusted for our UI-Framework & needs. /// [Generator] +#pragma warning disable RS1042 public class XamlUiPartialClassGenerator : ISourceGenerator +#pragma warning restore RS1042 { private const string AttributeName = "Robust.Client.AutoGenerated.GenerateTypedNameReferencesAttribute"; private const string AttributeFile = "GenerateTypedNameReferencesAttribute"; diff --git a/Robust.UnitTesting/Client/Graphics/EyeTest.cs b/Robust.Client.Tests/Graphics/EyeTest.cs similarity index 85% rename from Robust.UnitTesting/Client/Graphics/EyeTest.cs rename to Robust.Client.Tests/Graphics/EyeTest.cs index a992c7286..b4645aba3 100644 --- a/Robust.UnitTesting/Client/Graphics/EyeTest.cs +++ b/Robust.Client.Tests/Graphics/EyeTest.cs @@ -1,14 +1,13 @@ using System.Numerics; using NUnit.Framework; -using Robust.Client.Graphics; using Robust.Shared.Graphics; using Robust.Shared.Map; -using Robust.Shared.Maths; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Client.Graphics +namespace Robust.Client.Tests.Graphics { [TestFixture, Parallelizable, TestOf(typeof(Eye))] - sealed class EyeTest + internal sealed class EyeTest { [Test] public void Constructor_DefaultZoom_isTwo() diff --git a/Robust.UnitTesting/Client/Graphics/StyleBoxTest.cs b/Robust.Client.Tests/Graphics/StyleBoxTest.cs similarity index 93% rename from Robust.UnitTesting/Client/Graphics/StyleBoxTest.cs rename to Robust.Client.Tests/Graphics/StyleBoxTest.cs index afe6986ca..ec1a28918 100644 --- a/Robust.UnitTesting/Client/Graphics/StyleBoxTest.cs +++ b/Robust.Client.Tests/Graphics/StyleBoxTest.cs @@ -3,12 +3,12 @@ using NUnit.Framework; using Robust.Client.Graphics; using Robust.Shared.Maths; -namespace Robust.UnitTesting.Client.Graphics +namespace Robust.Client.Tests.Graphics { [TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(StyleBox))] - public sealed class StyleBoxTest + internal sealed class StyleBoxTest { [Test] public void TestGetEnvelopBox() diff --git a/Robust.UnitTesting/Client/Graphics/TextureLoadParametersTest.cs b/Robust.Client.Tests/Graphics/TextureLoadParametersTest.cs similarity index 95% rename from Robust.UnitTesting/Client/Graphics/TextureLoadParametersTest.cs rename to Robust.Client.Tests/Graphics/TextureLoadParametersTest.cs index c08eb693c..c06d200b7 100644 --- a/Robust.UnitTesting/Client/Graphics/TextureLoadParametersTest.cs +++ b/Robust.Client.Tests/Graphics/TextureLoadParametersTest.cs @@ -1,13 +1,11 @@ -using System.IO; using NUnit.Framework; -using Robust.Client.Graphics; using Robust.Shared.Graphics; using YamlDotNet.RepresentationModel; -namespace Robust.UnitTesting.Client.Graphics +namespace Robust.Client.Tests.Graphics { [TestFixture] - public sealed class TextureLoadParametersTest + internal sealed class TextureLoadParametersTest { [Test] public void TestLoadEmptyYaml() diff --git a/Robust.UnitTesting/Client/Input/KeyboardTest.cs b/Robust.Client.Tests/Input/KeyboardTest.cs similarity index 81% rename from Robust.UnitTesting/Client/Input/KeyboardTest.cs rename to Robust.Client.Tests/Input/KeyboardTest.cs index 77be60742..9f3131455 100644 --- a/Robust.UnitTesting/Client/Input/KeyboardTest.cs +++ b/Robust.Client.Tests/Input/KeyboardTest.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; +using NUnit.Framework; using Robust.Client.Input; -namespace Robust.UnitTesting.Client.Input +namespace Robust.Client.Tests.Input { [Parallelizable(ParallelScope.All)] public sealed class KeyboardTest diff --git a/Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs b/Robust.Client.Tests/ResourceManagement/RsiResourceTest.cs similarity index 98% rename from Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs rename to Robust.Client.Tests/ResourceManagement/RsiResourceTest.cs index f87123f99..2051c3a26 100644 --- a/Robust.UnitTesting/Client/ResourceManagement/RsiResourceTest.cs +++ b/Robust.Client.Tests/ResourceManagement/RsiResourceTest.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Robust.Client.ResourceManagement; -namespace Robust.UnitTesting.Client.ResourceManagement +namespace Robust.Client.Tests.ResourceManagement { [TestOf(typeof(RSIResource))] [Parallelizable(ParallelScope.All)] diff --git a/Robust.Client.Tests/Robust.Client.Tests.csproj b/Robust.Client.Tests/Robust.Client.Tests.csproj new file mode 100644 index 000000000..673e59564 --- /dev/null +++ b/Robust.Client.Tests/Robust.Client.Tests.csproj @@ -0,0 +1,27 @@ + + + + + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/Robust.Client.Tests/Sprite/TransformCenteredBoxTest.cs b/Robust.Client.Tests/Sprite/TransformCenteredBoxTest.cs new file mode 100644 index 000000000..3fd0da499 --- /dev/null +++ b/Robust.Client.Tests/Sprite/TransformCenteredBoxTest.cs @@ -0,0 +1,35 @@ +using System.Numerics; +using NUnit.Framework; +using Robust.Client.Graphics.Clyde; +using Robust.Shared.Maths; +using Robust.UnitTesting; + +namespace Robust.Client.Tests.Sprite; + +[TestFixture] +[TestOf(typeof(Clyde))] +public sealed class TransformCenteredBoxTest +{ + private static IEnumerable<(Box2 box, float angle, Vector2 offset, Vector2 scale)> _args = + [ + (Box2.UnitCentered, 0f, new Vector2(0f,0f), new Vector2(1f,-1f)), + (Box2.UnitCentered, MathF.PI / 7, new Vector2(0f, 0f), new Vector2(1f, -1f)), + (Box2.UnitCentered, MathF.PI / 7, new Vector2(-1f, 2.3f), new Vector2(1f, -1f)), + (Box2.UnitCentered, MathF.PI / 7, new Vector2(-1f, 2.3f), new Vector2(1.25f, -0.32f)), + (new Box2(1,2,3,4), MathF.PI / 7, new Vector2(-1f, 2.3f), new Vector2(1.25f, -0.32f)), + ]; + + [Test] + public void TestTransformCenteredBox([ValueSource(nameof(_args))] + (Box2 box, float angle, Vector2 offset, Vector2 scale) args) + { + var result = Clyde.TransformCenteredBox(args.box, args.angle, args.offset, args.scale); + var expected = Matrix3x2.CreateRotation(args.angle).TransformBox(args.box).Translated(args.offset); + expected = new( + expected.Left * args.scale.X, + expected.Top * args.scale.Y, + expected.Right * args.scale.X, + expected.Bottom * args.scale.Y); + Assert.That(result, Is.Approximately(expected)); + } +} diff --git a/Robust.Client.WebView/Cef/RobustCefApp.cs b/Robust.Client.WebView/Cef/RobustCefApp.cs index 983cc88b0..2a057137a 100644 --- a/Robust.Client.WebView/Cef/RobustCefApp.cs +++ b/Robust.Client.WebView/Cef/RobustCefApp.cs @@ -41,7 +41,7 @@ namespace Robust.Client.WebView.Cef // commandLine.AppendSwitch("--single-process"); //commandLine.AppendSwitch("--disable-gpu"); - //commandLine.AppendSwitch("--disable-gpu-compositing"); + commandLine.AppendSwitch("--disable-gpu-compositing"); //commandLine.AppendSwitch("--in-process-gpu"); commandLine.AppendSwitch("--off-screen-rendering-enabled"); diff --git a/Robust.Client.WebView/Cef/WebViewManagerCef.BrowserWindow.cs b/Robust.Client.WebView/Cef/WebViewManagerCef.BrowserWindow.cs index 13100c10e..56359efa6 100644 --- a/Robust.Client.WebView/Cef/WebViewManagerCef.BrowserWindow.cs +++ b/Robust.Client.WebView/Cef/WebViewManagerCef.BrowserWindow.cs @@ -141,7 +141,7 @@ namespace Robust.Client.WebView.Cef { Closed = true; _manager._browserWindows.Remove(this); - Logger.Debug("Removing window"); + _manager._sawmill.Debug("Removing window"); } private void CheckClosed() diff --git a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs index 72a1af5be..88ab3afa2 100644 --- a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs +++ b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs @@ -402,6 +402,7 @@ namespace Robust.Client.WebView.Cef _data.Browser.GetHost().WasResized(); _data.Texture.Dispose(); _data.Texture = _clyde.CreateBlankTexture((Owner.PixelWidth, Owner.PixelHeight)); + _data.Browser.GetHost().Invalidate(CefPaintElementType.View); } public void Draw(DrawingHandleScreen handle) @@ -409,15 +410,21 @@ namespace Robust.Client.WebView.Cef if (_data == null) return; - var bufImg = _data.Renderer.Buffer.Buffer; + // update texture only when CEF has rendered new content + if (_data.Renderer.IsDirty) + { + _data.Renderer.IsDirty = false; - _data.Texture.SetSubImage( - Vector2i.Zero, - bufImg, - new UIBox2i( - 0, 0, - Math.Min(Owner.PixelWidth, bufImg.Width), - Math.Min(Owner.PixelHeight, bufImg.Height))); + var bufImg = _data.Renderer.Buffer.Buffer; + + _data.Texture.SetSubImage( + Vector2i.Zero, + bufImg, + new UIBox2i( + 0, 0, + Math.Min(Owner.PixelWidth, bufImg.Width), + Math.Min(Owner.PixelHeight, bufImg.Height))); + } handle.UseShader(_shaderInstance); handle.DrawTexture(_data.Texture, Vector2.Zero); @@ -543,6 +550,7 @@ namespace Robust.Client.WebView.Cef { public ImageBuffer Buffer { get; } private ControlImpl _control; + internal volatile bool IsDirty; internal ControlRenderHandler(ControlImpl control) { @@ -596,6 +604,8 @@ namespace Robust.Client.WebView.Cef { Buffer.UpdateBuffer(width, height, buffer, dirtyRect); } + + IsDirty = true; } protected override void OnAcceleratedPaint( diff --git a/Robust.Client.WebView/Robust.Client.WebView.csproj b/Robust.Client.WebView/Robust.Client.WebView.csproj index 6a1cc687a..f965cc3c8 100644 --- a/Robust.Client.WebView/Robust.Client.WebView.csproj +++ b/Robust.Client.WebView/Robust.Client.WebView.csproj @@ -11,11 +11,14 @@ + + + diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs index 3492e5b96..acf412008 100644 --- a/Robust.Client/Audio/AudioSystem.cs +++ b/Robust.Client/Audio/AudioSystem.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using JetBrains.Annotations; using OpenTK.Audio.OpenAL; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -45,6 +46,26 @@ public sealed partial class AudioSystem : SharedAudioSystem [Dependency] private readonly SharedTransformSystem _xformSys = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + /// + /// An optional method that, if provided, will override the behavior of . + /// Contains the same parameters in the same order as the method it overrides. + /// + /// + /// This event only supports a single invocation target. + /// + [PublicAPI] + public event Action? ProcessStreamOverride; + + /// + /// An optional method that, if provided, will override the behavior of . + /// Contains the same parameters in the same order as the method it overrides. + /// + /// + /// This event only supports a single invocation target. + /// + [PublicAPI] + public event Func? GetOcclusionOverride; + /// /// Per-tick cache of relevant streams. /// @@ -341,6 +362,15 @@ public sealed partial class AudioSystem : SharedAudioSystem private void ProcessStream(EntityUid entity, AudioComponent component, TransformComponent xform, MapCoordinates listener) { + // If content wants to process the stream in their own special way, we simply let them handle that. + if (ProcessStreamOverride is not null) + { + // Assert that we are not processing audio multiple times. + DebugTools.Assert(ProcessStreamOverride.HasSingleTarget, $"Event {nameof(ProcessStreamOverride)} has multiple invocation targets. This is not permitted."); + ProcessStreamOverride.Invoke(entity, component, xform, listener); + return; // Exit and do not perform remaining function behavior + } + // TODO: // I Originally tried to be fancier here but it caused audio issues so just trying // to replicate the old behaviour for now. @@ -434,6 +464,14 @@ public sealed partial class AudioSystem : SharedAudioSystem /// public float GetOcclusion(MapCoordinates listener, Vector2 delta, float distance, EntityUid? ignoredEnt = null) { + // If content has defined a custom occlusion method, use that instead. + if (GetOcclusionOverride is not null) + { + // There can only be one occlusion override defined. + DebugTools.Assert(GetOcclusionOverride.HasSingleTarget, $"Event {nameof(GetOcclusionOverride)} has multiple invocation targets. This is not permitted."); + return GetOcclusionOverride.Invoke(listener, delta, distance, ignoredEnt); + } + float occlusion = 0; if (distance > 0.1) diff --git a/Robust.Client/Audio/IAudioManager.cs b/Robust.Client/Audio/IAudioManager.cs index f1d81b2b3..ce65d1d0c 100644 --- a/Robust.Client/Audio/IAudioManager.cs +++ b/Robust.Client/Audio/IAudioManager.cs @@ -7,6 +7,7 @@ namespace Robust.Client.Audio; /// /// Public audio API for stuff that can't go through /// +[NotContentImplementable] public interface IAudioManager { IAudioSource? CreateAudioSource(AudioStream stream); diff --git a/Robust.Client/Audio/Midi/IMidiManager.cs b/Robust.Client/Audio/Midi/IMidiManager.cs index a171564cc..d5b69d294 100644 --- a/Robust.Client/Audio/Midi/IMidiManager.cs +++ b/Robust.Client/Audio/Midi/IMidiManager.cs @@ -4,6 +4,7 @@ using Robust.Shared.Audio.Midi; namespace Robust.Client.Audio.Midi; +[NotContentImplementable] public interface IMidiManager { /// diff --git a/Robust.Client/Audio/Midi/IMidiRenderer.cs b/Robust.Client/Audio/Midi/IMidiRenderer.cs index 3c8c51c97..e5ca95631 100644 --- a/Robust.Client/Audio/Midi/IMidiRenderer.cs +++ b/Robust.Client/Audio/Midi/IMidiRenderer.cs @@ -17,6 +17,7 @@ public enum MidiRendererStatus : byte File, } +[NotContentImplementable] public interface IMidiRenderer : IDisposable { /// diff --git a/Robust.Client/BaseClient.cs b/Robust.Client/BaseClient.cs index 4445e63db..5756c24ce 100644 --- a/Robust.Client/BaseClient.cs +++ b/Robust.Client/BaseClient.cs @@ -61,6 +61,7 @@ namespace Robust.Client NetMessageAccept.Handshake | NetMessageAccept.Client); _configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true); + _configManager.OnValueChanged(CVars.GameTimeScale, TimeScaleChanged, invokeImmediately: true); _playMan.Initialize(0); _playMan.PlayerListUpdated += OnPlayerListUpdated; @@ -95,6 +96,18 @@ namespace Robust.Client _logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}"); } + private void TimeScaleChanged(float timeScale, in CVarChangeInfo info) + { + if (!GameTiming.IsTimescaleValid(timeScale)) + { + _logger.Error($"Invalid time scale set: {timeScale}, ignoring"); + return; + } + + _timing.TimeScale = timeScale; + _logger.Info($"Tickrate changed to: {timeScale} on tick {_timing.CurTick}"); + } + /// public void ConnectToServer(DnsEndPoint endPoint) { diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index bf905bc55..47061d5b7 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -8,6 +8,7 @@ using Robust.Client.GameObjects; using Robust.Client.GameStates; using Robust.Client.Graphics; using Robust.Client.Graphics.Clyde; +using Robust.Client.Graphics.FontManagement; using Robust.Client.HWId; using Robust.Client.Input; using Robust.Client.Localization; @@ -67,6 +68,7 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); + deps.Register(); deps.Register(); deps.Register(); deps.Register(); @@ -108,6 +110,8 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); + deps.Register(); + deps.Register(); switch (mode) { @@ -118,8 +122,9 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); - deps.Register(); deps.Register(); + deps.Register(); + deps.Register(); break; case GameController.DisplayMode.Clyde: deps.Register(); @@ -128,8 +133,9 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); - deps.Register(); deps.Register(); + deps.Register(); + deps.Register(); break; default: throw new ArgumentOutOfRangeException(); @@ -155,6 +161,7 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); + deps.Register(); #if TOOLS deps.Register(); diff --git a/Robust.Client/Configuration/IClientNetConfigurationManager.cs b/Robust.Client/Configuration/IClientNetConfigurationManager.cs index b72b43384..e71ba29a1 100644 --- a/Robust.Client/Configuration/IClientNetConfigurationManager.cs +++ b/Robust.Client/Configuration/IClientNetConfigurationManager.cs @@ -9,6 +9,7 @@ namespace Robust.Client.Configuration; /// A networked configuration manager that controls the replication of /// console variables between client and server. /// +[NotContentImplementable] public interface IClientNetConfigurationManager : INetConfigurationManager { /// diff --git a/Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs b/Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs index 8e4752255..f14909d84 100644 --- a/Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs +++ b/Robust.Client/Console/Commands/DumpMetadataMembersCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Robust.Shared.Console; using Robust.Shared.ContentPack; @@ -19,10 +20,28 @@ namespace Robust.Client.Console.Commands return; } - foreach (var sig in AssemblyTypeChecker.DumpMetaMembers(type)) + var members = AssemblyTypeChecker.DumpMetaMembers(type) + .GroupBy(x => x.IsField) + .ToDictionary(x => x.Key, x => x.Select(t => t.Value).ToList()); + + if (members.TryGetValue(true, out var fields)) { - System.Console.WriteLine(@$"- ""{sig}"""); - shell.WriteLine(sig); + fields.Sort(StringComparer.Ordinal); + + foreach (var member in fields) + { + System.Console.WriteLine(@$"- ""{member}"""); + } + } + + if (members.TryGetValue(false, out var methods)) + { + methods.Sort(StringComparer.Ordinal); + + foreach (var member in methods) + { + System.Console.WriteLine(@$"- ""{member}"""); + } } } diff --git a/Robust.Client/Console/Commands/UITestCommand.cs b/Robust.Client/Console/Commands/UITestCommand.cs index 647a964af..ffd38a45e 100644 --- a/Robust.Client/Console/Commands/UITestCommand.cs +++ b/Robust.Client/Console/Commands/UITestCommand.cs @@ -3,6 +3,7 @@ using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.RichText; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -157,6 +158,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males _tabContainer.AddChild(_sprite); _tabContainer.AddChild(TabCursorShapes()); _tabContainer.AddChild(new TabWrapContainer { Name = nameof(Tab.WrapContainer) }); + _tabContainer.AddChild(new TabOkLab()); } public void OnClosed() @@ -207,7 +209,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males private Control TabRichText() { var label = new RichTextLabel(); - label.SetMessage(FormattedMessage.FromMarkupOrThrow(Lipsum)); + var msg = FormattedMessage.FromMarkupOrThrow(Lipsum); + msg.AddMarkupOrThrow("\n\nWAWWAAWAWWAWA [cmdlink=\"DOES IT WORK\" command=\"help\" /] DOES IT WORK"); + + label.SetMessage(msg, [typeof(CommandLinkTag)]); TabContainer.SetTabTitle(label, "RichText"); return label; diff --git a/Robust.Client/Console/Commands/UITestControl.TabOkLab.cs b/Robust.Client/Console/Commands/UITestControl.TabOkLab.cs new file mode 100644 index 000000000..d4df2de0a --- /dev/null +++ b/Robust.Client/Console/Commands/UITestControl.TabOkLab.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Maths; + +namespace Robust.Client.Console.Commands; + +internal sealed partial class UITestControl +{ + private sealed class TabOkLab : Control + { + public TabOkLab() + { + string[] colors = ["#d52800", "#fd9954", "#ffffff", "#d261a3", "#a30061"]; + + var controls = new List(); + var box = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + + foreach (var c in colors) + { + var control = new ColorControl(c); + controls.Add(control); + box.AddChild(control); + } + + var slider = new Slider + { + MaxValue = 1, + Value = 0.85f, + }; + + var slider2 = new Slider + { + MaxValue = 1, + Value = 1, + }; + + slider.OnValueChanged += _ => UpdateValue(); + slider2.OnValueChanged += _ => UpdateValue(); + UpdateValue(); + + AddChild(new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = + { + slider, + slider2, + box + } + }); + + return; + + void UpdateValue() + { + foreach (var control in controls) + { + var sb = new StringBuilder(); + + var origLab = Color.ToLab(Color.FromSrgb(control.OrigColor)); + + sb.AppendLine($"ORIG:\n{control.OrigColor.ToHex()}"); + sb.AppendLine($"LAB:\n{origLab}"); + + var lch = Color.ToLch(origLab); + sb.AppendLine($"LCH:\n{lch}"); + + lch = Color.ToLch(origLab); + lch.X *= slider.Value; + lch.Y *= slider2.Value; + + var newLab = Color.FromLch(lch); + + var newColor = Color.ToSrgb(Color.FromLab(newLab)); + + sb.AppendLine($"NEW:\n{newColor.ToHex()}"); + sb.AppendLine($"LAB:\n{newLab}"); + sb.AppendLine($"LCH:\n{lch}"); + + control.DataLabel.Text = sb.ToString(); + + var newLab2 = Vector4.Lerp(new Vector4(0, 0, 0, 1), origLab, slider.Value); + + control.PanelModifiedLightnessNaive.BackgroundColor = newColor; + control.PanelModifiedScaleAll.BackgroundColor = Color.ToSrgb(Color.FromLab(newLab2)); + } + } + } + + private sealed class ColorControl : Control + { + public readonly StyleBoxFlat PanelModifiedLightnessNaive = new(); + public readonly StyleBoxFlat PanelModifiedScaleAll = new(); + public readonly Label DataLabel = new(); + public readonly Color OrigColor; + + public ColorControl(string color) + { + HorizontalExpand = true; + + OrigColor = Color.FromHex(color); + + var panelSize = new Vector2(200, 200); + + AddChild(new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = + { + new AspectRatioPanel + { + Children = + { + new PanelContainer + { + PanelOverride = new StyleBoxFlat(OrigColor), + MinSize = panelSize + }, + } + }, + DataLabel, + new AspectRatioPanel + { + Children = + { + new PanelContainer + { + PanelOverride = PanelModifiedLightnessNaive, + MinSize = panelSize + }, + } + }, + new AspectRatioPanel + { + Children = + { + new PanelContainer + { + PanelOverride = PanelModifiedScaleAll, + MinSize = panelSize + }, + } + }, + } + }); + } + } + } +} diff --git a/Robust.Client/Console/IClientConGroupController.cs b/Robust.Client/Console/IClientConGroupController.cs index d43c17256..07974d709 100644 --- a/Robust.Client/Console/IClientConGroupController.cs +++ b/Robust.Client/Console/IClientConGroupController.cs @@ -1,5 +1,6 @@ namespace Robust.Client.Console { + [NotContentImplementable] public interface IClientConGroupController : IClientConGroupImplementation { IClientConGroupImplementation Implementation { set; } diff --git a/Robust.Client/Console/IClientConsoleHost.cs b/Robust.Client/Console/IClientConsoleHost.cs index ea09fa0be..85a5138a4 100644 --- a/Robust.Client/Console/IClientConsoleHost.cs +++ b/Robust.Client/Console/IClientConsoleHost.cs @@ -7,6 +7,7 @@ using Robust.Shared.Utility; namespace Robust.Client.Console { + [NotContentImplementable] public interface IClientConsoleHost : IConsoleHost, IDisposable { /// diff --git a/Robust.Client/Console/IScriptClient.cs b/Robust.Client/Console/IScriptClient.cs index 4c29f7d7b..e7cb0a7c3 100644 --- a/Robust.Client/Console/IScriptClient.cs +++ b/Robust.Client/Console/IScriptClient.cs @@ -3,6 +3,7 @@ namespace Robust.Client.Console /// /// Client manager for server side scripting. /// + [NotContentImplementable] public interface IScriptClient { void Initialize(); diff --git a/Robust.Client/GameController/GameController.Standalone.cs b/Robust.Client/GameController/GameController.Standalone.cs index 5d2d24032..e899f8c7a 100644 --- a/Robust.Client/GameController/GameController.Standalone.cs +++ b/Robust.Client/GameController/GameController.Standalone.cs @@ -15,6 +15,7 @@ namespace Robust.Client internal partial class GameController : IPostInjectInit { private IGameLoop? _mainLoop; + private bool _dontStart; [Dependency] private readonly IClientGameTiming _gameTiming = default!; [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; @@ -162,8 +163,11 @@ namespace Robust.Client return; } - DebugTools.AssertNotNull(_mainLoop); - _mainLoop!.Run(); + if (!_dontStart) + { + DebugTools.AssertNotNull(_mainLoop); + _mainLoop!.Run(); + } CleanupGameThread(); } diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index dc4f1c4f0..02cbb5cde 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -96,6 +96,8 @@ namespace Robust.Client [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IReloadManager _reload = default!; [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!; + [Dependency] private readonly LoadingScreenManager _loadscr = default!; private IWebViewManagerHook? _webViewHook; @@ -133,27 +135,39 @@ namespace Robust.Client return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? ""; } + public bool ShowLoadingBar() + { + return _resourceManifest!.ShowLoadingBar ?? _configurationManager.GetCVar(CVars.LoadingShowBar); + } + internal bool StartupContinue(DisplayMode displayMode) { DebugTools.AssertNotNull(_resourceManifest); - _clyde.InitializePostWindowing(); - _audio.InitializePostWindowing(); - _clyde.SetWindowTitle(GameTitle()); + _loadscr.Initialize(42); - _taskManager.Initialize(); - _parallelMgr.Initialize(); + _loadscr.BeginLoadingSection("Init graphics", dontRender: true); + _clyde.InitializePostWindowing(); + _clyde.SetWindowTitle(GameTitle()); + _loadscr.EndLoadingSection(); + + _loadscr.LoadingStep(_audio.InitializePostWindowing, "Init audio"); + + _loadscr.LoadingStep(_taskManager.Initialize, _taskManager); + _loadscr.LoadingStep(_parallelMgr.Initialize, _parallelMgr); _fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi)); - // Load optional Robust modules. - LoadOptionalRobustModules(displayMode, _resourceManifest!); + _loadscr.LoadingStep(_systemFontManager.Initialize, "System fonts"); + // Load optional Robust modules. + _loadscr.LoadingStep(() => LoadOptionalRobustModules(displayMode, _resourceManifest!), "Robust Modules"); + + _loadscr.BeginLoadingSection(_modLoader); // Disable load context usage on content start. // This prevents Content.Client being loaded twice and things like csi blowing up because of it. _modLoader.SetUseLoadContext(!ContentStart); var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1"; _modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing); - if (!LoadModules()) return false; @@ -162,16 +176,22 @@ namespace Robust.Client _configurationManager.LoadCVarsFromAssembly(loadedModule); } - _serializationManager.Initialize(); - _loc.Initialize(); + _loadscr.EndLoadingSection(); + + _loadscr.LoadingStep(_serializationManager.Initialize, _serializationManager); + _loadscr.LoadingStep(_loc.Initialize, _loc); // Call Init in game assemblies. - _modLoader.BroadcastRunLevel(ModRunLevel.PreInit); + _loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PreInit), "Content PreInit"); - // Finish initialization of WebView if loaded. - _webViewHook?.Initialize(); + _loadscr.LoadingStep(() => + { + // Finish initialization of WebView if loaded. + _webViewHook?.Initialize(); + }, + "WebView init"); - _modLoader.BroadcastRunLevel(ModRunLevel.Init); + _loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.Init), "Content Init"); // Start bad file extensions check after content init, // in case content screws with the VFS. @@ -180,42 +200,51 @@ namespace Robust.Client _configurationManager, _logManager.GetSawmill("res")); - _resourceCache.PreloadTextures(); - _networkManager.Initialize(false); - _configurationManager.SetupNetworking(); - _serializer.Initialize(); - _inputManager.Initialize(); - _console.Initialize(); + _loadscr.LoadingStep(_resourceCache.PreloadTextures, "Texture preload"); + _loadscr.LoadingStep(() => { _networkManager.Initialize(false); }, _networkManager); + _loadscr.LoadingStep(_configurationManager.SetupNetworking, _configurationManager); + _loadscr.LoadingStep(_serializer.Initialize, _serializer); + _loadscr.LoadingStep(_inputManager.Initialize, _inputManager); + _loadscr.LoadingStep(_console.Initialize, _console); // Make sure this is done before we try to load prototypes, // avoid any possibility of race conditions causing the check to not finish // before prototype load. - ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions); + _loadscr.LoadingStep( + () => ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions), + "Check bad file extensions"); - _reload.Initialize(); - _reflectionManager.Initialize(); + _loadscr.LoadingStep(_reload.Initialize, _reload); + _loadscr.LoadingStep(_reflectionManager.Initialize, _reflectionManager); + _loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager); + _loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager); + _loadscr.BeginLoadingSection(_prototypeManager); _prototypeManager.Initialize(); _prototypeManager.LoadDefaultPrototypes(); - _xamlProxyManager.Initialize(); - _xamlHotReloadManager.Initialize(); - _userInterfaceManager.Initialize(); - _eyeManager.Initialize(); - _entityManager.Initialize(); - _mapManager.Initialize(); - _gameStateManager.Initialize(); - _placementManager.Initialize(); - _viewVariablesManager.Initialize(); - _scriptClient.Initialize(); - _client.Initialize(); - _discord.Initialize(); - _tagManager.Initialize(); - _protoLoadMan.Initialize(); - _netResMan.Initialize(); - _replayLoader.Initialize(); - _replayPlayback.Initialize(); - _replayRecording.Initialize(); - _userInterfaceManager.PostInitialize(); - _modLoader.BroadcastRunLevel(ModRunLevel.PostInit); + _loadscr.EndLoadingSection(); + _loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init"); + _loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager); + _loadscr.LoadingStep(_entityManager.Initialize, _entityManager); + _loadscr.LoadingStep(_mapManager.Initialize, _mapManager); + _loadscr.LoadingStep(_gameStateManager.Initialize, _gameStateManager); + _loadscr.LoadingStep(_placementManager.Initialize, _placementManager); + _loadscr.LoadingStep(_viewVariablesManager.Initialize, _viewVariablesManager); + _loadscr.LoadingStep(_scriptClient.Initialize, _scriptClient); + _loadscr.LoadingStep(_client.Initialize, _client); + _loadscr.LoadingStep(_discord.Initialize, _discord); + _loadscr.LoadingStep(_tagManager.Initialize, _tagManager); + _loadscr.LoadingStep(_protoLoadMan.Initialize, _protoLoadMan); + _loadscr.LoadingStep(_netResMan.Initialize, _netResMan); + _loadscr.LoadingStep(_replayLoader.Initialize, _replayLoader); + _loadscr.LoadingStep(_replayPlayback.Initialize, _replayPlayback); + _loadscr.LoadingStep(_replayRecording.Initialize, _replayRecording); + _loadscr.LoadingStep(_userInterfaceManager.PostInitialize, "UI postinit"); + + // Init stuff before this if at all possible. + + _loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PostInit), "Content PostInit"); + + _loadscr.Finish(); if (_commandLineArgs?.Username != null) { @@ -359,9 +388,6 @@ namespace Robust.Client var userDataDir = GetUserDataDir(); _configurationManager.Initialize(false); - - // MUST load cvars before loading from config file so the cfg manager is aware of secure cvars. - // So SECURE CVars are blacklisted from config. _configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client _configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared @@ -434,7 +460,8 @@ namespace Robust.Client _configurationManager.OverrideConVars(new[] { (CVars.DisplayWindowIconSet.Name, WindowIconSet()), - (CVars.DisplaySplashLogo.Name, SplashLogo()) + (CVars.DisplaySplashLogo.Name, SplashLogo()), + (CVars.LoadingShowBar.Name, ShowLoadingBar().ToString()), }); } @@ -499,10 +526,18 @@ namespace Robust.Client public void Shutdown(string? reason = null) { - DebugTools.AssertNotNull(_mainLoop); + if (_mainLoop == null) + { + if (!_dontStart) + { + _logger.Info($"Shutdown called before client init completed: {reason ?? "No reason provided"}"); + _dontStart = true; + } + return; + } // Already got shut down I assume, - if (!_mainLoop!.Running) + if (!_mainLoop.Running) { return; } diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index feac45b13..a86e432a9 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -216,35 +216,36 @@ namespace Robust.Client.GameObjects } } - using (histogram?.WithLabels("PredictedQueueDel").NewTimer()) + base.TickUpdate(frameTime, noPredictions, histogram); + } + + internal override void ProcessQueueudDeletions() + { + base.ProcessQueueudDeletions(); + while (_queuedPredictedDeletions.TryDequeue(out var uid)) { - while (_queuedPredictedDeletions.TryDequeue(out var uid)) + if (!MetaQuery.TryGetComponentInternal(uid, out var meta)) + continue; + + if (meta.EntityLifeStage >= EntityLifeStage.Terminating) + continue; + + var xform = TransformQuery.GetComponentInternal(uid); + if (meta.NetEntity.IsClientSide()) { - if (!MetaQuery.TryGetComponentInternal(uid, out var meta)) - continue; - - if (meta.EntityLifeStage >= EntityLifeStage.Terminating) - continue; - - var xform = TransformQuery.GetComponentInternal(uid); - if (meta.NetEntity.IsClientSide()) - { - DeleteEntity(uid, meta, xform); - } - else - { - _xforms.DetachEntity(uid, xform, meta, null); - // base call bypasses IGameTiming.InPrediction check - // This is pretty janky and there should be a way for the client to dirty an entity outside of prediction - // TODO PREDICTION - base.Dirty(uid, xform, meta); - } + DeleteEntity(uid, meta, xform); + } + else + { + _xforms.DetachEntity(uid, xform, meta, null); + // base call bypasses IGameTiming.InPrediction check + // This is pretty janky and there should be a way for the client to dirty an entity outside of prediction + // TODO PREDICTION Is actually needed after the current predicted deletion fix? + base.Dirty(uid, xform, meta); } - - _queuedPredictedDeletionsSet.Clear(); } - base.TickUpdate(frameTime, noPredictions, histogram); + _queuedPredictedDeletionsSet.Clear(); } /// diff --git a/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs b/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs deleted file mode 100644 index 65e1c2a03..000000000 --- a/Robust.Client/GameObjects/Components/Renderable/IRenderableComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Maths; - -namespace Robust.Client.GameObjects -{ - [Obsolete] - public partial interface IRenderableComponent : IComponent - { - int DrawDepth { get; set; } - float Bottom { get; } - Box2 LocalAABB { get; } - Box2 AverageAABB { get; } - MapId MapID { get; } - } -} diff --git a/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs b/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs index 9f8d0dae6..b4331a138 100644 --- a/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs +++ b/Robust.Client/GameObjects/Components/Renderable/ISpriteLayer.cs @@ -5,6 +5,7 @@ using Robust.Shared.Maths; namespace Robust.Client.GameObjects { + [NotContentImplementable] public interface ISpriteLayer { SpriteComponent.DirectionOffset DirOffset { get; set; } diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs index 1134fb33e..9ed7511b4 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs @@ -17,7 +17,7 @@ public sealed partial class SpriteSystem if (!_query.Resolve(sprite.Owner, ref sprite.Comp)) return false; - return index > 0 && index < sprite.Comp.Layers.Count; + return index >= 0 && index < sprite.Comp.Layers.Count; } public bool TryGetLayer( diff --git a/Robust.Client/GameObjects/IClientEntityManager.cs b/Robust.Client/GameObjects/IClientEntityManager.cs index 82051a89d..c220ffdca 100644 --- a/Robust.Client/GameObjects/IClientEntityManager.cs +++ b/Robust.Client/GameObjects/IClientEntityManager.cs @@ -2,6 +2,7 @@ using Robust.Shared.GameObjects; namespace Robust.Client.GameObjects { + [NotContentImplementable] public interface IClientEntityManager : IEntityManager, IEntityNetworkManager { /// diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index f53c02b06..b5046816b 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -361,6 +361,9 @@ namespace Robust.Client.GameStates // avoid exception spam from repeatedly trying to reset the same entity. _entitySystemManager.GetEntitySystem().Reset(); _runtimeLog.LogException(e, "ResetPredictedEntities"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } // If we were waiting for a new state, we are now applying it. @@ -437,7 +440,7 @@ namespace Robust.Client.GameStates // If we are about to process an another tick in the same frame, lets not bother unnecessarily running prediction ticks // Really the main-loop ticking just needs to be more specialized for clients. - if (_timing.TickRemainder >= _timing.CalcAdjustedTickPeriod()) + if (_timing.TickRemainderRealtime >= _timing.CalcAdjustedTickPeriod()) return; if (!processedAny) @@ -464,7 +467,8 @@ namespace Robust.Client.GameStates DebugTools.Assert(_timing.InSimulation); var ping = (_network.ServerChannel?.Ping ?? 0) / 1000f + PredictLagBias; // seconds. - var predictionTarget = _timing.LastProcessedTick + (uint) (_processor.TargetBufferSize + Math.Ceiling(_timing.TickRate * ping) + PredictTickBias); + var lagTickCount = Math.Ceiling(_timing.TickRate * ping / _timing.TimeScale); + var predictionTarget = _timing.LastProcessedTick + (uint) (_processor.TargetBufferSize + lagTickCount + PredictTickBias); if (IsPredictionEnabled) { @@ -541,6 +545,11 @@ namespace Robust.Client.GameStates { ((IBroadcastEventBusInternal)_entities.EventBus).ProcessEventQueue(); } + + using (_prof.Group("QueueDel")) + { + _entities.ProcessQueueudDeletions(); + } } _prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(_timing.CurTick.Value)); @@ -949,6 +958,9 @@ namespace Robust.Client.GameStates { _sawmill.Error($"Caught exception while deleting entities"); _runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(ApplyEntityStates)}"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } } diff --git a/Robust.Client/GameStates/IClientGameStateManager.cs b/Robust.Client/GameStates/IClientGameStateManager.cs index 0aba55ed9..533df2494 100644 --- a/Robust.Client/GameStates/IClientGameStateManager.cs +++ b/Robust.Client/GameStates/IClientGameStateManager.cs @@ -12,6 +12,7 @@ namespace Robust.Client.GameStates /// /// Engine service that provides processing and management of game states. /// + [NotContentImplementable] public interface IClientGameStateManager { /// diff --git a/Robust.Client/Graphics/ClientEye/IEyeManager.cs b/Robust.Client/Graphics/ClientEye/IEyeManager.cs index 8ae4d1f6c..3dd75e6ba 100644 --- a/Robust.Client/Graphics/ClientEye/IEyeManager.cs +++ b/Robust.Client/Graphics/ClientEye/IEyeManager.cs @@ -11,6 +11,7 @@ namespace Robust.Client.Graphics /// Keeps a reference to the current eye (camera) that the client is seeing though, and provides /// utility functions for the current eye. /// + [NotContentImplementable] public interface IEyeManager { /// diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index f65230431..65f55ce4f 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -76,9 +76,9 @@ namespace Robust.Client.Graphics.Clyde } // Short path to render only the splash. - if (_drawingSplash) + if (_drawingLoadingScreen) { - DrawSplash(_renderHandle); + DrawLoadingScreen(_renderHandle); FlushRenderQueue(); SwapAllBuffers(); return; @@ -430,18 +430,11 @@ namespace Robust.Client.Graphics.Clyde FlushRenderQueue(); } - private void DrawSplash(IRenderHandle handle) + private void DrawLoadingScreen(IRenderHandle handle) { - // Clear screen to black for splash. ClearFramebuffer(Color.Black); - var splashTex = _cfg.GetCVar(CVars.DisplaySplashLogo); - if (string.IsNullOrEmpty(splashTex)) - return; - - var texture = _resourceCache.GetResource(splashTex).Texture; - - handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2); + _loadingScreenManager.DrawLoadingScreen(handle, ScreenSize); } private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearColor=default) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs index a00ecd14e..fb86fb579 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -161,47 +161,33 @@ internal partial class Clyde } /// - /// This is effectively a specialized combination of a and . + /// This is effectively a specialized combination of a . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe Box2 TransformCenteredBox(in Box2 box, float angle, in Vector2 offset, in Vector2 scale) + internal static unsafe Box2 TransformCenteredBox(in Box2 box, float angle, in Vector2 offset, in Vector2 scale) { - // This function is for sprites, which flip the y axis, so here we flip the definition of t and b relative to the normal function. - DebugTools.Assert(scale.Y < 0); - var boxVec = Unsafe.As>(ref Unsafe.AsRef(in box)); - var sin = Vector128.Create(MathF.Sin(angle)); var cos = Vector128.Create(MathF.Cos(angle)); - var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2)); - var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1)); - var modX = allX * cos - allY * sin; - var modY = allX * sin + allY * cos; + var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2)); + var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1)); + + var x = boxX * cos - boxY * sin; + var y = boxX * sin + boxY * cos; + var lbrt = SimdHelpers.GetAABB(x, y); + + // This function is for sprites, which flip the y-axis via the scale, so we need to flip t & b. + DebugTools.Assert(scale.Y < 0); + lbrt = Vector128.Shuffle(lbrt, Vector128.Create(0,3,2,1)); var offsetVec = Unsafe.As>(ref Unsafe.AsRef(in offset)); // upper undefined var scaleVec = Unsafe.As>(ref Unsafe.AsRef(in scale)); // upper undefined offsetVec = Vector128.Shuffle(offsetVec, Vector128.Create(0, 1, 0, 1)); scaleVec = Vector128.Shuffle(scaleVec, Vector128.Create(0, 1, 0, 1)); - Vector128 lbrt; - if (Sse.IsSupported) - { - var lrlr = SimdHelpers.MinMaxHorizontalSse(modX); - var btbt = SimdHelpers.MaxMinHorizontalSse(modY); - lbrt = Sse.UnpackLow(lrlr, btbt); - } - else - { - var l = SimdHelpers.MinHorizontal128(allX); - var b = SimdHelpers.MaxHorizontal128(allY); - var r = SimdHelpers.MaxHorizontal128(allX); - var t = SimdHelpers.MinHorizontal128(allY); - lbrt = SimdHelpers.MergeRows128(l, b, r, t); - } - // offset and scale box. + // note that the scaling here is scaling the whole space, not jut the box. I.e., the centre of the box is changing lbrt = (lbrt + offsetVec) * scaleVec; - return Unsafe.As, Box2>(ref lbrt); } diff --git a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs index a3fcbf20e..40faf8219 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs @@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde private long _nextViewportId = 1; - private readonly ConcurrentQueue _viewportDisposeQueue = new(); + private readonly ConcurrentQueue<(string? name, ViewportDisposeData data)> _viewportDisposeQueue = new(); private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null) { @@ -66,13 +66,14 @@ namespace Robust.Client.Graphics.Clyde { while (_viewportDisposeQueue.TryDequeue(out var data)) { - DisposeViewport(data); + DisposeViewport(data.data, data.name, wasLeaked: true); } } - private void DisposeViewport(ViewportDisposeData disposeData) + private void DisposeViewport(ViewportDisposeData disposeData, string? name = null, bool wasLeaked = false) { - _clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked"); + if (wasLeaked) + _clydeSawmill.Warning($"Viewport {disposeData.Id} ({name ?? "null"}) got leaked"); _viewports.Remove(disposeData.Handle); if (disposeData.ClearEvent is not { } clearEvent) @@ -211,7 +212,7 @@ namespace Robust.Client.Graphics.Clyde ~Viewport() { - _clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false)); + _clyde._viewportDisposeQueue.Enqueue((Name, DisposeData(referenceSelf: false))); } public void Dispose() @@ -224,7 +225,7 @@ namespace Robust.Client.Graphics.Clyde WallBleedIntermediateRenderTarget1.Dispose(); WallBleedIntermediateRenderTarget2.Dispose(); - _clyde.DisposeViewport(DisposeData(referenceSelf: false)); + _clyde.DisposeViewport(DisposeData(referenceSelf: false), Name); } private ViewportDisposeData DisposeData(bool referenceSelf) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Windowing.cs b/Robust.Client/Graphics/Clyde/Clyde.Windowing.cs index 3c6b78171..fd7db7d4e 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Windowing.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Windowing.cs @@ -102,25 +102,18 @@ namespace Robust.Client.Graphics.Clyde _windowingThread = Thread.CurrentThread; - // Default to SDL3 on ARM64. GLFW is not feature complete there (lacking file dialog implementation) - if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - _cfg.SetCVar(CVars.DisplayWindowingApi, "sdl3"); - var windowingApi = _cfg.GetCVar(CVars.DisplayWindowingApi); IWindowingImpl winImpl; switch (windowingApi) { - case "glfw": - winImpl = new GlfwWindowingImpl(this, _deps); - break; case "sdl3": winImpl = new Sdl3WindowingImpl(this, _deps); break; default: _logManager.GetSawmill("clyde.win").Log( - LogLevel.Error, "Unknown windowing API: {name}. Falling back to GLFW.", windowingApi); - goto case "glfw"; + LogLevel.Error, "Unknown windowing API: {name}. Falling back to SDL3.", windowingApi); + goto case "sdl3"; } _windowing = winImpl; diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index 2f16de31e..cd40da5bc 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -52,6 +52,7 @@ namespace Robust.Client.Graphics.Clyde [Dependency] private readonly ClientEntityManager _entityManager = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IReloadManager _reloads = default!; + [Dependency] private readonly LoadingScreenManager _loadingScreenManager = default!; private GLUniformBuffer ProjViewUBO = default!; private GLUniformBuffer UniformConstantsUBO = default!; @@ -68,7 +69,7 @@ namespace Robust.Client.Graphics.Clyde // VAO is per-window and not stored (not necessary!) private GLBuffer WindowVBO = default!; - private bool _drawingSplash = true; + private bool _drawingLoadingScreen = true; private GLShaderProgram? _currentProgram; @@ -213,7 +214,7 @@ namespace Robust.Client.Graphics.Clyde public void Ready() { - _drawingSplash = false; + _drawingLoadingScreen = false; InitLighting(); } diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Cursors.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Cursors.cs deleted file mode 100644 index 75ec1c1f7..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Cursors.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Client.Utility; -using Robust.Shared.Maths; -using Robust.Shared.Utility; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed unsafe partial class GlfwWindowingImpl - { - private readonly Dictionary _winThreadCursors = new(); - private readonly Dictionary _standardCursors = new(); - - public ICursor CursorGetStandard(StandardCursorShape shape) - { - return _standardCursors[shape]; - } - - public ICursor CursorCreate(Image image, Vector2i hotSpot) - { - var cloneImg = new Image(image.Width, image.Height); - image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan()); - - var id = _clyde.AllocRid(); - SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id)); - - return new CursorImpl(this, id, false); - } - - private void WinThreadCursorCreate(CmdCursorCreate cmd) - { - var (img, (hotX, hotY), id) = cmd; - - fixed (Rgba32* pixPtr = img.GetPixelSpan()) - { - var gImg = new GlfwImage(img.Width, img.Height, (byte*) pixPtr); - var ptr = GLFW.CreateCursor(gImg, hotX, hotY); - - _winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr}); - } - - img.Dispose(); - } - - public void CursorSet(WindowReg window, ICursor? cursor) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg) window; - - if (reg.Cursor == cursor) - { - // Nothing has to be done! - return; - } - - if (cursor == null) - { - reg.Cursor = null; - SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, default)); - return; - } - - var impl = (CursorImpl) cursor; - DebugTools.Assert(impl.Owner == this); - - if (impl.Id == default) - { - throw new ObjectDisposedException(nameof(cursor)); - } - - reg.Cursor = impl; - SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, impl.Id)); - } - - private void WinThreadWinCursorSet(CmdWinCursorSet cmd) - { - var window = (Window*) cmd.Window; - Cursor* ptr = null; - if (cmd.Cursor != default) - ptr = _winThreadCursors[cmd.Cursor].Ptr; - -#if DEBUG - if (_win32Experience) - { - // Based on a true story. - Thread.Sleep(15); - } -#endif - - GLFW.SetCursor(window, ptr); - } - - private void WinThreadCursorDestroy(CmdCursorDestroy cmd) - { - var cursorReg = _winThreadCursors[cmd.Cursor]; - - GLFW.DestroyCursor(cursorReg.Ptr); - - _winThreadCursors.Remove(cmd.Cursor); - } - - private void InitCursors() - { - // Gets ran on window thread don't worry about it. - - void AddStandardCursor(StandardCursorShape standardShape, CursorShape shape) - { - var id = _clyde.AllocRid(); - var ptr = GLFW.CreateStandardCursor(shape); - - var impl = new CursorImpl(this, id, true); - - _standardCursors.Add(standardShape, impl); - _winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr}); - } - - AddStandardCursor(StandardCursorShape.Arrow, CursorShape.Arrow); - AddStandardCursor(StandardCursorShape.IBeam, CursorShape.IBeam); - AddStandardCursor(StandardCursorShape.Crosshair, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand); - AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize); - AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize); - AddStandardCursor(StandardCursorShape.Progress, CursorShape.Arrow); - AddStandardCursor(StandardCursorShape.NWSEResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.NESWResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.Move, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.NotAllowed, CursorShape.Arrow); - AddStandardCursor(StandardCursorShape.NWResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.NResize, CursorShape.VResize); - AddStandardCursor(StandardCursorShape.NEResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.EResize, CursorShape.HResize); - AddStandardCursor(StandardCursorShape.SEResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.SResize, CursorShape.VResize); - AddStandardCursor(StandardCursorShape.SWResize, CursorShape.Crosshair); - AddStandardCursor(StandardCursorShape.WResize, CursorShape.HResize); - } - - private sealed class CursorImpl : ICursor - { - private readonly bool _standard; - public GlfwWindowingImpl Owner { get; } - public ClydeHandle Id { get; private set; } - - public CursorImpl(GlfwWindowingImpl clyde, ClydeHandle id, bool standard) - { - _standard = standard; - Owner = clyde; - Id = id; - } - - ~CursorImpl() - { - DisposeImpl(); - } - - private void DisposeImpl() - { - Owner.SendCmd(new CmdCursorDestroy(Id)); - Id = default; - } - - public void Dispose() - { - if (_standard) - { - throw new InvalidOperationException("Can't dispose standard cursor shape."); - } - - GC.SuppressFinalize(this); - DisposeImpl(); - } - } - - public sealed class WinThreadCursorReg - { - public Cursor* Ptr; - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Events.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Events.cs deleted file mode 100644 index 57f04b629..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Events.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System; -using System.Numerics; -using System.Text; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Client.Input; -using Robust.Shared.Map; - -namespace Robust.Client.Graphics.Clyde -{ - partial class Clyde - { - private partial class GlfwWindowingImpl - { - public void ProcessEvents(bool single=false) - { - while (_eventReader.TryRead(out var ev)) - { - try - { - ProcessEvent(ev); - } - catch (Exception e) - { - _sawmill.Error($"Caught exception in windowing event ({ev.GetType()}):\n{e}"); - } - - if (single) - break; - } - } - - // Block waiting on the windowing -> game thread channel. - // I swear to god do not use this unless you know what you are doing. - private void WaitEvents() - { - _eventReader.WaitToReadAsync().AsTask().Wait(); - } - - private void ProcessEvent(EventBase evb) - { - switch (evb) - { - case EventMouseButton mb: - ProcessEventMouseButton(mb); - break; - case EventCursorPos cp: - ProcessEventCursorPos(cp); - break; - case EventCursorEnter ev: - ProcessEventCursorEnter(ev); - break; - case EventScroll s: - ProcessEventScroll(s); - break; - case EventKey k: - ProcessEventKey(k); - break; - case EventChar c: - ProcessEventChar(c); - break; - case EventMonitorSetup ms: - ProcessSetupMonitor(ms); - break; - case EventMonitorDestroy md: - ProcessEventDestroyMonitor(md); - break; - case EventWindowCreate wCreate: - FinishWindowCreate(wCreate); - break; - case EventWindowClose wc: - ProcessEventWindowClose(wc); - break; - case EventWindowFocus wf: - ProcessEventWindowFocus(wf); - break; - case EventWindowSize ws: - ProcessEventWindowSize(ws); - break; - case EventWindowPos wp: - ProcessEventWindowPos(wp); - break; - case EventWindowIconify wi: - ProcessEventWindowIconify(wi); - break; - case EventWindowContentScale cs: - ProcessEventWindowContentScale(cs); - break; - case EventSetFullscreenAck: - ProcessEventSetFullscreenAck(); - break; - default: - _sawmill.Error($"Unknown GLFW event type: {evb.GetType()}"); - break; - } - } - - private void ProcessEventChar(EventChar ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg is not { TextInputActive: true }) - return; - - _clyde.SendText(new TextEnteredEventArgs(new Rune(ev.CodePoint).ToString())); - } - - private void ProcessEventCursorPos(EventCursorPos ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - var newPos = new Vector2((float) ev.XPos, (float) ev.YPos) * windowReg.PixelRatio; - var delta = newPos - windowReg.LastMousePos; - windowReg.LastMousePos = newPos; - - _clyde._currentHoveredWindow = windowReg; - - _clyde.SendMouseMove(new MouseMoveEventArgs(delta, new ScreenCoordinates(newPos, windowReg.Id))); - } - - private void ProcessEventCursorEnter(EventCursorEnter ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - if (ev.Entered) - { - _clyde._currentHoveredWindow = windowReg; - } - else if (_clyde._currentHoveredWindow == windowReg) - { - _clyde._currentHoveredWindow = null; - } - - _clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(windowReg.Handle, ev.Entered)); - } - - private void ProcessEventKey(EventKey ev) - { - EmitKeyEvent(ConvertGlfwKey(ev.Key), ev.Action, ev.Mods, ev.ScanCode); - } - - private void EmitKeyEvent(Keyboard.Key key, InputAction action, KeyModifiers mods, int scanCode) - { - var shift = (mods & KeyModifiers.Shift) != 0; - var alt = (mods & KeyModifiers.Alt) != 0; - var control = (mods & KeyModifiers.Control) != 0; - var system = (mods & KeyModifiers.Super) != 0; - - var ev = new KeyEventArgs( - key, - action == InputAction.Repeat, - alt, control, shift, system, - scanCode); - - switch (action) - { - case InputAction.Release: - _clyde.SendKeyUp(ev); - break; - case InputAction.Press: - case InputAction.Repeat: - _clyde.SendKeyDown(ev); - break; - default: - throw new ArgumentOutOfRangeException(nameof(action), action, null); - } - } - - private void ProcessEventMouseButton(EventMouseButton ev) - { - EmitKeyEvent(Mouse.MouseButtonToKey(ConvertGlfwButton(ev.Button)), ev.Action, ev.Mods, default); - } - - private void ProcessEventScroll(EventScroll ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - var eventArgs = new MouseWheelEventArgs( - new((float) ev.XOffset, (float) ev.YOffset), - new ScreenCoordinates(windowReg.LastMousePos, windowReg.Id)); - _clyde.SendScroll(eventArgs); - } - - private void ProcessEventWindowClose(EventWindowClose ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - _clyde.SendCloseWindow(windowReg, new WindowRequestClosedEventArgs(windowReg.Handle)); - } - - private void ProcessEventWindowSize(EventWindowSize ev) - { - var window = ev.Window; - var width = ev.Width; - var height = ev.Height; - var fbW = ev.FramebufferWidth; - var fbH = ev.FramebufferHeight; - - var windowReg = FindWindow(window); - if (windowReg == null) - return; - - var oldSize = windowReg.FramebufferSize; - windowReg.FramebufferSize = (fbW, fbH); - windowReg.WindowSize = (width, height); - - if (fbW == 0 || fbH == 0 || width == 0 || height == 0) - return; - - windowReg.PixelRatio = windowReg.FramebufferSize / windowReg.WindowSize; - - _clyde.SendWindowResized(windowReg, oldSize); - } - - private void ProcessEventWindowPos(EventWindowPos ev) - { - var window = ev.Window; - var x = ev.X; - var y = ev.Y; - - var windowReg = FindWindow(window); - if (windowReg == null) - return; - - windowReg.WindowPos = (x, y); - } - - private void ProcessEventWindowContentScale(EventWindowContentScale ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - windowReg.WindowScale = new Vector2(ev.XScale, ev.YScale); - _clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle)); - } - - private void ProcessEventWindowIconify(EventWindowIconify ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - windowReg.IsMinimized = ev.Iconified; - } - - private void ProcessEventWindowFocus(EventWindowFocus ev) - { - var windowReg = FindWindow(ev.Window); - if (windowReg == null) - return; - - windowReg.IsFocused = ev.Focused; - _clyde.SendWindowFocus(new WindowFocusedEventArgs(ev.Focused, windowReg.Handle)); - } - - private void ProcessEventSetFullscreenAck() - { - // As far as I can tell, sometimes entering fullscreen just disables vsync. - // Hilarious! - _clyde._glContext?.UpdateVSync(); - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Keys.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Keys.cs deleted file mode 100644 index 884085434..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Keys.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Robust.Shared; -using System.Threading; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Client.Input; -using Robust.Shared.Localization; -using GlfwKey = OpenToolkit.GraphicsLibraryFramework.Keys; -using GlfwButton = OpenToolkit.GraphicsLibraryFramework.MouseButton; -using static Robust.Client.Input.Mouse; -using static Robust.Client.Input.Keyboard; -using Robust.Shared.IoC; -using Robust.Shared.Configuration; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed partial class GlfwWindowingImpl - { - // TODO: to avoid having to ask the windowing thread, key names are cached. - // This means they don't update correctly if the user switches keyboard mode. RIP. - - private readonly Dictionary _printableKeyNameMap = new(); - - private void InitKeyMap() - { - _printableKeyNameMap.Clear(); - // From GLFW's source code: this is the actual list of "printable" keys - // that GetKeyName returns something for. - CacheKey(Keys.KeyPadEqual); - for (var k = Keys.KeyPad0; k <= Keys.KeyPadAdd; k++) - { - CacheKey(k); - } - - for (var k = Keys.Apostrophe; k <= Keys.World2; k++) - { - CacheKey(k); - } - - void CacheKey(GlfwKey key) - { - var rKey = ConvertGlfwKey(key); - if (rKey == Key.Unknown) - return; - - var name = GLFW.GetKeyName(key, 0); - - if (!string.IsNullOrEmpty(name)) - _printableKeyNameMap.Add(rKey, name); - } - } - - public string? KeyGetName(Keyboard.Key key) - { - if (_printableKeyNameMap.TryGetValue(key, out var name)) - return name; - - return null; - } - - public static Button ConvertGlfwButton(GlfwButton button) - { - return MouseButtonMap[button]; - } - - private static readonly FrozenDictionary MouseButtonMap = new Dictionary() - { - {GlfwButton.Left, Button.Left}, - {GlfwButton.Middle, Button.Middle}, - {GlfwButton.Right, Button.Right}, - {GlfwButton.Button4, Button.Button4}, - {GlfwButton.Button5, Button.Button5}, - {GlfwButton.Button6, Button.Button6}, - {GlfwButton.Button7, Button.Button7}, - {GlfwButton.Button8, Button.Button8}, - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary KeyMap; - private static readonly FrozenDictionary KeyMapReverse; - - - internal static Key ConvertGlfwKey(GlfwKey key) - { - if (KeyMap.TryGetValue(key, out var result)) - { - return result; - } - - return Key.Unknown; - } - - internal static GlfwKey ConvertGlfwKeyReverse(Key key) - { - if (KeyMapReverse.TryGetValue(key, out var result)) - { - return result; - } - - return GlfwKey.Unknown; - } - - static GlfwWindowingImpl() - { - KeyMap = new Dictionary - { - {GlfwKey.A, Key.A}, - {GlfwKey.B, Key.B}, - {GlfwKey.C, Key.C}, - {GlfwKey.D, Key.D}, - {GlfwKey.E, Key.E}, - {GlfwKey.F, Key.F}, - {GlfwKey.G, Key.G}, - {GlfwKey.H, Key.H}, - {GlfwKey.I, Key.I}, - {GlfwKey.J, Key.J}, - {GlfwKey.K, Key.K}, - {GlfwKey.L, Key.L}, - {GlfwKey.M, Key.M}, - {GlfwKey.N, Key.N}, - {GlfwKey.O, Key.O}, - {GlfwKey.P, Key.P}, - {GlfwKey.Q, Key.Q}, - {GlfwKey.R, Key.R}, - {GlfwKey.S, Key.S}, - {GlfwKey.T, Key.T}, - {GlfwKey.U, Key.U}, - {GlfwKey.V, Key.V}, - {GlfwKey.W, Key.W}, - {GlfwKey.X, Key.X}, - {GlfwKey.Y, Key.Y}, - {GlfwKey.Z, Key.Z}, - {GlfwKey.D0, Key.Num0}, - {GlfwKey.D1, Key.Num1}, - {GlfwKey.D2, Key.Num2}, - {GlfwKey.D3, Key.Num3}, - {GlfwKey.D4, Key.Num4}, - {GlfwKey.D5, Key.Num5}, - {GlfwKey.D6, Key.Num6}, - {GlfwKey.D7, Key.Num7}, - {GlfwKey.D8, Key.Num8}, - {GlfwKey.D9, Key.Num9}, - {GlfwKey.KeyPad0, Key.NumpadNum0}, - {GlfwKey.KeyPad1, Key.NumpadNum1}, - {GlfwKey.KeyPad2, Key.NumpadNum2}, - {GlfwKey.KeyPad3, Key.NumpadNum3}, - {GlfwKey.KeyPad4, Key.NumpadNum4}, - {GlfwKey.KeyPad5, Key.NumpadNum5}, - {GlfwKey.KeyPad6, Key.NumpadNum6}, - {GlfwKey.KeyPad7, Key.NumpadNum7}, - {GlfwKey.KeyPad8, Key.NumpadNum8}, - {GlfwKey.KeyPad9, Key.NumpadNum9}, - {GlfwKey.Escape, Key.Escape}, - {GlfwKey.LeftControl, Key.Control}, - {GlfwKey.RightControl, Key.Control}, - {GlfwKey.RightShift, Key.Shift}, - {GlfwKey.LeftShift, Key.Shift}, - {GlfwKey.LeftAlt, Key.Alt}, - {GlfwKey.RightAlt, Key.Alt}, - {GlfwKey.LeftSuper, Key.LSystem}, - {GlfwKey.RightSuper, Key.RSystem}, - {GlfwKey.Menu, Key.Menu}, - {GlfwKey.LeftBracket, Key.LBracket}, - {GlfwKey.RightBracket, Key.RBracket}, - {GlfwKey.Semicolon, Key.SemiColon}, - {GlfwKey.Comma, Key.Comma}, - {GlfwKey.Period, Key.Period}, - {GlfwKey.Apostrophe, Key.Apostrophe}, - {GlfwKey.Slash, Key.Slash}, - {GlfwKey.Backslash, Key.BackSlash}, - {GlfwKey.GraveAccent, Key.Tilde}, - {GlfwKey.Equal, Key.Equal}, - {GlfwKey.Space, Key.Space}, - {GlfwKey.Enter, Key.Return}, - {GlfwKey.KeyPadEnter, Key.NumpadEnter}, - {GlfwKey.Backspace, Key.BackSpace}, - {GlfwKey.Tab, Key.Tab}, - {GlfwKey.PageUp, Key.PageUp}, - {GlfwKey.PageDown, Key.PageDown}, - {GlfwKey.End, Key.End}, - {GlfwKey.Home, Key.Home}, - {GlfwKey.Insert, Key.Insert}, - {GlfwKey.Delete, Key.Delete}, - {GlfwKey.Minus, Key.Minus}, - {GlfwKey.KeyPadAdd, Key.NumpadAdd}, - {GlfwKey.KeyPadSubtract, Key.NumpadSubtract}, - {GlfwKey.KeyPadDivide, Key.NumpadDivide}, - {GlfwKey.KeyPadMultiply, Key.NumpadMultiply}, - {GlfwKey.KeyPadDecimal, Key.NumpadDecimal}, - {GlfwKey.Left, Key.Left}, - {GlfwKey.Right, Key.Right}, - {GlfwKey.Up, Key.Up}, - {GlfwKey.Down, Key.Down}, - {GlfwKey.F1, Key.F1}, - {GlfwKey.F2, Key.F2}, - {GlfwKey.F3, Key.F3}, - {GlfwKey.F4, Key.F4}, - {GlfwKey.F5, Key.F5}, - {GlfwKey.F6, Key.F6}, - {GlfwKey.F7, Key.F7}, - {GlfwKey.F8, Key.F8}, - {GlfwKey.F9, Key.F9}, - {GlfwKey.F10, Key.F10}, - {GlfwKey.F11, Key.F11}, - {GlfwKey.F12, Key.F12}, - {GlfwKey.F13, Key.F13}, - {GlfwKey.F14, Key.F14}, - {GlfwKey.F15, Key.F15}, - {GlfwKey.F16, Key.F16}, - {GlfwKey.F17, Key.F17}, - {GlfwKey.F18, Key.F18}, - {GlfwKey.F19, Key.F19}, - {GlfwKey.F20, Key.F20}, - {GlfwKey.F21, Key.F21}, - {GlfwKey.F22, Key.F22}, - {GlfwKey.F23, Key.F23}, - {GlfwKey.F24, Key.F24}, - {GlfwKey.Pause, Key.Pause}, - {GlfwKey.World1, Key.World1}, - }.ToFrozenDictionary(); - - var keyMapReverse = new Dictionary(); - - foreach (var (key, value) in KeyMap) - { - keyMapReverse[value] = key; - } - - KeyMapReverse = keyMapReverse.ToFrozenDictionary(); - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Monitors.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Monitors.cs deleted file mode 100644 index abd3e3d91..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Monitors.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Shared.Utility; -using GlfwVideoMode = OpenToolkit.GraphicsLibraryFramework.VideoMode; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed unsafe partial class GlfwWindowingImpl - { - // TODO: GLFW doesn't have any events for complex monitor config changes, - // so we need some way to reload stuff if e.g. the primary monitor changes. - // Still better than SDL2 though which doesn't acknowledge monitor changes at all. - - // Monitors are created at GLFW's will, - // so we need to make SURE monitors keep existing while operating on them. - // because, you know, async. Don't want a use-after-free. - private readonly Dictionary _winThreadMonitors = new(); - - // Can't use ClydeHandle because it's 64 bit. - private int _nextMonitorId = 1; - private readonly Dictionary _monitors = new(); - - private void InitMonitors() - { - var monitors = GLFW.GetMonitorsRaw(out var count); - - for (var i = 0; i < count; i++) - { - WinThreadSetupMonitor(monitors[i]); - } - - var primaryMonitor = GLFW.GetPrimaryMonitor(); - var up = GLFW.GetMonitorUserPointer(primaryMonitor); - _clyde._primaryMonitorId = (int) up; - - ProcessEvents(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void WinThreadSetupMonitor(Monitor* monitor) - { - var id = _nextMonitorId++; - - DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, - "GLFW window already has user pointer??"); - - var name = GLFW.GetMonitorName(monitor); - var videoMode = GLFW.GetVideoMode(monitor); - var modesPtr = GLFW.GetVideoModesRaw(monitor, out var modeCount); - var modes = new VideoMode[modeCount]; - for (var i = 0; i < modes.Length; i++) - { - modes[i] = ConvertVideoMode(modesPtr[i]); - } - - GLFW.SetMonitorUserPointer(monitor, (void*) id); - - _winThreadMonitors.Add(id, new WinThreadMonitorReg {Ptr = monitor}); - - SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(*videoMode), modes)); - } - - private static VideoMode ConvertVideoMode(in GlfwVideoMode mode) - { - return new() - { - Width = (ushort) mode.Width, - Height = (ushort) mode.Height, - RedBits = (byte) mode.RedBits, - RefreshRate = (ushort) mode.RefreshRate, - GreenBits = (byte) mode.GreenBits, - BlueBits = (byte) mode.BlueBits, - }; - } - - 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 GlfwMonitorReg - { - Id = ev.Id, - Handle = impl - }; - } - - private void WinThreadDestroyMonitor(Monitor* monitor) - { - var ptr = (int) GLFW.GetMonitorUserPointer(monitor); - - if (ptr == 0) - { - var name = GLFW.GetMonitorName(monitor); - _sawmill.Warning($"Monitor '{name}' had no user pointer set??"); - return; - } - - _winThreadMonitors.Remove(ptr); - - GLFW.SetMonitorUserPointer(monitor, null); - - SendEvent(new EventMonitorDestroy(ptr)); - } - - private void ProcessEventDestroyMonitor(EventMonitorDestroy ev) - { - _monitors.Remove(ev.Id); - _clyde._monitorHandles.Remove(ev.Id); - } - - private sealed class GlfwMonitorReg : MonitorReg - { - public int Id; - } - - private sealed class WinThreadMonitorReg - { - public Monitor* Ptr; - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.RawEvents.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.RawEvents.cs deleted file mode 100644 index c05569904..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.RawEvents.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Threading.Tasks; -using OpenToolkit.GraphicsLibraryFramework; - -namespace Robust.Client.Graphics.Clyde -{ - partial class Clyde - { - private unsafe partial class GlfwWindowingImpl - { - // Keep delegates around to prevent GC issues. - private GLFWCallbacks.ErrorCallback? _errorCallback; - private GLFWCallbacks.MonitorCallback? _monitorCallback; - private GLFWCallbacks.CharCallback? _charCallback; - private GLFWCallbacks.CursorPosCallback? _cursorPosCallback; - private GLFWCallbacks.CursorEnterCallback? _cursorEnterCallback; - private GLFWCallbacks.KeyCallback? _keyCallback; - private GLFWCallbacks.MouseButtonCallback? _mouseButtonCallback; - private GLFWCallbacks.ScrollCallback? _scrollCallback; - private GLFWCallbacks.WindowCloseCallback? _windowCloseCallback; - private GLFWCallbacks.WindowPosCallback? _windowPosCallback; - private GLFWCallbacks.WindowSizeCallback? _windowSizeCallback; - private GLFWCallbacks.WindowContentScaleCallback? _windowContentScaleCallback; - private GLFWCallbacks.WindowIconifyCallback? _windowIconifyCallback; - private GLFWCallbacks.WindowFocusCallback? _windowFocusCallback; - - private void StoreCallbacks() - { - _errorCallback = OnGlfwError; - _monitorCallback = OnGlfwMonitor; - _charCallback = OnGlfwChar; - _cursorPosCallback = OnGlfwCursorPos; - _cursorEnterCallback = OnGlfwCursorEnter; - _keyCallback = OnGlfwKey; - _mouseButtonCallback = OnGlfwMouseButton; - _scrollCallback = OnGlfwScroll; - _windowCloseCallback = OnGlfwWindowClose; - _windowSizeCallback = OnGlfwWindowSize; - _windowPosCallback = OnGlfwWindowPos; - _windowContentScaleCallback = OnGlfwWindowContentScale; - _windowIconifyCallback = OnGlfwWindowIconify; - _windowFocusCallback = OnGlfwWindowFocus; - } - - private void SetupGlobalCallbacks() - { - GLFW.SetMonitorCallback(_monitorCallback); - } - - private void OnGlfwMonitor(Monitor* monitor, ConnectedState state) - { - if (state == ConnectedState.Connected) - WinThreadSetupMonitor(monitor); - else - WinThreadDestroyMonitor(monitor); - } - - private void OnGlfwChar(Window* window, uint codepoint) - { - SendEvent(new EventChar((nint) window, codepoint)); - } - - private void OnGlfwCursorPos(Window* window, double x, double y) - { - // System.Console.WriteLine($"{(nint)window:X16}: {x},{y}"); - SendEvent(new EventCursorPos((nint) window, x, y)); - } - - private void OnGlfwCursorEnter(Window* window, bool entered) - { - // System.Console.WriteLine($"{(nint)window:X16}: {entered}"); - SendEvent(new EventCursorEnter((nint) window, entered)); - } - - private void OnGlfwKey(Window* window, Keys key, int scanCode, InputAction action, KeyModifiers mods) - { - SendEvent(new EventKey((nint) window, key, scanCode, action, mods)); - } - - private void OnGlfwMouseButton(Window* window, MouseButton button, InputAction action, KeyModifiers mods) - { - SendEvent(new EventMouseButton((nint) window, button, action, mods)); - } - - private void OnGlfwScroll(Window* window, double offsetX, double offsetY) - { - SendEvent(new EventScroll((nint) window, offsetX, offsetY)); - } - - private void OnGlfwWindowClose(Window* window) - { - SendEvent(new EventWindowClose((nint) window)); - } - - private void OnGlfwWindowSize(Window* window, int width, int height) - { - GLFW.GetFramebufferSize(window, out var fbW, out var fbH); - SendEvent(new EventWindowSize((nint) window, width, height, fbW, fbH)); - } - - private void OnGlfwWindowPos(Window* window, int x, int y) - { - SendEvent(new EventWindowPos((nint) window, x, y)); - } - - private void OnGlfwWindowContentScale(Window* window, float xScale, float yScale) - { - SendEvent(new EventWindowContentScale((nint) window, xScale, yScale)); - } - - private void OnGlfwWindowIconify(Window* window, bool iconified) - { - SendEvent(new EventWindowIconify((nint) window, iconified)); - } - - private void OnGlfwWindowFocus(Window* window, bool focused) - { - SendEvent(new EventWindowFocus((nint) window, focused)); - } - - // NOTE: events do not correspond 1:1 to GLFW events - // This is because they need to pack all the data required - // for the game-thread event handling. - - private abstract record EventBase; - - private record EventMouseButton( - nint Window, - MouseButton Button, - InputAction Action, - KeyModifiers Mods - ) : EventBase; - - private record EventCursorPos( - nint Window, - double XPos, - double YPos - ) : EventBase; - - private record EventCursorEnter( - nint Window, - bool Entered - ) : EventBase; - - private record EventScroll( - nint Window, - double XOffset, - double YOffset - ) : EventBase; - - private record EventKey( - nint Window, - Keys Key, - int ScanCode, - InputAction Action, - KeyModifiers Mods - ) : EventBase; - - private record EventChar - ( - nint Window, - uint CodePoint - ) : EventBase; - - private record EventWindowClose - ( - nint Window - ) : EventBase; - - private record EventWindowCreate( - GlfwWindowCreateResult Result, - TaskCompletionSource Tcs - ) : EventBase; - - private record EventWindowSize - ( - nint Window, - int Width, - int Height, - int FramebufferWidth, - int FramebufferHeight - ) : EventBase; - - private record EventWindowPos - ( - nint Window, - int X, - int Y - ) : EventBase; - - private record EventWindowContentScale - ( - nint Window, - float XScale, - float YScale - ) : EventBase; - - private record EventWindowIconify - ( - nint Window, - bool Iconified - ) : EventBase; - - private record EventWindowFocus - ( - nint Window, - bool Focused - ) : EventBase; - - private record EventMonitorSetup - ( - int Id, - string Name, - VideoMode CurrentMode, - VideoMode[] AllModes - ) : EventBase; - - private record EventMonitorDestroy - ( - int Id - ) : EventBase; - - private sealed record EventSetFullscreenAck : EventBase; - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs deleted file mode 100644 index 551206c5f..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Threading.Channels; -using System.Threading.Tasks; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Shared; -using Robust.Shared.Maths; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed partial class GlfwWindowingImpl - { - private bool _windowingRunning; - private ChannelWriter _cmdWriter = default!; - private ChannelReader _cmdReader = default!; - - private ChannelReader _eventReader = default!; - private ChannelWriter _eventWriter = default!; - - // - // Let it be forever recorded that I started work on windowing thread separation - // because win32 SetCursor was taking 15ms spinwaiting inside the kernel. - // - - // - // To avoid stutters and solve some other problems like smooth window resizing, - // we (by default) use a separate thread for windowing. - // - // Types like WindowReg are considered to be part of the "game" thread - // and should **NOT** be directly updated/accessed from the windowing thread. - // - // Got that? - // - - // - // The windowing -> game channel is bounded so that the OS properly detects the game as locked - // up when it actually locks up. The other way around is not bounded to avoid deadlocks. - // This also means that all operations like clipboard reading, window creation, etc.... - // have to be asynchronous. - // - - public void EnterWindowLoop() - { - _windowingRunning = true; - - while (_windowingRunning) - { - // glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread - // (despite what the docs claim, and yes this makes it useless). - // Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead. - if (OperatingSystem.IsMacOS()) - GLFW.WaitEventsTimeout(0.008); - else - GLFW.WaitEvents(); - - while (_cmdReader.TryRead(out var cmd) && _windowingRunning) - { - ProcessGlfwCmd(cmd); - } - } - } - - public void PollEvents() - { - GLFW.PollEvents(); - } - - private void ProcessGlfwCmd(CmdBase cmdb) - { - switch (cmdb) - { - case CmdTerminate: - _windowingRunning = false; - _eventWriter.Complete(); - break; - - case CmdWinSetTitle cmd: - WinThreadWinSetTitle(cmd); - break; - - case CmdWinSetMonitor cmd: - WinThreadWinSetMonitor(cmd); - break; - - case CmdWinSetSize cmd: - WinThreadWinSetSize(cmd); - break; - - case CmdWinSetVisible cmd: - WinThreadWinSetVisible(cmd); - break; - - case CmdWinRequestAttention cmd: - WinThreadWinRequestAttention(cmd); - break; - - case CmdWinSetFullscreen cmd: - WinThreadWinSetFullscreen(cmd); - break; - - case CmdWinCreate cmd: - WinThreadWinCreate(cmd); - break; - - case CmdWinDestroy cmd: - WinThreadWinDestroy(cmd); - break; - - case CmdSetClipboard cmd: - WinThreadSetClipboard(cmd); - break; - - case CmdGetClipboard cmd: - WinThreadGetClipboard(cmd); - break; - - case CmdCursorCreate cmd: - WinThreadCursorCreate(cmd); - break; - - case CmdCursorDestroy cmd: - WinThreadCursorDestroy(cmd); - break; - - case CmdWinCursorSet cmd: - WinThreadWinCursorSet(cmd); - break; - - case CmdRunAction cmd: - cmd.Action(); - break; - } - } - - public void TerminateWindowLoop() - { - SendCmd(new CmdTerminate()); - _cmdWriter.Complete(); - - // Drain command queue ignoring it until the window thread confirms completion. -#pragma warning disable RA0004 - while (_eventReader.WaitToReadAsync().AsTask().Result) -#pragma warning restore RA0004 - { - _eventReader.TryRead(out _); - } - } - - private void InitChannels() - { - var cmdChannel = Channel.CreateUnbounded(new UnboundedChannelOptions - { - SingleReader = true, - // Finalizers can write to this in some cases. - SingleWriter = false - }); - - _cmdReader = cmdChannel.Reader; - _cmdWriter = cmdChannel.Writer; - - var bufferSize = _cfg.GetCVar(CVars.DisplayInputBufferSize); - var eventChannel = Channel.CreateBounded(new BoundedChannelOptions(bufferSize) - { - FullMode = BoundedChannelFullMode.Wait, - SingleReader = true, - SingleWriter = true, - // For unblocking continuations. - AllowSynchronousContinuations = true - }); - - _eventReader = eventChannel.Reader; - _eventWriter = eventChannel.Writer; - } - - private void SendCmd(CmdBase cmd) - { - if (_clyde._threadWindowApi) - { - _cmdWriter.TryWrite(cmd); - - // Post empty event to unstuck WaitEvents if necessary. - if (!OperatingSystem.IsMacOS()) - GLFW.PostEmptyEvent(); - } - else - { - ProcessGlfwCmd(cmd); - } - } - - private void SendEvent(EventBase ev) - { - if (_clyde._threadWindowApi) - { - var task = _eventWriter.WriteAsync(ev); - - if (!task.IsCompletedSuccessfully) - { - task.AsTask().Wait(); - } - } - else - { - ProcessEvent(ev); - } - } - - public void RunOnWindowThread(Action action) - { - SendCmd(new CmdRunAction(action)); - } - - private abstract record CmdBase; - - private sealed record CmdTerminate : CmdBase; - - private sealed record CmdWinSetTitle( - nint Window, - string Title - ) : CmdBase; - - private sealed record CmdWinSetMonitor( - nint Window, - int MonitorId, - int X, int Y, - int W, int H, - int RefreshRate - ) : CmdBase; - - private sealed record CmdWinMaximize( - nint Window - ) : CmdBase; - - private sealed record CmdWinSetFullscreen( - nint Window - ) : CmdBase; - - private sealed record CmdWinSetSize( - nint Window, - int W, int H - ) : CmdBase; - - private sealed record CmdWinSetVisible( - nint Window, - bool Visible - ) : CmdBase; - - private sealed record CmdWinRequestAttention( - nint Window - ) : CmdBase; - - private sealed record CmdWinCreate( - GLContextSpec? GLSpec, - WindowCreateParameters Parameters, - nint ShareWindow, - nint OwnerWindow, - TaskCompletionSource Tcs - ) : CmdBase; - - private sealed record CmdWinDestroy( - nint Window, - bool hadOwner - ) : CmdBase; - - private sealed record GlfwWindowCreateResult( - GlfwWindowReg? Reg, - (string Desc, ErrorCode Code)? Error - ); - - private sealed record CmdSetClipboard( - nint Window, - string Text - ) : CmdBase; - - private sealed record CmdGetClipboard( - nint Window, - TaskCompletionSource Tcs - ) : CmdBase; - - private sealed record CmdWinCursorSet( - nint Window, - ClydeHandle Cursor - ) : CmdBase; - - private sealed record CmdCursorCreate( - Image Bytes, - Vector2i Hotspot, - ClydeHandle Cursor - ) : CmdBase; - - private sealed record CmdCursorDestroy( - ClydeHandle Cursor - ) : CmdBase; - - private sealed record CmdRunAction( - Action Action - ) : CmdBase; - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs deleted file mode 100644 index c09f269ea..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs +++ /dev/null @@ -1,759 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Client.Utility; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Utility; -using SixLabors.ImageSharp.PixelFormats; -using TerraFX.Interop.Windows; -using TerraFX.Interop.Xlib; -using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image; -using Monitor = OpenToolkit.GraphicsLibraryFramework.Monitor; -using Window = OpenToolkit.GraphicsLibraryFramework.Window; -using X11Window = TerraFX.Interop.Xlib.Window; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed unsafe partial class GlfwWindowingImpl - { - private int _nextWindowId = 1; - - public void WindowSetTitle(WindowReg window, string title) - { - CheckWindowDisposed(window); - - if (title == null) - { - throw new ArgumentNullException(nameof(title)); - } - - var reg = (GlfwWindowReg) window; - - SendCmd(new CmdWinSetTitle((nint) reg.GlfwWindow, title)); - } - - private void WinThreadWinSetTitle(CmdWinSetTitle cmd) - { - GLFW.SetWindowTitle((Window*) cmd.Window, cmd.Title); - } - - public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor) - { - CheckWindowDisposed(window); - - var winReg = (GlfwWindowReg) window; - - var monitorImpl = (MonitorHandle) monitor; - - SendCmd(new CmdWinSetMonitor( - (nint) winReg.GlfwWindow, - monitorImpl.Id, - 0, 0, - monitorImpl.Size.X, monitorImpl.Size.Y, - monitorImpl.RefreshRate)); - } - - private void WinThreadWinSetMonitor(CmdWinSetMonitor cmd) - { - Monitor* monitorPtr; - if (cmd.MonitorId == 0) - { - monitorPtr = null; - } - else if (_winThreadMonitors.TryGetValue(cmd.MonitorId, out var monitorReg)) - { - monitorPtr = monitorReg.Ptr; - } - else - { - return; - } - - GLFW.SetWindowMonitor( - (Window*) cmd.Window, - monitorPtr, - cmd.X, cmd.Y, - cmd.W, cmd.H, - cmd.RefreshRate - ); - } - - public void WindowSetSize(WindowReg window, Vector2i size) - { - var reg = (GlfwWindowReg) window; - - SendCmd(new CmdWinSetSize((nint) reg.GlfwWindow, size.X, size.Y)); - } - - public void WindowSetVisible(WindowReg window, bool visible) - { - var reg = (GlfwWindowReg) window; - reg.IsVisible = visible; - - SendCmd(new CmdWinSetVisible((nint) reg.GlfwWindow, visible)); - } - - private void WinThreadWinSetSize(CmdWinSetSize cmd) - { - var win = (Window*) cmd.Window; - - GLFW.SetWindowSize(win, cmd.W, cmd.H); - } - - private void WinThreadWinSetVisible(CmdWinSetVisible cmd) - { - var win = (Window*) cmd.Window; - - if (cmd.Visible) - { - GLFW.ShowWindow(win); - } - else - { - GLFW.HideWindow(win); - } - } - - public void WindowRequestAttention(WindowReg window) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg) window; - - SendCmd(new CmdWinRequestAttention((nint) reg.GlfwWindow)); - } - - private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd) - { - var win = (Window*) cmd.Window; - - GLFW.RequestWindowAttention(win); - } - - public void WindowSwapBuffers(WindowReg window) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg) window; - - GLFW.SwapBuffers(reg.GlfwWindow); - } - - public void UpdateMainWindowMode() - { - if (_clyde._mainWindow == null) - return; - - var win = (GlfwWindowReg) _clyde._mainWindow; - if (_clyde._windowMode == WindowMode.Fullscreen) - { - win.PrevWindowSize = win.WindowSize; - win.PrevWindowPos = win.WindowPos; - - SendCmd(new CmdWinSetFullscreen((nint) win.GlfwWindow)); - } - else - { - SendCmd(new CmdWinSetMonitor( - (nint) win.GlfwWindow, - 0, - win.PrevWindowPos.X, win.PrevWindowPos.Y, - win.PrevWindowSize.X, win.PrevWindowSize.Y, - 0 - )); - } - } - - private void WinThreadWinSetFullscreen(CmdWinSetFullscreen cmd) - { - var ptr = (Window*) cmd.Window; - //GLFW.GetWindowSize(ptr, out var w, out var h); - //GLFW.GetWindowPos(ptr, out var x, out var y); - - var monitor = MonitorForWindow(ptr); - var mode = GLFW.GetVideoMode(monitor); - - GLFW.SetWindowMonitor( - ptr, - monitor, - 0, 0, - mode->Width, mode->Height, - mode->RefreshRate); - - SendEvent(new EventSetFullscreenAck()); - } - - // glfwGetWindowMonitor only works for fullscreen windows. - // Picks the monitor with the top-left corner of the window. - private Monitor* MonitorForWindow(Window* window) - { - GLFW.GetWindowPos(window, out var winPosX, out var winPosY); - var monitors = GLFW.GetMonitorsRaw(out var count); - for (var i = 0; i < count; i++) - { - var monitor = monitors[i]; - GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY); - var videoMode = GLFW.GetVideoMode(monitor); - - var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height); - if (box.Contains(winPosX, winPosY)) - return monitor; - } - - // Fallback - return GLFW.GetPrimaryMonitor(); - } - - public uint? WindowGetX11Id(WindowReg window) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg) window; - try - { - return GLFW.GetX11Window(reg.GlfwWindow); - } - catch (EntryPointNotFoundException) - { - return null; - } - } - - public nint? WindowGetX11Display(WindowReg window) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg) window; - try - { - return GLFW.GetX11Display(reg.GlfwWindow); - } - catch (EntryPointNotFoundException) - { - return null; - } - } - - public nint? WindowGetWin32Window(WindowReg window) - { - if (!OperatingSystem.IsWindows()) - return null; - - var reg = (GlfwWindowReg) window; - try - { - return GLFW.GetWin32Window(reg.GlfwWindow); - } - catch (EntryPointNotFoundException) - { - return null; - } - } - - public (WindowReg?, string? error) WindowCreate( - GLContextSpec? spec, - WindowCreateParameters parameters, - WindowReg? share, - WindowReg? owner) - { - Window* sharePtr = null; - if (share is GlfwWindowReg glfwReg) - sharePtr = glfwReg.GlfwWindow; - - Window* ownerPtr = null; - if (owner is GlfwWindowReg glfwOwnerReg) - ownerPtr = glfwOwnerReg.GlfwWindow; - - var task = SharedWindowCreate( - spec, - parameters, - sharePtr, - ownerPtr); - - // Block the main thread (to avoid stuff like texture uploads being problematic). - WaitWindowCreate(task); -#pragma warning disable RA0004 - var (reg, errorResult) = task.Result; -#pragma warning restore RA0004 - - if (reg != null) - { - reg.Owner = reg.Handle; - return (reg, null); - } - - var (desc, errCode) = errorResult!.Value; - return (null, (string)$"[{errCode}]: {desc}"); - } - - public void WindowDestroy(WindowReg window) - { - var reg = (GlfwWindowReg) window; - SendCmd(new CmdWinDestroy((nint) reg.GlfwWindow, window.Owner != null)); - } - - private void WaitWindowCreate(Task windowTask) - { - while (!windowTask.IsCompleted) - { - // Keep processing events until the window task gives either an error or success. - WaitEvents(); - ProcessEvents(single: true); - } - } - - private Task SharedWindowCreate( - GLContextSpec? glSpec, - WindowCreateParameters parameters, - Window* share, Window* 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(); - SendCmd(new CmdWinCreate( - glSpec, - parameters, - (nint) share, - (nint) 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, share, owner, tcs) = cmd; - - var window = CreateGlfwWindowForRenderer(glSpec, parameters, (Window*) share, (Window*) owner); - - if (window == null) - { - var err = GLFW.GetError(out var desc); - - SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(null, (desc, 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); - - SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(reg, null), tcs)); - } - - private static void WinThreadWinDestroy(CmdWinDestroy cmd) - { - var window = (Window*) cmd.Window; - - 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. - - var hWnd = (HWND) GLFW.GetWin32Window(window); - DebugTools.Assert(hWnd != HWND.NULL); - - Windows.SetWindowLongPtrW( - hWnd, - GWLP.GWLP_HWNDPARENT, - 0); - } - - GLFW.DestroyWindow((Window*) cmd.Window); - } - - private Window* CreateGlfwWindowForRenderer( - GLContextSpec? spec, - WindowCreateParameters parameters, - Window* contextShare, - Window* ownerWindow) - { - GLFW.WindowHint(WindowHintString.X11ClassName, "RobustToolbox"); - GLFW.WindowHint(WindowHintString.X11InstanceName, "RobustToolbox"); - GLFW.WindowHint(WindowHintBool.ScaleToMonitor, true); - - if (spec == null) - { - // No OpenGL context requested. - GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.NoApi); - } - else - { - var s = spec.Value; - -#if DEBUG - GLFW.WindowHint(WindowHintBool.OpenGLDebugContext, true); -#endif - - GLFW.WindowHint(WindowHintInt.ContextVersionMajor, s.Major); - GLFW.WindowHint(WindowHintInt.ContextVersionMinor, s.Minor); - GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, s.Profile != GLContextProfile.Compatibility); - GLFW.WindowHint(WindowHintBool.SrgbCapable, true); - - switch (s.Profile) - { - case GLContextProfile.Compatibility: - GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any); - GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi); - break; - case GLContextProfile.Core: - GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core); - GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi); - break; - case GLContextProfile.Es: - GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any); - GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlEsApi); - break; - } - - GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, - s.CreationApi == GLContextCreationApi.Egl - ? ContextApi.EglContextApi - : ContextApi.NativeContextApi); - - if (s.CreationApi == GLContextCreationApi.Egl) - WsiShared.EnsureEglAvailable(); - } - - Monitor* monitor = null; - if (parameters.Monitor != null && - _winThreadMonitors.TryGetValue(parameters.Monitor.Id, out var monitorReg)) - { - monitor = monitorReg.Ptr; - var mode = GLFW.GetVideoMode(monitor); - // Set refresh rate to monitor's so that GLFW doesn't manually select one. - GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate); - } - else - { - GLFW.WindowHint(WindowHintInt.RefreshRate, -1); - } - - GLFW.WindowHint(WindowHintBool.Visible, false); - - GLFW.WindowHint(WindowHintInt.RedBits, 8); - GLFW.WindowHint(WindowHintInt.GreenBits, 8); - GLFW.WindowHint(WindowHintInt.BlueBits, 8); - 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, - parameters.Fullscreen ? monitor : null, - contextShare); - - // Check if window failed to create. - if (window == null) - return null; - - if (parameters.Maximized) - { - GLFW.GetMonitorPos(monitor, out var x, out var y); - GLFW.SetWindowPos(window, x, y); - GLFW.MaximizeWindow(window); - } - - if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0) - { - if (OperatingSystem.IsWindows()) - { - var hWnd = (HWND) GLFW.GetWin32Window(window); - WsiShared.SetWindowStyleNoTitleOptionsWindows(hWnd); - } - else if (OperatingSystem.IsLinux()) - { - try - { - var x11Window = (X11Window)GLFW.GetX11Window(window); - var x11Display = (Display*) GLFW.GetX11Display(window); - WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window); - } - catch (EntryPointNotFoundException) - { - _sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this windowing manager"); - } - } - else - { - _sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this platform"); - } - } - - if (ownerWindow != null) - { - if (OperatingSystem.IsWindows()) - { - var hWnd = (HWND) GLFW.GetWin32Window(window); - var ownerHWnd = (HWND) GLFW.GetWin32Window(ownerWindow); - DebugTools.Assert(hWnd != HWND.NULL); - - Windows.SetWindowLongPtrW( - hWnd, - GWLP.GWLP_HWNDPARENT, - ownerHWnd); - } - else if (OperatingSystem.IsLinux()) - { - try - { - var x11Display = (Display*) GLFW.GetX11Display(window); - var thisWindow = (X11Window)GLFW.GetX11Window(window); - var parentWindow = (X11Window)GLFW.GetX11Window(ownerWindow); - DebugTools.Assert(thisWindow != X11Window.NULL); - DebugTools.Assert(parentWindow != X11Window.NULL); - -#pragma warning disable CA1806 - Xlib.XSetTransientForHint(x11Display, thisWindow, parentWindow); -#pragma warning restore CA1806 - } - catch (EntryPointNotFoundException) - { - _sawmill.Warning("owner windows not implemented on this windowing manager"); - } - } - else - { - _sawmill.Warning("owner windows not implemented on this platform"); - } - - - if (parameters.StartupLocation == WindowStartupLocation.CenterOwner) - { - // TODO: Maybe include window frames in size calculations here? - // Figure out frame sizes of both windows. - GLFW.GetWindowPos(ownerWindow, out var ownerX, out var ownerY); - GLFW.GetWindowSize(ownerWindow, out var ownerW, out var ownerH); - - // Re-fetch this in case DPI scaling is changing it I guess. - GLFW.GetWindowSize(window, out var thisW, out var thisH); - - GLFW.SetWindowPos(window, ownerX + (ownerW - thisW) / 2, ownerY + (ownerH - thisH) / 2); - } - } - - if (OperatingSystem.IsWindows()) - WsiShared.WindowsSharedWindowCreate((HWND) GLFW.GetWin32Window(window), _cfg); - - if (parameters.Visible) - { - GLFW.ShowWindow(window); - } - - return window; - } - - private GlfwWindowReg WinThreadSetupWindow(Window* window) - { - var reg = new GlfwWindowReg - { - GlfwWindow = window, - Id = new WindowId(_nextWindowId++) - }; - var handle = new WindowHandle(_clyde, reg); - reg.Handle = handle; - - LoadWindowIcon(window); - - GLFW.SetCharCallback(window, _charCallback); - GLFW.SetKeyCallback(window, _keyCallback); - GLFW.SetWindowCloseCallback(window, _windowCloseCallback); - GLFW.SetCursorPosCallback(window, _cursorPosCallback); - GLFW.SetCursorEnterCallback(window, _cursorEnterCallback); - GLFW.SetWindowSizeCallback(window, _windowSizeCallback); - GLFW.SetWindowPosCallback(window, _windowPosCallback); - GLFW.SetScrollCallback(window, _scrollCallback); - GLFW.SetMouseButtonCallback(window, _mouseButtonCallback); - GLFW.SetWindowContentScaleCallback(window, _windowContentScaleCallback); - GLFW.SetWindowIconifyCallback(window, _windowIconifyCallback); - GLFW.SetWindowFocusCallback(window, _windowFocusCallback); - - GLFW.GetFramebufferSize(window, out var fbW, out var fbH); - reg.FramebufferSize = (fbW, fbH); - - GLFW.GetWindowContentScale(window, out var scaleX, out var scaleY); - reg.WindowScale = new Vector2(scaleX, scaleY); - - GLFW.GetWindowSize(window, out var w, out var h); - reg.PrevWindowSize = reg.WindowSize = (w, h); - - GLFW.GetWindowPos(window, out var x, out var y); - reg.PrevWindowPos = (x, y); - - reg.PixelRatio = reg.FramebufferSize / (Vector2) reg.WindowSize; - - return reg; - } - - private GlfwWindowReg? FindWindow(nint window) => FindWindow((Window*) window); - - private GlfwWindowReg? FindWindow(Window* window) - { - foreach (var windowReg in _clyde._windows) - { - var glfwReg = (GlfwWindowReg) windowReg; - if (glfwReg.GlfwWindow == window) - { - return glfwReg; - } - } - - return null; - } - - public Task ClipboardGetText(WindowReg mainWindow) - { - var tcs = new TaskCompletionSource(); - SendCmd(new CmdGetClipboard((nint) ((GlfwWindowReg) mainWindow).GlfwWindow, tcs)); - return tcs.Task; - } - - private static void WinThreadGetClipboard(CmdGetClipboard cmd) - { - var clipboard = GLFW.GetClipboardString((Window*) cmd.Window) ?? ""; - // Don't have to care about synchronization I don't think so just fire this immediately. - cmd.Tcs.TrySetResult(clipboard); - } - - public void ClipboardSetText(WindowReg mainWindow, string text) - { - SendCmd(new CmdSetClipboard((nint) ((GlfwWindowReg) mainWindow).GlfwWindow, text)); - } - - private static void WinThreadSetClipboard(CmdSetClipboard cmd) - { - GLFW.SetClipboardString((Window*) cmd.Window, cmd.Text); - } - - public void LoadWindowIcon(Window* window) - { - var icons = _clyde.LoadWindowIcons().ToArray(); - - // Done if no icon (e.g., macOS) - if (icons.Length == 0) - return; - - // Turn each image into a byte[] so we can actually pin their contents. - // Wish I knew a clean way to do this without allocations. - var images = icons - .Select(i => (MemoryMarshal.Cast(i.GetPixelSpan()).ToArray(), i.Width, i.Height)) - .ToList(); - - // ReSharper disable once SuggestVarOrType_Elsewhere - Span handles = stackalloc GCHandle[images.Count]; - Span glfwImages = stackalloc GlfwImage[images.Count]; - - for (var i = 0; i < images.Count; i++) - { - var image = images[i]; - handles[i] = GCHandle.Alloc(image.Item1, GCHandleType.Pinned); - var addrOfPinnedObject = (byte*) handles[i].AddrOfPinnedObject(); - glfwImages[i] = new GlfwImage(image.Width, image.Height, addrOfPinnedObject); - } - - GLFW.SetWindowIcon(window, glfwImages); - - foreach (var handle in handles) - { - handle.Free(); - } - } - - public void GLMakeContextCurrent(WindowReg? window) - { - if (window != null) - { - CheckWindowDisposed(window); - - var reg = (GlfwWindowReg)window; - - GLFW.MakeContextCurrent(reg.GlfwWindow); - } - else - { - GLFW.MakeContextCurrent(null); - } - } - - public void GLSwapInterval(WindowReg reg, int interval) - { - GLFW.SwapInterval(interval); - } - - public void* GLGetProcAddress(string procName) - { - return (void*) GLFW.GetProcAddress(procName); - } - - public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor) - { - // Not supported on GLFW. - } - - public void TextInputStart(WindowReg reg) - { - // Not properly supported on GLFW. - - ((GlfwWindowReg)reg).TextInputActive = true; - } - - public void TextInputStop(WindowReg reg) - { - // Not properly supported on GLFW. - - ((GlfwWindowReg)reg).TextInputActive = false; - } - - private void CheckWindowDisposed(WindowReg reg) - { - if (reg.IsDisposed) - throw new ObjectDisposedException("Window disposed"); - } - - private sealed class GlfwWindowReg : WindowReg - { - public Window* GlfwWindow; - - // 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; - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.cs deleted file mode 100644 index eb54210be..000000000 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Runtime.Serialization; -using OpenToolkit.GraphicsLibraryFramework; -using Robust.Client.Input; -using Robust.Shared; -using Robust.Shared.Configuration; -using Robust.Shared.IoC; -using Robust.Shared.Log; - -namespace Robust.Client.Graphics.Clyde -{ - internal partial class Clyde - { - private sealed partial class GlfwWindowingImpl : IWindowingImpl - { - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - - private readonly Clyde _clyde; - - private readonly ISawmill _sawmill; - private readonly ISawmill _sawmillGlfw; - - private bool _glfwInitialized; -#if DEBUG - private bool _win32Experience; -#endif - - public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps) - { - _clyde = clyde; - deps.InjectDependencies(this, true); - - _sawmill = _logManager.GetSawmill("clyde.win"); - _sawmillGlfw = _logManager.GetSawmill("clyde.win.glfw"); - } - - public bool Init() - { -#if DEBUG - _cfg.OnValueChanged(CVars.DisplayWin32Experience, b => _win32Experience = b, true); -#endif - _cfg.OnValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap); - - InitChannels(); - - if (!InitGlfw()) - { - return false; - } - - SetupGlobalCallbacks(); - InitMonitors(); - InitCursors(); - InitKeyMap(); - - return true; - } - - public void Shutdown() - { - if (_glfwInitialized) - { - _sawmill.Debug("Terminating GLFW."); - _cfg.UnsubValueChanged(CVars.DisplayUSQWERTYHotkeys, ReInitKeyMap); - GLFW.Terminate(); - } - } - - public void FlushDispose() - { - // Not currently used - } - - public string GetDescription() - { - return $"GLFW {GLFW.GetVersionString()}"; - } - - private void ReInitKeyMap(bool onValueChanged) - { - InitKeyMap(); - _inputManager.InputModeChanged(); - } - - private bool InitGlfw() - { - StoreCallbacks(); - - GLFW.SetErrorCallback(_errorCallback); - if (!GLFW.Init()) - { - var err = GLFW.GetError(out var desc); - _sawmill.Fatal($"Failed to initialize GLFW! [{err}] {desc}"); - return false; - } - - _glfwInitialized = true; - var version = GLFW.GetVersionString(); - _sawmill.Debug("GLFW initialized, version: {0}.", version); - - return true; - } - - private void OnGlfwError(ErrorCode code, string description) - { - _sawmillGlfw.Error("GLFW Error: [{0}] {1}", code, description); - } - - [Serializable] - [Virtual] - public class GlfwException : Exception - { - public GlfwException() - { - } - - public GlfwException(string message) : base(message) - { - } - - public GlfwException(string message, Exception inner) : base(message, inner) - { - } - } - } - } -} diff --git a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs index 55c9a88ba..18e9f79dd 100644 --- a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs +++ b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Key.cs @@ -146,7 +146,7 @@ internal partial class Clyde 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_APPLICATION, Key.Menu); MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket); MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket); MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon); @@ -173,7 +173,7 @@ internal partial class Clyde 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_KP_PERIOD, Key.NumpadDecimal); MapKey(SC.SDL_SCANCODE_LEFT, Key.Left); MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right); MapKey(SC.SDL_SCANCODE_UP, Key.Up); @@ -203,6 +203,7 @@ internal partial class Clyde MapKey(SC.SDL_SCANCODE_F23, Key.F23); MapKey(SC.SDL_SCANCODE_F24, Key.F24); MapKey(SC.SDL_SCANCODE_PAUSE, Key.Pause); + MapKey(SC.SDL_SCANCODE_CAPSLOCK, Key.CapsLock); var keyMapReverse = new Dictionary(); diff --git a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs index 9344bcad8..f88c27f7e 100644 --- a/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs +++ b/Robust.Client/Graphics/Clyde/Windowing/Sdl3.Window.cs @@ -606,7 +606,7 @@ internal partial class Clyde private void WinThreadSetClipboard(CmdSetClipboard cmd) { var res = SDL.SDL_SetClipboardText(cmd.Text); - if (res) + if (!res) _sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError()); } diff --git a/Robust.Client/Graphics/Font.cs b/Robust.Client/Graphics/Font.cs index da627769c..e1f16b92f 100644 --- a/Robust.Client/Graphics/Font.cs +++ b/Robust.Client/Graphics/Font.cs @@ -104,6 +104,12 @@ namespace Robust.Client.Graphics Handle = IoCManager.Resolve().MakeInstance(res.FontFaceHandle, size); } + internal VectorFont(IFontInstanceHandle handle, int size) + { + Size = size; + Handle = handle; + } + public override int GetAscent(float scale) => Handle.GetAscent(scale); public override int GetHeight(float scale) => Handle.GetHeight(scale); public override int GetDescent(float scale) => Handle.GetDescent(scale); @@ -222,4 +228,74 @@ namespace Robust.Client.Graphics return null; } } + + /// + /// Possible values for font weights. Larger values have thicker font strokes. + /// + /// + /// + /// These values are based on the usWeightClass property of the OpenType specification: + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass + /// + /// + /// + public enum FontWeight : ushort + { + Thin = 100, + ExtraLight = 200, + UltraLight = ExtraLight, + Light = 300, + SemiLight = 350, + Normal = 400, + Regular = Normal, + Medium = 500, + SemiBold = 600, + DemiBold = SemiBold, + Bold = 700, + ExtraBold = 800, + UltraBold = ExtraBold, + Black = 900, + Heavy = Black, + ExtraBlack = 950, + UltraBlack = ExtraBlack, + } + + /// + /// Possible slant values for fonts. + /// + /// + public enum FontSlant : byte + { + // NOTE: Enum values correspond to DWRITE_FONT_STYLE. + Normal = 0, + Oblique = 1, + + // FUN FACT: they're called "italics" because they look like the Leaning Tower of Pisa. + // Don't fact-check that. + Italic = 2 + } + + /// + /// Possible values for font widths. Larger values are proportionally wider. + /// + /// + /// + /// These values are based on the usWidthClass property of the OpenType specification: + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass + /// + /// + /// + public enum FontWidth : ushort + { + UltraCondensed = 1, + ExtraCondensed = 2, + Condensed = 3, + SemiCondensed = 4, + Normal = 5, + Medium = Normal, + SemiExpanded = 6, + Expanded = 7, + ExtraExpanded = 8, + UltraExpanded = 9, + } } diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs b/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs new file mode 100644 index 000000000..8bead2c13 --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Console; + +namespace Robust.Client.Graphics.FontManagement; + +internal sealed class SystemFontDebugCommand : IConsoleCommand +{ + public string Command => "system_font_debug"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + new SystemFontDebugWindow().OpenCentered(); + } +} diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml new file mode 100644 index 000000000..bee4a157e --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs new file mode 100644 index 000000000..8061d766f --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs @@ -0,0 +1,98 @@ +using System.Linq; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Client.Graphics.FontManagement; + +[GenerateTypedNameReferences] +internal sealed partial class SystemFontDebugWindow : DefaultWindow +{ + private static readonly int[] ExampleFontSizes = [8, 12, 16, 24, 36]; + private const string ExampleString = "The quick brown fox jumps over the lazy dog"; + + [Dependency] private readonly ISystemFontManager _systemFontManager = default!; + + public SystemFontDebugWindow() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + var buttonGroup = new ButtonGroup(); + + foreach (var group in _systemFontManager.SystemFontFaces.GroupBy(k => k.FamilyName).OrderBy(k => k.Key)) + { + var fonts = group.ToArray(); + SelectorContainer.AddChild(new Selector(this, buttonGroup, group.Key, fonts)); + } + } + + private void SelectFontFamily(ISystemFontFace[] fonts) + { + FamilyLabel.Text = fonts[0].FamilyName; + + FaceContainer.RemoveAllChildren(); + + foreach (var font in fonts) + { + var exampleContainer = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Margin = new Thickness(8) + }; + + foreach (var size in ExampleFontSizes) + { + var fontInstance = font.Load(size); + + var richTextLabel = new RichTextLabel + { + Stylesheet = new Stylesheet([ + StylesheetHelpers.Element().Prop("font", fontInstance) + ]), + }; + richTextLabel.SetMessage(FormattedMessage.FromUnformatted(ExampleString)); + exampleContainer.AddChild(richTextLabel); + } + + FaceContainer.AddChild(new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = + { + new RichTextLabel + { + Text = $""" + {font.FullName} + Family: "{font.FamilyName}", face: "{font.FaceName}", PostScript = "{font.PostscriptName}" + Weight: {font.Weight} ({(int) font.Weight}), slant: {font.Slant} ({(int) font.Slant}), width: {font.Width} ({(int) font.Width}) + """, + }, + exampleContainer + }, + Margin = new Thickness(0, 0, 0, 8) + }); + } + } + + private sealed class Selector : Control + { + public Selector(SystemFontDebugWindow window, ButtonGroup group, string family, ISystemFontFace[] fonts) + { + var button = new Button + { + Text = family, + Group = group, + ToggleMode = true + }; + AddChild(button); + + button.OnPressed += _ => window.SelectFontFamily(fonts); + } + } +} diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs new file mode 100644 index 000000000..4af351c29 --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Threading; +using Robust.Shared.Log; + +namespace Robust.Client.Graphics.FontManagement; + +internal abstract class SystemFontManagerBase +{ + /// + /// The "standard" locale used when looking up the PostScript name of a font face. + /// + /// + /// + /// Font files allow the PostScript name to be localized, however in practice + /// we would really like to have a language-unambiguous identifier to refer to a font file. + /// We use this locale (en-US) to look up teh PostScript font name, if there are multiple provided. + /// This matches the behavior of the Local Font Access web API: + /// https://wicg.github.io/local-font-access/#concept-font-representation + /// + /// + protected static readonly CultureInfo StandardLocale = new("en-US", false); + + protected readonly IFontManagerInternal FontManager; + protected readonly ISawmill Sawmill; + + protected readonly Lock Lock = new(); + protected readonly List Fonts = []; + + public IEnumerable SystemFontFaces { get; } + + public SystemFontManagerBase(ILogManager logManager, IFontManagerInternal fontManager) + { + FontManager = fontManager; + Sawmill = logManager.GetSawmill("font.system"); + + SystemFontFaces = Fonts.AsReadOnly(); + } + + protected abstract IFontFaceHandle LoadFontFace(BaseHandle handle); + + protected static string GetLocalizedForLocaleOrFirst(LocalizedStringSet set, CultureInfo culture) + { + var matchCulture = culture; + while (!Equals(matchCulture, CultureInfo.InvariantCulture)) + { + if (set.Values.TryGetValue(culture.Name, out var value)) + return value; + + matchCulture = matchCulture.Parent; + } + + return set.Values[set.Primary]; + } + + protected abstract class BaseHandle(SystemFontManagerBase parent) : ISystemFontFace + { + private IFontFaceHandle? _cachedFont; + + public required string PostscriptName { get; init; } + + public required LocalizedStringSet FullNames; + public required LocalizedStringSet FamilyNames; + public required LocalizedStringSet FaceNames; + + public required FontWeight Weight { get; init; } + public required FontSlant Slant { get; init; } + public required FontWidth Width { get; init; } + + public string FullName => GetLocalizedFullName(CultureInfo.CurrentCulture); + public string FamilyName => GetLocalizedFamilyName(CultureInfo.CurrentCulture); + public string FaceName => GetLocalizedFaceName(CultureInfo.CurrentCulture); + + public string GetLocalizedFullName(CultureInfo culture) + { + return GetLocalizedForLocaleOrFirst(FullNames, culture); + } + + public string GetLocalizedFamilyName(CultureInfo culture) + { + return GetLocalizedForLocaleOrFirst(FamilyNames, culture); + } + + public string GetLocalizedFaceName(CultureInfo culture) + { + return GetLocalizedForLocaleOrFirst(FaceNames, culture); + } + + public Font Load(int size) + { + var handle = GetFaceHandle(); + + var instance = parent.FontManager.MakeInstance(handle, size); + + return new VectorFont(instance, size); + } + + private IFontFaceHandle GetFaceHandle() + { + lock (parent.Lock) + { + if (_cachedFont != null) + return _cachedFont; + + parent.Sawmill.Verbose($"Loading system font face: {PostscriptName}"); + + return _cachedFont = parent.LoadFontFace(this); + } + } + } + + protected struct LocalizedStringSet + { + public static readonly LocalizedStringSet Empty = FromSingle(""); + + /// + /// The first locale to appear in the list of localized strings. + /// Used as fallback if the desired locale is not provided. + /// + public required string Primary; + public required Dictionary Values; + + public static LocalizedStringSet FromSingle(string value, string language = "en") + { + return new LocalizedStringSet + { + Primary = language, + Values = new Dictionary { { language, value } } + }; + } + } + + protected sealed class MemoryMappedFontMemoryHandle : IFontMemoryHandle + { + private readonly MemoryMappedFile _mappedFile; + private readonly MemoryMappedViewAccessor _accessor; + + public MemoryMappedFontMemoryHandle(string filePath) + { + _mappedFile = MemoryMappedFile.CreateFromFile( + filePath, + FileMode.Open, + null, + 0, + MemoryMappedFileAccess.Read); + + _accessor = _mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); + } + + public unsafe byte* GetData() + { + byte* pointer = null; + _accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); + return pointer; + } + + public nint GetDataSize() + { + return (nint)_accessor.Capacity; + } + + public void Dispose() + { + _accessor.Dispose(); + _mappedFile.Dispose(); + } + } +} diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerCoreText.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerCoreText.cs new file mode 100644 index 000000000..c86f3e6dd --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerCoreText.cs @@ -0,0 +1,195 @@ +#if MACOS +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Robust.Client.Interop.MacOS; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using CF = Robust.Client.Interop.MacOS.CoreFoundation; +using CT = Robust.Client.Interop.MacOS.CoreText; + +namespace Robust.Client.Graphics.FontManagement; + +/// +/// Implementation of that uses CoreText on macOS. +/// +internal sealed class SystemFontManagerCoreText : SystemFontManagerBase, ISystemFontManagerInternal +{ + private static readonly FontWidth[] FontWidths = Enum.GetValues(); + + public bool IsSupported => true; + + public SystemFontManagerCoreText(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager, + fontManager) + { + } + + public unsafe void Initialize() + { + Sawmill.Verbose("Getting CTFontCollection..."); + + var collection = CT.CTFontCollectionCreateFromAvailableFonts(null); + var array = CT.CTFontCollectionCreateMatchingFontDescriptors(collection); + + var count = CF.CFArrayGetCount(array); + Sawmill.Verbose($"Have {count} descriptors..."); + + for (nint i = 0; i < count.Value; i++) + { + var item = (__CTFontDescriptor*)CF.CFRetain(CF.CFArrayGetValueAtIndex(array, new CLong(i))); + + try + { + LoadFontDescriptor(item); + } + catch (Exception ex) + { + Sawmill.Error($"Failed to load font descriptor: {ex}"); + } + finally + { + CF.CFRelease(item); + } + } + + CF.CFRelease(array); + CF.CFRelease(collection); + } + + private unsafe void LoadFontDescriptor(__CTFontDescriptor* descriptor) + { + var displayName = GetFontAttributeManaged(descriptor, CT.kCTFontDisplayNameAttribute); + var postscriptName = GetFontAttributeManaged(descriptor, CT.kCTFontNameAttribute); + var familyName = GetFontAttributeManaged(descriptor, CT.kCTFontFamilyNameAttribute); + var styleName = GetFontAttributeManaged(descriptor, CT.kCTFontStyleNameAttribute); + + var url = (__CFURL*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontURLAttribute); + + const int maxPath = 1024; + var buf = stackalloc byte[maxPath]; + var result = CF.CFURLGetFileSystemRepresentation(url, 1, buf, new CLong(maxPath)); + if (result == 0) + throw new Exception("CFURLGetFileSystemRepresentation failed!"); + + // Sawmill.Verbose(CF.CFStringToManaged(CF.CFURLGetString(url))); + + CF.CFRelease(url); + + var traits = (__CFDictionary*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontTraitsAttribute); + var (weight, slant, width) = ParseTraits(traits); + + CF.CFRelease(traits); + + var path = Marshal.PtrToStringUTF8((nint)buf)!; + + Fonts.Add(new Handle(this) + { + PostscriptName = postscriptName, + FullNames = LocalizedStringSet.FromSingle(displayName), + FamilyNames = LocalizedStringSet.FromSingle(familyName), + FaceNames = LocalizedStringSet.FromSingle(styleName), + Weight = weight, + Slant = slant, + Width = width, + Path = path + }); + } + + private static unsafe (FontWeight, FontSlant, FontWidth) ParseTraits(__CFDictionary* dictionary) + { + var weight = FontWeight.Normal; + var slant = FontSlant.Normal; + var width = FontWidth.Normal; + + var weightVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWeightTrait); + if (weightVal != null) + weight = ConvertWeight(weightVal); + + var slantVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontSlantTrait); + if (slantVal != null) + slant = ConvertSlant(slantVal); + + var widthVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWidthTrait); + if (widthVal != null) + width = ConvertWidth(widthVal); + + return (weight, slant, width); + } + + private static readonly (float, FontWeight)[] FontWeightTable = + [ + ((float) AppKit.NSFontWeightUltraLight, FontWeight.UltraLight), + ((float) AppKit.NSFontWeightThin, FontWeight.Thin), + ((float) AppKit.NSFontWeightLight, FontWeight.Light), + ((float) AppKit.NSFontWeightRegular, FontWeight.Regular), + ((float) AppKit.NSFontWeightMedium, FontWeight.Medium), + ((float) AppKit.NSFontWeightSemiBold, FontWeight.SemiBold), + ((float) AppKit.NSFontWeightBold, FontWeight.Bold), + ((float) AppKit.NSFontWeightHeavy, FontWeight.Heavy), + ((float) AppKit.NSFontWeightBlack, FontWeight.Black) + ]; + + private static unsafe FontWeight ConvertWeight(__CFNumber* number) + { + float val; + CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val); + + var valCopy = val; + return FontWeightTable.MinBy(tup => Math.Abs(tup.Item1 - valCopy)).Item2; + } + + private static unsafe FontWidth ConvertWidth(__CFNumber* number) + { + float val; + CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val); + + // Normalize to 0-1 range + val = (val + 1) / 2; + var lerped = MathHelper.Lerp((float)FontWidths[0], (float)FontWidths[^1], val); + return FontWidths.MinBy(x => Math.Abs((float)x - lerped)); + } + + private static unsafe FontSlant ConvertSlant(__CFNumber* number) + { + float val; + CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val); + + // Normalize to 0-1 range + return val == 0 ? FontSlant.Normal : FontSlant.Italic; + } + + private static unsafe string GetFontAttributeManaged(__CTFontDescriptor* descriptor, __CFString* key) + { + var str = (__CFString*)CT.CTFontDescriptorCopyAttribute(descriptor, key); + + try + { + return CF.CFStringToManaged(str); + } + finally + { + CF.CFRelease(str); + } + } + + public void Shutdown() + { + // Nothing to do. + } + + protected override IFontFaceHandle LoadFontFace(BaseHandle handle) + { + var path = ((Handle)handle).Path; + Sawmill.Verbose(path); + + // CTFontDescriptor does not seem to have any way to identify *which* index in the font file should be accessed. + // So we have to just load every one until the postscript name matches. + return FontManager.LoadWithPostscriptName(new MemoryMappedFontMemoryHandle(path), handle.PostscriptName); + } + + private sealed class Handle(SystemFontManagerCoreText parent) : BaseHandle(parent) + { + public required string Path; + } +} +#endif diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs new file mode 100644 index 000000000..ea439cdf7 --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs @@ -0,0 +1,503 @@ +#if WINDOWS +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Log; +using Robust.Shared.Utility; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; +using static TerraFX.Interop.DirectX.DWRITE_FACTORY_TYPE; +using static TerraFX.Interop.DirectX.DWRITE_FONT_PROPERTY_ID; +using static TerraFX.Interop.Windows.Windows; + +namespace Robust.Client.Graphics.FontManagement; + +/// +/// Implementation of that uses DirectWrite on Windows. +/// +internal sealed unsafe class SystemFontManagerDirectWrite : SystemFontManagerBase, ISystemFontManagerInternal +{ + // For future implementors of other platforms: + // a significant amount of code in this file will be shareable with that of other platforms, + // so some refactoring is warranted. + + private readonly IConfigurationManager _cfg; + + private IDWriteFactory3* _dWriteFactory; + private IDWriteFontSet* _systemFontSet; + + public bool IsSupported => true; + + /// + /// Implementation of that uses DirectWrite on Windows. + /// + public SystemFontManagerDirectWrite( + ILogManager logManager, + IConfigurationManager cfg, + IFontManagerInternal fontManager) + : base(logManager, fontManager) + { + _cfg = cfg; + } + + public void Initialize() + { + CreateDWriteFactory(); + + _systemFontSet = GetSystemFontSet(_dWriteFactory); + + lock (Lock) + { + var fontCount = _systemFontSet->GetFontCount(); + for (var i = 0u; i < fontCount; i++) + { + LoadSingleFontFromSet(_systemFontSet, i); + } + } + + Sawmill.Verbose($"Loaded {Fonts.Count} fonts"); + } + + public void Shutdown() + { + _systemFontSet->Release(); + _systemFontSet = null; + + _dWriteFactory->Release(); + _dWriteFactory = null; + + lock (Lock) + { + foreach (var systemFont in Fonts) + { + ((Handle)systemFont).FontFace->Release(); + } + + Fonts.Clear(); + } + } + + private void LoadSingleFontFromSet(IDWriteFontSet* set, uint fontIndex) + { + // Get basic parameters that every font should probably have? + if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_POSTSCRIPT_NAME, out var postscriptNames)) + return; + + if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FULL_NAME, out var fullNames)) + return; + + if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, out var familyNames)) + return; + + if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FACE_NAME, out var faceNames)) + return; + + // I assume these parameters can't be missing in practice, but better safe than sorry. + TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_WEIGHT, out var weight); + TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STYLE, out var style); + TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STRETCH, out var stretch); + + var parsedWeight = ParseFontWeight(weight); + var parsedSlant = ParseFontSlant(style); + var parsedWidth = ParseFontWidth(stretch); + + IDWriteFontFaceReference* reference = null; + var result = set->GetFontFaceReference(fontIndex, &reference); + ThrowIfFailed(result); + + var handle = new Handle(this, reference) + { + PostscriptName = GetLocalizedForLocaleOrFirst(postscriptNames, StandardLocale), + FullNames = fullNames, + FamilyNames = familyNames, + FaceNames = faceNames, + Weight = parsedWeight, + Slant = parsedSlant, + Width = parsedWidth + }; + + Fonts.Add(handle); + } + + private static FontWeight ParseFontWeight(DWriteLocalizedString[]? strings) + { + if (strings == null) + return FontWeight.Regular; + + return (FontWeight)Parse.Int32(strings[0].Value); + } + + private static FontSlant ParseFontSlant(DWriteLocalizedString[]? strings) + { + if (strings == null) + return FontSlant.Normal; + + return (FontSlant)Parse.Int32(strings[0].Value); + } + + private static FontWidth ParseFontWidth(DWriteLocalizedString[]? strings) + { + if (strings == null) + return FontWidth.Normal; + + return (FontWidth)Parse.Int32(strings[0].Value); + } + + private void CreateDWriteFactory() + { + fixed (IDWriteFactory3** pFactory = &_dWriteFactory) + { + var result = DirectX.DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(), + (IUnknown**)pFactory); + + ThrowIfFailed(result); + } + } + + private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory) + { + IDWriteFactory6* factory6; + IDWriteFontSet* fontSet; + var result = factory->QueryInterface(__uuidof(), (void**)&factory6); + if (result.SUCCEEDED) + { + Sawmill.Verbose("IDWriteFactory6 available, using newer GetSystemFontSet"); + + result = factory6->GetSystemFontSet( + _cfg.GetCVar(CVars.FontWindowsDownloadable), + (IDWriteFontSet1**)(&fontSet)); + + factory6->Release(); + } + else + { + Sawmill.Verbose("IDWriteFactory6 not available"); + + result = factory->GetSystemFontSet(&fontSet); + } + + ThrowIfFailed(result, "GetSystemFontSet"); + return fontSet; + } + + protected override IFontFaceHandle LoadFontFace(BaseHandle handle) + { + var fontFace = ((Handle)handle).FontFace; + IDWriteFontFile* file = null; + IDWriteFontFileLoader* loader = null; + + try + { + var result = fontFace->GetFontFile(&file); + ThrowIfFailed(result, "IDWriteFontFaceReference::GetFontFile"); + result = file->GetLoader(&loader); + ThrowIfFailed(result, "IDWriteFontFile::GetLoader"); + + void* referenceKey; + uint referenceKeyLength; + result = file->GetReferenceKey(&referenceKey, &referenceKeyLength); + ThrowIfFailed(result, "IDWriteFontFile::GetReferenceKey"); + + IDWriteLocalFontFileLoader* localLoader; + result = loader->QueryInterface(__uuidof(), (void**)&localLoader); + if (result.SUCCEEDED) + { + Sawmill.Verbose("Loading font face via memory mapped file..."); + + // We can get the local file path on disk. This means we can directly load it via mmap. + uint filePathLength; + ThrowIfFailed( + localLoader->GetFilePathLengthFromKey(referenceKey, referenceKeyLength, &filePathLength), + "IDWriteLocalFontFileLoader::GetFilePathLengthFromKey"); + var filePath = new char[filePathLength + 1]; + fixed (char* pFilePath = filePath) + { + ThrowIfFailed( + localLoader->GetFilePathFromKey( + referenceKey, + referenceKeyLength, + pFilePath, + (uint)filePath.Length), + "IDWriteLocalFontFileLoader::GetFilePathFromKey"); + } + + var path = new string(filePath, 0, (int)filePathLength); + + localLoader->Release(); + + return FontManager.Load(new MemoryMappedFontMemoryHandle(path)); + } + else + { + Sawmill.Verbose("Loading font face via stream..."); + + // DirectWrite doesn't give us anything to go with for this file, read it into regular memory. + // If the font file has multiple faces, which is possible, then this approach will duplicate memory. + // That sucks, but I'm really not sure whether there's any way around this short of + // comparing the memory contents by hashing to check equality. + // As I'm pretty sure we can't like reference equality check the font objects somehow. + IDWriteFontFileStream* stream; + result = loader->CreateStreamFromKey(referenceKey, referenceKeyLength, &stream); + ThrowIfFailed(result, "IDWriteFontFileLoader::CreateStreamFromKey"); + + using var streamObject = new DirectWriteStream(stream); + return FontManager.Load(streamObject, (int)fontFace->GetFontFaceIndex()); + } + } + finally + { + if (file != null) + file->Release(); + if (loader != null) + loader->Release(); + } + } + + private static bool TryGetStrings( + IDWriteFontSet* set, + uint listIndex, + DWRITE_FONT_PROPERTY_ID property, + [NotNullWhen(true)] out DWriteLocalizedString[]? strings) + { + BOOL exists; + IDWriteLocalizedStrings* dWriteStrings = null; + var result = set->GetPropertyValues( + listIndex, + property, + &exists, + &dWriteStrings); + ThrowIfFailed(result, "IDWriteFontSet::GetPropertyValues"); + + if (!exists) + { + strings = null; + return false; + } + + try + { + strings = GetStrings(dWriteStrings); + return true; + } + finally + { + dWriteStrings->Release(); + } + } + + private static bool TryGetStringsSet( + IDWriteFontSet* set, + uint listIndex, + DWRITE_FONT_PROPERTY_ID property, + out LocalizedStringSet strings) + { + if (!TryGetStrings(set, listIndex, property, out var stringsArray)) + { + strings = default; + return false; + } + + strings = StringsToSet(stringsArray); + return true; + } + + private static DWriteLocalizedString[] GetStrings(IDWriteLocalizedStrings* localizedStrings) + { + IDWriteStringList* list; + ThrowIfFailed(localizedStrings->QueryInterface(__uuidof(), (void**)&list)); + + try + { + return GetStrings(list); + } + finally + { + list->Release(); + } + } + + private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList) + { + var array = new DWriteLocalizedString[stringList->GetCount()]; + + var stringPool = ArrayPool.Shared.Rent(256); + + for (var i = 0; i < array.Length; i++) + { + uint length; + + ThrowIfFailed(stringList->GetStringLength((uint)i, &length), "IDWriteStringList::GetStringLength"); + ExpandIfNecessary(ref stringPool, length + 1); + fixed (char* pArr = stringPool) + { + ThrowIfFailed( + stringList->GetString((uint)i, pArr, (uint)stringPool.Length), + "IDWriteStringList::GetString"); + } + + var value = new string(stringPool, 0, (int)length); + + ThrowIfFailed(stringList->GetLocaleNameLength((uint)i, &length), "IDWriteStringList::GetLocaleNameLength"); + ExpandIfNecessary(ref stringPool, length + 1); + fixed (char* pArr = stringPool) + { + ThrowIfFailed( + stringList->GetLocaleName((uint)i, pArr, (uint)stringPool.Length), + "IDWriteStringList::GetLocaleName"); + } + + var localeName = new string(stringPool, 0, (int)length); + + array[i] = new DWriteLocalizedString(value, localeName); + } + + ArrayPool.Shared.Return(stringPool); + + return array; + } + + private static void ExpandIfNecessary(ref char[] array, uint requiredLength) + { + if (requiredLength < array.Length) + return; + + ArrayPool.Shared.Return(array); + array = ArrayPool.Shared.Rent(checked((int)requiredLength)); + } + + private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings) + { + var dict = new Dictionary(); + + foreach (var (value, localeName) in strings) + { + dict[localeName] = value; + } + + return new LocalizedStringSet { Primary = strings[0].LocaleName, Values = dict }; + } + + private sealed class Handle(SystemFontManagerDirectWrite parent, IDWriteFontFaceReference* fontFace) : BaseHandle(parent) + { + public readonly IDWriteFontFaceReference* FontFace = fontFace; + } + + /// + /// A simple implementation of a .NET Stream over a IDWriteFontFileStream. + /// + private sealed class DirectWriteStream : Stream + { + private readonly IDWriteFontFileStream* _stream; + private readonly ulong _size; + + private ulong _position; + private bool _disposed; + + public DirectWriteStream(IDWriteFontFileStream* stream) + { + _stream = stream; + + fixed (ulong* pSize = &_size) + { + var result = _stream->GetFileSize(pSize); + ThrowIfFailed(result, "IDWriteFontFileStream::GetFileSize"); + } + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } + + public override int Read(Span buffer) + { + if (_disposed) + throw new ObjectDisposedException(nameof(DirectWriteStream)); + + var readLength = (uint)buffer.Length; + if (readLength + _position > _size) + readLength = (uint)(_size - _position); + + void* fragmentStart; + void* fragmentContext; + + var result = _stream->ReadFileFragment(&fragmentStart, _position, readLength, &fragmentContext); + ThrowIfFailed(result); + + var data = new ReadOnlySpan(fragmentStart, (int)readLength); + data.CopyTo(buffer); + + _stream->ReleaseFileFragment(fragmentContext); + + _position += readLength; + return (int)readLength; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = Length + offset; + break; + default: + throw new ArgumentOutOfRangeException(nameof(origin), origin, null); + } + + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => (long)_size; + + public override long Position + { + get => (long)_position; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, _size); + + _position = (ulong)value; + } + } + + protected override void Dispose(bool disposing) + { + _stream->Release(); + _disposed = true; + } + } + + private record struct DWriteLocalizedString(string Value, string LocaleName); +} +#endif diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs new file mode 100644 index 000000000..31638699e --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Robust.Client.Graphics.FontManagement; + +/// +/// A fallback implementation of that just loads no fonts. +/// +internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal +{ + public void Initialize() + { + + } + + public void Shutdown() + { + + } + + public bool IsSupported => false; + public IEnumerable SystemFontFaces => []; +} diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs new file mode 100644 index 000000000..ee1616951 --- /dev/null +++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs @@ -0,0 +1,235 @@ +#if FREEDESKTOP +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Robust.Shared.Log; +using SpaceWizards.Fontconfig.Interop; + +namespace Robust.Client.Graphics.FontManagement; + +internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal +{ + private static readonly (int Fc, FontWidth Width)[] WidthTable = [ + (Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed), + (Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed), + (Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed), + (Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed), + (Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal), + (Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded), + (Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded), + (Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded), + (Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded), + ]; + + public bool IsSupported => true; + + public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager) + : base(logManager, fontManager) + { + } + + public void Initialize() + { + Sawmill.Verbose("Initializing Fontconfig..."); + + var result = Fontconfig.FcInit(); + if (result == Fontconfig.FcFalse) + throw new InvalidOperationException("Failed to initialize fontconfig!"); + + Sawmill.Verbose("Listing fonts..."); + + var os = Fontconfig.FcObjectSetCreate(); + AddToObjectSet(os, Fontconfig.FC_FAMILY); + AddToObjectSet(os, Fontconfig.FC_FAMILYLANG); + AddToObjectSet(os, Fontconfig.FC_STYLE); + AddToObjectSet(os, Fontconfig.FC_STYLELANG); + AddToObjectSet(os, Fontconfig.FC_FULLNAME); + AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG); + AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME); + + AddToObjectSet(os, Fontconfig.FC_SLANT); + AddToObjectSet(os, Fontconfig.FC_WEIGHT); + AddToObjectSet(os, Fontconfig.FC_WIDTH); + + AddToObjectSet(os, Fontconfig.FC_FILE); + AddToObjectSet(os, Fontconfig.FC_INDEX); + + var allPattern = Fontconfig.FcPatternCreate(); + var set = Fontconfig.FcFontList(null, allPattern, os); + + for (var i = 0; i < set->nfont; i++) + { + var pattern = set->fonts[i]; + + try + { + LoadPattern(pattern); + } + catch (Exception e) + { + Sawmill.Error($"Error while loading pattern: {e}"); + } + } + + Fontconfig.FcPatternDestroy(allPattern); + Fontconfig.FcObjectSetDestroy(os); + Fontconfig.FcFontSetDestroy(set); + } + + public void Shutdown() + { + // Nada. + } + + private void LoadPattern(FcPattern* pattern) + { + var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0]; + var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0]; + + var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG); + var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG); + var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG); + var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME); + if (psName == null) + return; + + var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN]; + var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR]; + var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL]; + + Fonts.Add(new Handle(this) + { + FilePath = path, + FileIndex = idx, + FaceNames = style ?? LocalizedStringSet.Empty, + FullNames = fullName ?? LocalizedStringSet.Empty, + FamilyNames = family ?? LocalizedStringSet.Empty, + PostscriptName = psName[0], + Slant = SlantFromFontconfig(slant[0]), + Weight = WeightFromFontconfig(weight[0]), + Width = WidthFromFontconfig(width[0]) + }); + } + + private static FontWeight WeightFromFontconfig(int value) + { + return (FontWeight)Fontconfig.FcWeightToOpenType(value); + } + + private static FontSlant SlantFromFontconfig(int value) + { + return value switch + { + Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic, + Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic, + _ => FontSlant.Normal, + }; + } + + private static FontWidth WidthFromFontconfig(int value) + { + return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width; + } + + private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan value) + { + var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value))); + if (result == Fontconfig.FcFalse) + throw new InvalidOperationException("Failed to add to object set!"); + } + + private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan @object) + { + return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) => + { + byte* str = null; + var res = Fontconfig.FcPatternGetString(p, o, i, &str); + value = Marshal.PtrToStringUTF8((nint)str)!; + return res; + }); + } + + private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan @object) + { + return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) => + { + FcResult res; + fixed (int* pValue = &value) + { + res = Fontconfig.FcPatternGetInteger(p, o, i, pValue); + } + return res; + }); + } + + private delegate FcResult GetValue(FcPattern* p, sbyte* o, int i, out T value); + private static unsafe T[]? PatternGetValues(FcPattern* pattern, ReadOnlySpan @object, GetValue getValue) + { + var list = new List(); + + var i = 0; + while (true) + { + var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value); + if (result == FcResult.FcResultMatch) + { + list.Add(value); + } + else if (result == FcResult.FcResultNoMatch) + { + return null; + } + else if (result == FcResult.FcResultNoId) + { + break; + } + else + { + throw new Exception($"FcPatternGetString gave error: {result}"); + } + } + + return list.ToArray(); + } + + private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan @object, ReadOnlySpan objectLang) + { + var values = PatternGetStrings(pattern, @object); + var languages = PatternGetStrings(pattern, objectLang); + + if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length) + return null; + + var dict = new Dictionary(); + + for (var i = 0; i < values.Length; i++) + { + var val = values[i]; + var lang = languages[i]; + + dict.TryAdd(lang, val); + } + + return new LocalizedStringSet + { + Primary = languages[0], + Values = dict + }; + } + + protected override IFontFaceHandle LoadFontFace(BaseHandle handle) + { + var cast = (Handle)handle; + + return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex); + } + + private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent) + { + public required string FilePath; + public required int FileIndex; + } +} +#endif diff --git a/Robust.Client/Graphics/FontManager.cs b/Robust.Client/Graphics/FontManager.cs index 19c0d410a..ca048ecd9 100644 --- a/Robust.Client/Graphics/FontManager.cs +++ b/Robust.Client/Graphics/FontManager.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Text; using JetBrains.Annotations; using Robust.Client.Utility; -using Robust.Shared.Graphics; +using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Utility; using SharpFont; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using TerraFX.Interop.Windows; namespace Robust.Client.Graphics { @@ -20,6 +20,7 @@ namespace Robust.Client.Graphics private const int SheetHeight = 256; private readonly IClyde _clyde; + private readonly ISawmill _sawmill; private uint _baseFontDpi = 96; @@ -28,22 +29,56 @@ namespace Robust.Client.Graphics private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances = new(); - public FontManager(IClyde clyde) + public FontManager(IClyde clyde, ILogManager logManager) { _clyde = clyde; _library = new Library(); + _sawmill = logManager.GetSawmill("font"); } - public IFontFaceHandle Load(Stream stream) + public IFontFaceHandle Load(Stream stream, int index = 0) { // Freetype directly operates on the font memory managed by us. // As such, the font data should be pinned in POH. var fontData = stream.CopyToPinnedArray(); - var face = new Face(_library, fontData, 0); - var handle = new FontFaceHandle(face); + return Load(new ArrayMemoryHandle(fontData), index); + } + + public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0) + { + var face = FaceLoad(memory, index); + var handle = new FontFaceHandle(face, memory); return handle; } + public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName) + { + var numFaces = 1; + + for (var i = 0; i < numFaces; i++) + { + var face = FaceLoad(memory, i); + numFaces = face.FaceCount; + + if (face.GetPostscriptName() == postscriptName) + return new FontFaceHandle(face, memory); + + face.Dispose(); + } + + // Fallback, load SOMETHING. + _sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}"); + return new FontFaceHandle(FaceLoad(memory, 0), memory); + } + + private unsafe Face FaceLoad(IFontMemoryHandle memory, int index) + { + return new Face(_library, + (nint)memory.GetData(), + checked((int)memory.GetDataSize()), + index); + } + void IFontManagerInternal.SetFontDpi(uint fontDpi) { _baseFontDpi = fontDpi; @@ -235,10 +270,13 @@ namespace Robust.Client.Graphics private sealed class FontFaceHandle : IFontFaceHandle { + // Keep this alive to avoid it being GC'd. + private readonly IFontMemoryHandle _memoryHandle; public Face Face { get; } - public FontFaceHandle(Face face) + public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle) { + _memoryHandle = memoryHandle; Face = face; } } @@ -377,5 +415,32 @@ namespace Robust.Client.Graphics public CharMetrics Metrics; public AtlasTexture? Texture; } + + private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle + { + private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned); + + public unsafe byte* GetData() + { + return (byte*) _gcHandle.AddrOfPinnedObject(); + } + + public IntPtr GetDataSize() + { + return array.Length; + } + + public void Dispose() + { + _gcHandle.Free(); + _gcHandle = default; + GC.SuppressFinalize(this); + } + + ~ArrayMemoryHandle() + { + Dispose(); + } + } } } diff --git a/Robust.Client/Graphics/IClyde.cs b/Robust.Client/Graphics/IClyde.cs index 11c20c6f6..c040eafcc 100644 --- a/Robust.Client/Graphics/IClyde.cs +++ b/Robust.Client/Graphics/IClyde.cs @@ -15,6 +15,7 @@ namespace Robust.Client.Graphics { public delegate void CopyPixelsDelegate(Image pixels) where T : unmanaged, IPixel; + [NotContentImplementable] public interface IClyde { IClydeWindow MainWindow { get; } diff --git a/Robust.Client/Graphics/IClydeMonitor.cs b/Robust.Client/Graphics/IClydeMonitor.cs index 494645f9e..fdd4a0788 100644 --- a/Robust.Client/Graphics/IClydeMonitor.cs +++ b/Robust.Client/Graphics/IClydeMonitor.cs @@ -6,6 +6,7 @@ namespace Robust.Client.Graphics /// /// Represents a connected monitor on the user's system. /// + [NotContentImplementable] public interface IClydeMonitor { /// diff --git a/Robust.Client/Graphics/IClydeViewport.cs b/Robust.Client/Graphics/IClydeViewport.cs index 8300b6459..8ad332e3c 100644 --- a/Robust.Client/Graphics/IClydeViewport.cs +++ b/Robust.Client/Graphics/IClydeViewport.cs @@ -11,6 +11,7 @@ namespace Robust.Client.Graphics /// A viewport is an API for rendering a section of the game map centered around an eye, /// complete with lighting, FOV and grid rendering. /// + [NotContentImplementable] public interface IClydeViewport : IDisposable { /// diff --git a/Robust.Client/Graphics/IClydeWindow.cs b/Robust.Client/Graphics/IClydeWindow.cs index 4ee6dcb22..ec859db51 100644 --- a/Robust.Client/Graphics/IClydeWindow.cs +++ b/Robust.Client/Graphics/IClydeWindow.cs @@ -8,6 +8,7 @@ namespace Robust.Client.Graphics /// /// Represents a single operating system window. /// + [NotContentImplementable] public interface IClydeWindow : IDisposable { bool IsDisposed { get; } @@ -67,7 +68,8 @@ namespace Robust.Client.Graphics void TextInputStop(); } - public interface IClydeWindowInternal : IClydeWindow + [NotContentImplementable] + internal interface IClydeWindowInternal : IClydeWindow { nint? WindowsHWnd { get; } } diff --git a/Robust.Client/Graphics/ICursor.cs b/Robust.Client/Graphics/ICursor.cs index 54a0ed3a0..b26edc9d2 100644 --- a/Robust.Client/Graphics/ICursor.cs +++ b/Robust.Client/Graphics/ICursor.cs @@ -13,6 +13,7 @@ namespace Robust.Client.Graphics /// /// /// + [NotContentImplementable] public interface ICursor : IDisposable { } diff --git a/Robust.Client/Graphics/IDirectionalTextureProvider.cs b/Robust.Client/Graphics/IDirectionalTextureProvider.cs index 4d253be8b..363cc0d11 100644 --- a/Robust.Client/Graphics/IDirectionalTextureProvider.cs +++ b/Robust.Client/Graphics/IDirectionalTextureProvider.cs @@ -2,6 +2,7 @@ using Robust.Shared.Maths; namespace Robust.Client.Graphics { + [NotContentImplementable] public interface IDirectionalTextureProvider { Texture Default { get; } diff --git a/Robust.Client/Graphics/IFontManager.cs b/Robust.Client/Graphics/IFontManager.cs index 34cacb93d..78ab72c37 100644 --- a/Robust.Client/Graphics/IFontManager.cs +++ b/Robust.Client/Graphics/IFontManager.cs @@ -1,16 +1,25 @@ +using System; using System.IO; using System.Text; -using Robust.Shared.Graphics; namespace Robust.Client.Graphics { + [NotContentImplementable] public interface IFontManager { public void ClearFontCache(); } internal interface IFontManagerInternal : IFontManager { - IFontFaceHandle Load(Stream stream); + IFontFaceHandle Load(Stream stream, int index = 0); + IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0); + + /// + /// Load a specified font in a font collection. + /// + /// Memory for the entire font collection. + /// The postscript name of the font to load. + IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName); IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size); void SetFontDpi(uint fontDpi); } @@ -22,8 +31,6 @@ namespace Robust.Client.Graphics internal interface IFontInstanceHandle { - - Texture? GetCharTexture(Rune codePoint, float scale); Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale); CharMetrics? GetCharMetrics(Rune codePoint, float scale); @@ -35,6 +42,12 @@ namespace Robust.Client.Graphics int GetLineHeight(float scale); } + internal unsafe interface IFontMemoryHandle : IDisposable + { + byte* GetData(); + nint GetDataSize(); + } + /// /// Metrics for a single glyph in a font. /// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information. diff --git a/Robust.Client/Graphics/IRenderHandle.cs b/Robust.Client/Graphics/IRenderHandle.cs index a14c31b5d..689620242 100644 --- a/Robust.Client/Graphics/IRenderHandle.cs +++ b/Robust.Client/Graphics/IRenderHandle.cs @@ -9,6 +9,7 @@ namespace Robust.Client.Graphics /// /// Unstable API. Likely to break hard during renderer rewrite if you rely on it. /// + [NotContentImplementable] public interface IRenderHandle { DrawingHandleScreen DrawingHandleScreen { get; } diff --git a/Robust.Client/Graphics/IRenderTarget.cs b/Robust.Client/Graphics/IRenderTarget.cs index dc8cc53c5..c2324aa22 100644 --- a/Robust.Client/Graphics/IRenderTarget.cs +++ b/Robust.Client/Graphics/IRenderTarget.cs @@ -9,6 +9,7 @@ namespace Robust.Client.Graphics /// /// Represents something that can be rendered to. /// + [NotContentImplementable] public interface IRenderTarget : IDisposable { /// diff --git a/Robust.Client/Graphics/IRenderTexture.cs b/Robust.Client/Graphics/IRenderTexture.cs index e05fd7b5b..d6b5797fc 100644 --- a/Robust.Client/Graphics/IRenderTexture.cs +++ b/Robust.Client/Graphics/IRenderTexture.cs @@ -5,6 +5,7 @@ namespace Robust.Client.Graphics /// /// A render target that renders into a texture that can be re-used later. /// + [NotContentImplementable] public interface IRenderTexture : IRenderTarget { /// diff --git a/Robust.Client/Graphics/IRsiStateLike.cs b/Robust.Client/Graphics/IRsiStateLike.cs index f8486d2f4..f16f41013 100644 --- a/Robust.Client/Graphics/IRsiStateLike.cs +++ b/Robust.Client/Graphics/IRsiStateLike.cs @@ -2,6 +2,7 @@ using Robust.Shared.Graphics.RSI; namespace Robust.Client.Graphics { + [NotContentImplementable] public interface IRsiStateLike : IDirectionalTextureProvider { RsiDirectionType RsiDirections { get; } diff --git a/Robust.Client/Graphics/ISystemFontManager.cs b/Robust.Client/Graphics/ISystemFontManager.cs new file mode 100644 index 000000000..1114b017c --- /dev/null +++ b/Robust.Client/Graphics/ISystemFontManager.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Globalization; + +namespace Robust.Client.Graphics; + +/// +/// Provides access to fonts installed on the user's operating system. +/// +/// +/// +/// Different operating systems ship different fonts, so you should generally not rely on any one +/// specific font being available. This system is primarily provided for allowing user preference. +/// +/// +/// +[NotContentImplementable] +public interface ISystemFontManager +{ + /// + /// Whether access to system fonts is currently supported on this platform. + /// + bool IsSupported { get; } + + /// + /// The list of font face available from the operating system. + /// + IEnumerable SystemFontFaces { get; } +} + +/// +/// A single font face, provided by the user's operating system. +/// +/// +[NotContentImplementable] +public interface ISystemFontFace +{ + /// + /// The PostScript name of the font face. + /// This is generally the closest to an unambiguous unique identifier as you're going to get. + /// + /// + /// + /// For example, "Arial-ItalicMT" + /// + /// + string PostscriptName { get; } + + /// + /// The full name of the font face, localized to the current locale. + /// + /// + /// + /// For example, "Arial Cursiva" + /// + /// + /// + string FullName { get; } + + /// + /// The family name of the font face, localized to the current locale. + /// + /// + /// + /// For example, "Arial" + /// + /// + /// + string FamilyName { get; } + + /// + /// The face name (or "style name") of the font face, localized to the current locale. + /// + /// + /// + /// For example, "Cursiva" + /// + /// + /// + string FaceName { get; } + + /// + /// Get the , localized to a specific locale. + /// + /// The locale to fetch the localized string for. + string GetLocalizedFullName(CultureInfo culture); + + /// + /// Get the , localized to a specific locale. + /// + /// The locale to fetch the localized string for. + string GetLocalizedFamilyName(CultureInfo culture); + + /// + /// Get the , localized to a specific locale. + /// + /// The locale to fetch the localized string for. + string GetLocalizedFaceName(CultureInfo culture); + + /// + /// The weight of the font face. + /// + FontWeight Weight { get; } + + /// + /// The slant of the font face. + /// + FontSlant Slant { get; } + + /// + /// The width of the font face. + /// + FontWidth Width { get; } + + /// + /// Load the font face so that it can be used in-engine. + /// + /// The size to load the font at. + /// A font object that can be used to render text. + Font Load(int size); +} + +/// +/// Engine-internal API for . +/// +internal interface ISystemFontManagerInternal : ISystemFontManager +{ + void Initialize(); + void Shutdown(); +} diff --git a/Robust.Client/Graphics/Lighting/ILightManager.cs b/Robust.Client/Graphics/Lighting/ILightManager.cs index 3204c4689..a44181d9e 100644 --- a/Robust.Client/Graphics/Lighting/ILightManager.cs +++ b/Robust.Client/Graphics/Lighting/ILightManager.cs @@ -2,6 +2,7 @@ using Robust.Shared.Maths; namespace Robust.Client.Graphics { + [NotContentImplementable] public interface ILightManager { /// diff --git a/Robust.Client/Graphics/LoadingScreenManager.cs b/Robust.Client/Graphics/LoadingScreenManager.cs new file mode 100644 index 000000000..894cd8b1e --- /dev/null +++ b/Robust.Client/Graphics/LoadingScreenManager.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Console; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Stopwatch = Robust.Shared.Timing.Stopwatch; + +namespace Robust.Client.Graphics; + +internal interface ILoadingScreenManager +{ + void BeginLoadingSection(string sectionName); + + /// + /// Start a loading bar "section" for the given method. + /// Must be ended with EndSection. + /// + void BeginLoadingSection(object method); + + void EndLoadingSection(); + + /// + /// Will run the giving function and add a custom "section" for it on the loading screen. + /// + void LoadingStep(Action action, object method); +} + +/// +/// Manager that creates and displays a basic splash screen and loading bar. +/// +internal sealed class LoadingScreenManager : ILoadingScreenManager +{ + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IClydeInternal _clyde = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; + + private readonly Stopwatch _sw = new(); + + #region UI constants + + private const int LoadingBarWidth = 250; + private const int LoadingBarHeight = 20; + private const int LoadingBarOutlineOffset = 5; + private static readonly Vector2i LogoLoadingBarOffset = (0, 20); + private static readonly Vector2i LoadTimesIndent = (20, 0); + + private const int NumLongestLoadTimes = 5; + + private static readonly Color LoadingBarColor = Color.White; + + #endregion + + #region Cvars + + private string _splashLogo = ""; + private bool _showLoadingBar; + private bool _showDebug; + + #endregion + + private const string FontLocation = "/EngineFonts/NotoSans/NotoSans-Regular.ttf"; + private const int FontSize = 11; + private VectorFont? _font; + + // Number of loading sections for the loading bar. This has to be manually set! + private int _numberOfLoadingSections; + + // The name of the section and how much time it took to load + internal readonly List<(string Name, TimeSpan LoadTime)> Times = []; + + private int _currentSection; + private string? _currentSectionName; + + private bool _currentlyInSection; + private bool _finished; + + public void Initialize(int sections) + { + if (_finished) + return; + + _clyde.VsyncEnabled = false; + + _numberOfLoadingSections = sections; + + _sawmill = _logManager.GetSawmill("loading"); + + _splashLogo = _cfg.GetCVar(CVars.DisplaySplashLogo); + _showLoadingBar = _cfg.GetCVar(CVars.LoadingShowBar); + _showDebug = _cfg.GetCVar(CVars.LoadingShowDebug); + + if (_resourceCache.TryGetResource(FontLocation, out var fontResource)) + _font = new VectorFont(fontResource, FontSize); + else + _sawmill.Error($"Could not load font: {FontLocation}"); + } + + public void BeginLoadingSection(string sectionName) => BeginLoadingSection(sectionName, false); + public void BeginLoadingSection(string sectionName, bool dontRender) + { + if (_finished) + return; + + if (_currentlyInSection) + throw new InvalidOperationException("You cannot begin more than one section at a time!"); + + _currentlyInSection = true; + + _currentSectionName = sectionName; + + if (!dontRender) + { + // This ensures that if the screen was resized or something the new size is properly updated to clyde. + _clyde.ProcessInput(new FrameEventArgs((float)_sw.Elapsed.TotalSeconds)); + _sw.Restart(); + _clyde.Render(); + } + else + { + _sw.Restart(); + } + } + + /// + /// Start a loading bar "section" for the given method. + /// Must be ended with EndSection. + /// + public void BeginLoadingSection(object method) + { + if (_finished) + return; + + BeginLoadingSection(method.GetType().Name); + } + + public void EndLoadingSection() + { + if (_finished) + return; + + var time = _sw.Elapsed; + if (_currentSectionName != null) + Times.Add((_currentSectionName, time)); + _currentSection++; + _currentlyInSection = false; + } + + /// + /// Will run the giving function and add a custom "section" for it on the loading screen. + /// + public void LoadingStep(Action action, object method) + { + if (_finished) + return; + + BeginLoadingSection(method as string ?? method.GetType().Name); + action(); + EndLoadingSection(); + } + + public void Finish() + { + if (_finished) + return; + + if (_currentSection != _numberOfLoadingSections) + _sawmill.Error($"The number of seen loading sections isn't equal to the total number of loading sections! Seen: {_currentSection}, Total: {_numberOfLoadingSections}"); + + _finished = true; + } + + #region Drawing functions + + /// + /// Draw out the splash and loading screen. + /// + public void DrawLoadingScreen(IRenderHandle handle, Vector2i screenSize) + { + if (_finished) + return; + + var scale = UserInterfaceManager.CalculateUIScale(_clyde.MainWindow.ContentScale.X, _cfg); + + // Start at the center! + var location = screenSize / 2; + + DrawSplash(handle, ref location, scale); + + DrawLoadingBar(handle, ref location, scale); + + if (_showDebug) + { + DrawCurrentLoading(handle, ref location, scale); + + DrawTopTimes(handle, ref location, scale); + } + } + + private void DrawSplash(IRenderHandle handle, ref Vector2i startLocation, float scale) + { + if (string.IsNullOrEmpty(_splashLogo)) + return; + + if (!_resourceCache.TryGetResource(_splashLogo, out var textureResource)) + return; + + var drawSize = textureResource.Texture.Size * scale; + + handle.DrawingHandleScreen.DrawTextureRect(textureResource.Texture, UIBox2.FromDimensions(startLocation - drawSize / 2, drawSize)); + startLocation += Vector2i.Up * (int) drawSize.Y / 2; + } + + private void DrawLoadingBar(IRenderHandle handle, ref Vector2i location, float scale) + { + var barWidth = (int)(LoadingBarWidth * scale); + var barHeight = (int)(LoadingBarHeight * scale); + var outlineOffset = (int)(LoadingBarOutlineOffset * scale); + + // Always do the offsets, it looks a lot better! + location.X -= barWidth / 2; + location += (Vector2i) (LogoLoadingBarOffset * scale); + + if (!_showLoadingBar) + return; + + var sectionWidth = barWidth / _numberOfLoadingSections; + + var barTopLeft = location; + var barBottomRight = new Vector2i(_currentSection * sectionWidth % barWidth, barHeight); + var barBottomRightMax = new Vector2i(barWidth, barHeight); + + var outlinePosition = barTopLeft + Vector2i.DownLeft * outlineOffset; + var outlineSize = barBottomRightMax + Vector2i.UpRight * 2 * outlineOffset; + + // Outline + handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(outlinePosition, outlineSize), LoadingBarColor, false); + + // Progress bar + handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(barTopLeft, barBottomRight), LoadingBarColor); + + location += Vector2i.Up * outlineSize; + } + + // Draw the currently loading section to the screen. + private void DrawCurrentLoading(IRenderHandle handle, ref Vector2i location, float scale) + { + if (_font == null || _currentSectionName == null) + return; + + handle.DrawingHandleScreen.DrawString(_font, location, _currentSectionName, scale, Color.White); + location += Vector2i.Up * _font.GetLineHeight(scale); + } + + // Draw the slowest loading times to the screen. + private void DrawTopTimes(IRenderHandle handle, ref Vector2i location, float scale) + { + if (_font == null) + return; + + location += (Vector2i)(LoadTimesIndent * scale); + + var offset = 0; + var x = 0; + Times.Sort((a, b) => b.LoadTime.CompareTo(a.LoadTime)); + + foreach (var (name, time) in Times) + { + if (x >= NumLongestLoadTimes) + break; + + var entry = $"{time.TotalSeconds:F2} - {name}"; + handle.DrawingHandleScreen.DrawString(_font, location + new Vector2i(0, offset), entry, scale, Color.White); + offset += _font.GetLineHeight(scale); + x++; + } + + location += Vector2i.Up * offset; + } + + #endregion // Drawing functions +} + +internal sealed class ShowTopLoadingTimesCommand : IConsoleCommand +{ + [Dependency] private readonly LoadingScreenManager _mgr = default!; + + public string Command => "loading_top"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var sorted = _mgr.Times.Where(x => x.LoadTime > TimeSpan.FromSeconds(0.01)).OrderByDescending(x => x.LoadTime); + foreach (var (name, time) in sorted) + { + shell.WriteLine($"{time.TotalSeconds:F2} - {name}"); + } + } +} diff --git a/Robust.Client/Graphics/Overlays/IOverlayManager.cs b/Robust.Client/Graphics/Overlays/IOverlayManager.cs index 1c94894e0..8095cee7f 100644 --- a/Robust.Client/Graphics/Overlays/IOverlayManager.cs +++ b/Robust.Client/Graphics/Overlays/IOverlayManager.cs @@ -7,6 +7,7 @@ using Robust.Shared.Timing; namespace Robust.Client.Graphics; [PublicAPI] +[NotContentImplementable] public interface IOverlayManager { bool AddOverlay(Overlay overlay); diff --git a/Robust.Client/Graphics/SystemFontManager.cs b/Robust.Client/Graphics/SystemFontManager.cs new file mode 100644 index 000000000..0ff534f92 --- /dev/null +++ b/Robust.Client/Graphics/SystemFontManager.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using Robust.Client.Graphics.FontManagement; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Timing; + +namespace Robust.Client.Graphics; + +/// +/// Implementation of that proxies to platform-specific implementations, +/// and adds additional logging. +/// +internal sealed class SystemFontManager : ISystemFontManagerInternal, IPostInjectInit +{ + [Dependency] private readonly IFontManagerInternal _fontManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private ISawmill _sawmill = default!; + + private ISystemFontManagerInternal _implementation = default!; + + public bool IsSupported => _implementation.IsSupported; + public IEnumerable SystemFontFaces => _implementation.SystemFontFaces; + + public void Initialize() + { + _implementation = GetImplementation(); + _sawmill.Verbose($"Using {_implementation.GetType()}"); + + _sawmill.Debug("Initializing system font manager implementation"); + try + { + var sw = RStopwatch.StartNew(); + _implementation.Initialize(); + _sawmill.Debug($"Done initializing system font manager in {sw.Elapsed}"); + } + catch (Exception e) + { + // This is a non-critical engine system that has to parse significant amounts of external data. + // Best to fail gracefully to avoid full startup failures. + + _sawmill.Error($"Error while initializing system font manager, resorting to fallback: {e}"); + _implementation = new SystemFontManagerFallback(); + } + } + + public void Shutdown() + { + _sawmill.Verbose("Shutting down system font manager"); + + try + { + _implementation.Shutdown(); + } + catch (Exception e) + { + _sawmill.Error($"Exception shutting down system font manager: {e}"); + return; + } + + _sawmill.Verbose("Successfully shut down system font manager"); + } + + private ISystemFontManagerInternal GetImplementation() + { + if (!_cfg.GetCVar(CVars.FontSystem)) + return new SystemFontManagerFallback(); + +#if WINDOWS + return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager); +#elif FREEDESKTOP + return new SystemFontManagerFontconfig(_logManager, _fontManager); +#elif MACOS + return new SystemFontManagerCoreText(_logManager, _fontManager); +#else + return new SystemFontManagerFallback(); +#endif + } + + void IPostInjectInit.PostInject() + { + _sawmill = _logManager.GetSawmill("font.system"); + // _sawmill.Level = LogLevel.Verbose; + } +} diff --git a/Robust.Client/IBaseClient.cs b/Robust.Client/IBaseClient.cs index 813257728..57cdda439 100644 --- a/Robust.Client/IBaseClient.cs +++ b/Robust.Client/IBaseClient.cs @@ -6,6 +6,7 @@ namespace Robust.Client /// /// Top level class that controls the game logic of the client. /// + [NotContentImplementable] public interface IBaseClient { /// diff --git a/Robust.Client/IGameController.cs b/Robust.Client/IGameController.cs index 1212e26a6..8316bd46b 100644 --- a/Robust.Client/IGameController.cs +++ b/Robust.Client/IGameController.cs @@ -4,6 +4,7 @@ using Robust.Shared.Timing; namespace Robust.Client; +[NotContentImplementable] public interface IGameController { InitialLaunchState LaunchState { get; } diff --git a/Robust.Client/Input/IInputManager.cs b/Robust.Client/Input/IInputManager.cs index c1eb8e845..29c49d516 100644 --- a/Robust.Client/Input/IInputManager.cs +++ b/Robust.Client/Input/IInputManager.cs @@ -11,6 +11,7 @@ namespace Robust.Client.Input /// /// Manages key bindings, input commands and other misc. input systems. /// + [NotContentImplementable] public interface IInputManager { bool Enabled { get; set; } @@ -76,6 +77,15 @@ namespace Robust.Client.Input /// event Func UIKeyBindStateChanged; + /// + /// Called to check if the UI is considered "focused". + /// + /// + /// This is effectively similar to the return value of , + /// but may be called at different times. + /// + event Func CheckUIIsFocused; + /// /// If UIKeyBindStateChanged did not handle the BoundKeyEvent, KeyBindStateChanged is called. /// diff --git a/Robust.Client/Input/IKeyBinding.cs b/Robust.Client/Input/IKeyBinding.cs index 3eb41ac4b..89394f85d 100644 --- a/Robust.Client/Input/IKeyBinding.cs +++ b/Robust.Client/Input/IKeyBinding.cs @@ -2,6 +2,7 @@ using Robust.Shared.Input; namespace Robust.Client.Input { + [NotContentImplementable] public interface IKeyBinding { BoundKeyState State { get; } @@ -17,6 +18,12 @@ namespace Robust.Client.Input bool CanFocus { get; } bool CanRepeat { get; } bool AllowSubCombs { get; } + + /// + /// For a -type binding, + /// whether the binding should activate if UI is focused. + /// + bool CommandWhenUIFocused { get; } int Priority { get; } /// diff --git a/Robust.Client/Input/InputDevices.cs b/Robust.Client/Input/InputDevices.cs index b520121dd..abed7da74 100644 --- a/Robust.Client/Input/InputDevices.cs +++ b/Robust.Client/Input/InputDevices.cs @@ -173,6 +173,7 @@ namespace Robust.Client.Input F24, Pause, World1, + CapsLock, } public static bool IsMouseKey(this Key key) diff --git a/Robust.Client/Input/InputManager.cs b/Robust.Client/Input/InputManager.cs index 1dcd194f6..96238a9c0 100644 --- a/Robust.Client/Input/InputManager.cs +++ b/Robust.Client/Input/InputManager.cs @@ -67,6 +67,7 @@ namespace Robust.Client.Input private readonly bool[] _keysPressed = new bool[256]; private ValueList> _uiKeyBindStateChanged; + private ValueList> _checkUIIsFocused; /// [ViewVariables] @@ -83,6 +84,12 @@ namespace Robust.Client.Input remove => _uiKeyBindStateChanged.Remove(value); } + public event Func CheckUIIsFocused + { + add => _checkUIIsFocused.Add(value); + remove => _checkUIIsFocused.Remove(value); + } + /// public event Action? KeyBindStateChanged; @@ -383,6 +390,18 @@ namespace Robust.Client.Input { if (binding.BindingType == KeyBindingType.Command && state == BoundKeyState.Down) { + if (!binding.CommandWhenUIFocused) + { + var uiFocused = false; + foreach (var handler in _checkUIIsFocused) + { + uiFocused |= handler(); + } + + if (uiFocused) + return false; + } + _console.ExecuteCommand(binding.FunctionCommand); return true; } @@ -578,7 +597,7 @@ namespace Robust.Client.Input Key baseKey, Key? mod1, Key? mod2, Key? mod3) { var binding = new KeyBinding(this, function.FunctionName, bindingType, baseKey, false, false, false, - 0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown); + 0, true, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown); RegisterBinding(binding); @@ -589,7 +608,7 @@ namespace Robust.Client.Input Key baseKey, Key? mod1, Key? mod2, Key? mod3) { var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false, - 0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown); + 0, true, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown); RegisterBinding(binding); @@ -599,7 +618,7 @@ namespace Robust.Client.Input public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true, bool invalid = false) { var binding = new KeyBinding(this, reg.Function.FunctionName, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat, - reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3); + reg.AllowSubCombs, reg.Priority, reg.CommandWhenUIFocused, reg.Mod1, reg.Mod2, reg.Mod3); RegisterBinding(binding, markModified); @@ -769,6 +788,8 @@ namespace Robust.Client.Input [ViewVariables] public bool AllowSubCombs { get; internal set; } + public bool CommandWhenUIFocused { get; } + [ViewVariables] public int Priority { get; internal set; } public KeyBinding( @@ -776,7 +797,9 @@ namespace Robust.Client.Input string function, KeyBindingType bindingType, Key baseKey, - bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown, + bool canFocus, bool canRepeat, bool allowSubCombs, int priority, + bool commandWhenUIFocused, + Key mod1 = Key.Unknown, Key mod2 = Key.Unknown, Key mod3 = Key.Unknown) { @@ -786,6 +809,7 @@ namespace Robust.Client.Input CanRepeat = canRepeat; AllowSubCombs = allowSubCombs; Priority = priority; + CommandWhenUIFocused = commandWhenUIFocused; _inputManager = inputManager; PackedKeyCombo = new PackedKeyCombo(baseKey, mod1, mod2, mod3); diff --git a/Robust.Client/Input/KeyBindingRegistration.cs b/Robust.Client/Input/KeyBindingRegistration.cs index 1824ac93c..f432f1c8c 100644 --- a/Robust.Client/Input/KeyBindingRegistration.cs +++ b/Robust.Client/Input/KeyBindingRegistration.cs @@ -26,5 +26,12 @@ namespace Robust.Client.Input public bool CanRepeat; [DataField("allowSubCombs")] public bool AllowSubCombs; + + /// + /// For a -type binding, + /// whether the binding should activate if UI is focused. + /// + [DataField] + public bool CommandWhenUIFocused { get; set; } = true; } } diff --git a/Robust.Client/Interop/MacOS/AppKit.cs b/Robust.Client/Interop/MacOS/AppKit.cs new file mode 100644 index 000000000..442154901 --- /dev/null +++ b/Robust.Client/Interop/MacOS/AppKit.cs @@ -0,0 +1,24 @@ +// ReSharper disable InconsistentNaming +#if MACOS + +namespace Robust.Client.Interop.MacOS; + +/// +/// Binding to macOS AppKit. +/// +internal static class AppKit +{ + // Values pulled from here: + // https://chromium.googlesource.com/chromium/src/+/b5019b491932dfa597acb3a13a9e7780fb6525a9/ui/gfx/platform_font_mac.mm#53 + public const double NSFontWeightUltraLight = -0.8; + public const double NSFontWeightThin = -0.6; + public const double NSFontWeightLight = -0.4; + public const double NSFontWeightRegular = 0; + public const double NSFontWeightMedium = 0.23; + public const double NSFontWeightSemiBold = 0.30; + public const double NSFontWeightBold = 0.40; + public const double NSFontWeightHeavy = 0.56; + public const double NSFontWeightBlack = 0.62; +} + +#endif diff --git a/Robust.Client/Interop/MacOS/CoreFoundation.cs b/Robust.Client/Interop/MacOS/CoreFoundation.cs new file mode 100644 index 000000000..fa1908522 --- /dev/null +++ b/Robust.Client/Interop/MacOS/CoreFoundation.cs @@ -0,0 +1,97 @@ +#if MACOS +using System.Runtime.InteropServices; +using CFIndex = System.Runtime.InteropServices.CLong; +using Boolean = byte; + +namespace Robust.Client.Interop.MacOS; + +// ReSharper disable InconsistentNaming + +/// +/// Binding to macOS Core Foundation. +/// +internal static unsafe class CoreFoundation +{ + private const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + + public const int kCFNumberFloat32Type = 5; + + public static string CFStringToManaged(__CFString* str) + { + var length = CFStringGetLength(str); + + return string.Create( + checked((int)length.Value), + (nint)str, + static (span, arg) => + { + fixed (char* pBuffer = span) + { + CFStringGetCharacters((__CFString*)arg, + new CFRange + { + location = new CFIndex(0), + length = new CFIndex(span.Length), + }, + pBuffer); + } + }); + } + + [DllImport(CoreFoundationLibrary)] + internal static extern void* CFRetain(void* cf); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFRelease(void* cf); + + [DllImport(CoreFoundationLibrary)] + internal static extern CFIndex CFArrayGetCount(__CFArray* array); + + [DllImport(CoreFoundationLibrary)] + internal static extern void* CFArrayGetValueAtIndex(__CFArray* array, CFIndex index); + + [DllImport(CoreFoundationLibrary)] + internal static extern CFIndex CFStringGetLength(__CFString* str); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFStringGetCharacters(__CFString* str, CFRange range, char* buffer); + + [DllImport(CoreFoundationLibrary)] + internal static extern Boolean CFURLGetFileSystemRepresentation( + __CFURL* url, + Boolean resolveAgainstBase, + byte* buffer, + CFIndex maxBufLen); + + [DllImport(CoreFoundationLibrary)] + internal static extern __CFString* CFURLGetString(__CFURL* url); + + [DllImport(CoreFoundationLibrary)] + internal static extern CFIndex CFDictionaryGetCount(__CFDictionary* theDict); + + [DllImport(CoreFoundationLibrary)] + internal static extern void* CFDictionaryGetValue(__CFDictionary* theDict, void* key); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFDictionaryGetKeysAndValues(__CFDictionary* theDict, void** keys, void** values); + + [DllImport(CoreFoundationLibrary)] + internal static extern void CFNumberGetValue(__CFNumber* number, CLong theType, void* valuePtr); +} + +internal struct __CFNumber; + +internal struct __CFString; + +internal struct __CFURL; + +internal struct __CFArray; + +internal struct __CFDictionary; + +internal struct CFRange +{ + public CFIndex location; + public CFIndex length; +} +#endif diff --git a/Robust.Client/Interop/MacOS/CoreText.cs b/Robust.Client/Interop/MacOS/CoreText.cs new file mode 100644 index 000000000..3f47dd785 --- /dev/null +++ b/Robust.Client/Interop/MacOS/CoreText.cs @@ -0,0 +1,54 @@ +#if MACOS +using System.Runtime.InteropServices; + +namespace Robust.Client.Interop.MacOS; + +// ReSharper disable InconsistentNaming + +/// +/// Binding to macOS Core Text. +/// +internal static unsafe class CoreText +{ + private const string CoreTextLibrary = "/System/Library/Frameworks/CoreText.framework/CoreText"; + + public static readonly __CFString* kCTFontURLAttribute; + public static readonly __CFString* kCTFontNameAttribute; + public static readonly __CFString* kCTFontDisplayNameAttribute; + public static readonly __CFString* kCTFontFamilyNameAttribute; + public static readonly __CFString* kCTFontStyleNameAttribute; + public static readonly __CFString* kCTFontTraitsAttribute; + public static readonly __CFString* kCTFontWeightTrait; + public static readonly __CFString* kCTFontWidthTrait; + public static readonly __CFString* kCTFontSlantTrait; + + static CoreText() + { + var lib = NativeLibrary.Load(CoreTextLibrary); + kCTFontURLAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontURLAttribute)); + kCTFontNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontNameAttribute)); + kCTFontDisplayNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontDisplayNameAttribute)); + kCTFontFamilyNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontFamilyNameAttribute)); + kCTFontStyleNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontStyleNameAttribute)); + kCTFontTraitsAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontTraitsAttribute)); + kCTFontWeightTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWeightTrait)); + kCTFontWidthTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWidthTrait)); + kCTFontSlantTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontSlantTrait)); + } + + [DllImport(CoreTextLibrary)] + public static extern __CTFontCollection* CTFontCollectionCreateFromAvailableFonts(__CFDictionary* options); + + [DllImport(CoreTextLibrary)] + public static extern __CFArray* CTFontCollectionCreateMatchingFontDescriptors(__CTFontCollection* collection); + + [DllImport(CoreTextLibrary)] + public static extern void* CTFontDescriptorCopyAttribute(__CTFontDescriptor* descriptor, __CFString* attribute); + + [DllImport(CoreTextLibrary)] + public static extern __CFDictionary* CTFontDescriptorCopyAttributes(__CTFontDescriptor* descriptor); +} + +internal struct __CTFontCollection; +internal struct __CTFontDescriptor; +#endif diff --git a/Robust.Client/Physics/PhysicsSystem.Predict.cs b/Robust.Client/Physics/PhysicsSystem.Predict.cs index 90552c7c1..5d8c130c7 100644 --- a/Robust.Client/Physics/PhysicsSystem.Predict.cs +++ b/Robust.Client/Physics/PhysicsSystem.Predict.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Collections.Generic; +using System.Numerics; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Maths; @@ -141,7 +142,8 @@ public sealed partial class PhysicsSystem if ((contact.Flags & ContactFlags.Filter) != 0x0) { if (!ShouldCollide(fixtureA, fixtureB) || - !ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB)) + !ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) || + !ShouldCollideJoints(uidA, uidB)) { contact.IsTouching = false; continue; @@ -206,7 +208,20 @@ public sealed partial class PhysicsSystem var uidB = contact.EntityB; var bodyATransform = GetPhysicsTransform(uidA, xformQuery.GetComponent(uidA)); var bodyBTransform = GetPhysicsTransform(uidB, xformQuery.GetComponent(uidB)); + var wasTouching = contact.IsTouching; + contact.UpdateIsTouching(bodyATransform, bodyBTransform); + var points = new FixedArray4(); + + // Need to re-run the event otherwise we skip the StartCollideEvent running again later in the physics step. + if (!wasTouching && contact.IsTouching) + { + contact.GetWorldManifold(bodyATransform, bodyBTransform, out var worldNormal, points.AsSpan); + // Use the 3rd Vector2 as the world normal, 4th is blank. + points._02 = worldNormal; + } + + RunContactEvents(Contact.GetContactStatus(contact, wasTouching), contact, points); } ArrayPool.Shared.Return(contacts); diff --git a/Robust.Client/Placement/IPlacementManager.cs b/Robust.Client/Placement/IPlacementManager.cs index 1a2f99c78..15b8f0916 100644 --- a/Robust.Client/Placement/IPlacementManager.cs +++ b/Robust.Client/Placement/IPlacementManager.cs @@ -8,6 +8,7 @@ using Robust.Shared.Timing; namespace Robust.Client.Placement { + [NotContentImplementable] public interface IPlacementManager { void Initialize(); diff --git a/Robust.Client/Player/IPlayerManager.cs b/Robust.Client/Player/IPlayerManager.cs index cb3b2cc00..5125f7673 100644 --- a/Robust.Client/Player/IPlayerManager.cs +++ b/Robust.Client/Player/IPlayerManager.cs @@ -7,6 +7,7 @@ using Robust.Shared.Player; namespace Robust.Client.Player; +[NotContentImplementable] public interface IPlayerManager : ISharedPlayerManager { /// diff --git a/Robust.Client/Properties/AssemblyInfo.cs b/Robust.Client/Properties/AssemblyInfo.cs index 21c92e6b0..34c45d7d7 100644 --- a/Robust.Client/Properties/AssemblyInfo.cs +++ b/Robust.Client/Properties/AssemblyInfo.cs @@ -5,6 +5,8 @@ [assembly: InternalsVisibleTo("Robust.Lite")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: InternalsVisibleTo("Robust.Benchmarks")] +[assembly: InternalsVisibleTo("Robust.Client.Tests")] +[assembly: InternalsVisibleTo("Robust.Client.IntegrationTests")] #if NET5_0_OR_GREATER [module: SkipLocalsInit] diff --git a/Robust.Client/Replays/Loading/IReplayFileReader.cs b/Robust.Client/Replays/Loading/IReplayFileReader.cs index 25608bbba..1b977c679 100644 --- a/Robust.Client/Replays/Loading/IReplayFileReader.cs +++ b/Robust.Client/Replays/Loading/IReplayFileReader.cs @@ -10,6 +10,7 @@ namespace Robust.Client.Replays.Loading; /// /// Simple interface that the replay system loads files from. /// +[NotContentImplementable] public interface IReplayFileReader : IDisposable { /// diff --git a/Robust.Client/Replays/Loading/IReplayLoadManager.cs b/Robust.Client/Replays/Loading/IReplayLoadManager.cs index fd8ca127f..e617e9c52 100644 --- a/Robust.Client/Replays/Loading/IReplayLoadManager.cs +++ b/Robust.Client/Replays/Loading/IReplayLoadManager.cs @@ -10,6 +10,7 @@ using Robust.Shared.Utility; namespace Robust.Client.Replays.Loading; +[NotContentImplementable] public interface IReplayLoadManager { public void Initialize(); diff --git a/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs b/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs index 3793f8140..12164f872 100644 --- a/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs +++ b/Robust.Client/Replays/Playback/IReplayPlaybackManager.cs @@ -9,6 +9,7 @@ using Robust.Shared.Serialization.Markdown.Mapping; namespace Robust.Client.Replays.Playback; +[NotContentImplementable] public interface IReplayPlaybackManager { public const string PlayCommand = "replay_play"; diff --git a/Robust.Client/ResourceManagement/IResourceCache.cs b/Robust.Client/ResourceManagement/IResourceCache.cs index dd13909bb..16a2532c7 100644 --- a/Robust.Client/ResourceManagement/IResourceCache.cs +++ b/Robust.Client/ResourceManagement/IResourceCache.cs @@ -11,6 +11,7 @@ namespace Robust.Client.ResourceManagement; /// /// Handles caching of /// +[NotContentImplementable] public interface IResourceCache : IResourceManager { T GetResource(string path, bool useFallback = true) diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index c1df15655..c99c8a4f4 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -27,6 +27,10 @@ + + + + @@ -42,7 +46,7 @@ - + @@ -70,6 +74,11 @@ + + + + + diff --git a/Robust.Client/Serialization/IClientRobustSerializer.cs b/Robust.Client/Serialization/IClientRobustSerializer.cs index 0dbfa7c92..c9c458b76 100644 --- a/Robust.Client/Serialization/IClientRobustSerializer.cs +++ b/Robust.Client/Serialization/IClientRobustSerializer.cs @@ -2,6 +2,7 @@ using Robust.Shared.Serialization; namespace Robust.Client.Serialization; +[NotContentImplementable] public interface IClientRobustSerializer : IRobustSerializer { /// diff --git a/Robust.Client/State/IStateManager.cs b/Robust.Client/State/IStateManager.cs index 79e27e4a0..f0e40fb3b 100644 --- a/Robust.Client/State/IStateManager.cs +++ b/Robust.Client/State/IStateManager.cs @@ -3,6 +3,7 @@ using Robust.Shared.Timing; namespace Robust.Client.State { + [NotContentImplementable] public interface IStateManager { event Action OnStateChanged; diff --git a/Robust.Client/Timing/IClientGameTiming.cs b/Robust.Client/Timing/IClientGameTiming.cs index 60f8625af..31c28591e 100644 --- a/Robust.Client/Timing/IClientGameTiming.cs +++ b/Robust.Client/Timing/IClientGameTiming.cs @@ -3,6 +3,7 @@ using Robust.Shared.Timing; namespace Robust.Client.Timing { + [NotContentImplementable] public interface IClientGameTiming : IGameTiming { /// diff --git a/Robust.Client/UserInterface/Axis.cs b/Robust.Client/UserInterface/Axis.cs index 0459eda78..7f681f2ac 100644 --- a/Robust.Client/UserInterface/Axis.cs +++ b/Robust.Client/UserInterface/Axis.cs @@ -52,6 +52,7 @@ public enum Axis : byte /// /// /// +[NotContentImplementable] public interface IAxisImplementation { // diff --git a/Robust.Client/UserInterface/Control.Styling.cs b/Robust.Client/UserInterface/Control.Styling.cs index aff99ddbc..64cae4101 100644 --- a/Robust.Client/UserInterface/Control.Styling.cs +++ b/Robust.Client/UserInterface/Control.Styling.cs @@ -19,6 +19,9 @@ namespace Robust.Client.UserInterface get => _stylesheet; set { + if (ReferenceEquals(_stylesheet, value)) + return; + _stylesheet = value; StylesheetUpdateRecursive(); } diff --git a/Robust.Client/UserInterface/Control.cs b/Robust.Client/UserInterface/Control.cs index 53d4e8b8f..546635abe 100644 --- a/Robust.Client/UserInterface/Control.cs +++ b/Robust.Client/UserInterface/Control.cs @@ -11,6 +11,7 @@ using Robust.Client.UserInterface.Themes; using Robust.Client.UserInterface.XAML; using Robust.Shared.Animations; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -74,6 +75,8 @@ namespace Robust.Client.UserInterface // _nameScope = nameScope; //} + public virtual ISawmill Log => UserInterfaceManager.ControlSawmill; + public UITheme Theme { get; internal set; } private UITheme? _themeOverride; @@ -381,8 +384,6 @@ namespace Robust.Client.UserInterface /// public event EventHandler? OnShowTooltip; - - /// /// If this control is currently showing a tooltip provided via TooltipSupplier, /// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot. diff --git a/Robust.Client/UserInterface/Controls/AspectRatioPanel.cs b/Robust.Client/UserInterface/Controls/AspectRatioPanel.cs new file mode 100644 index 000000000..895fd3625 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/AspectRatioPanel.cs @@ -0,0 +1,57 @@ +using System.Numerics; +using Robust.Shared.Maths; + +namespace Robust.Client.UserInterface.Controls; + +// TODO BEFORE MAKING PUBLIC: +// Do we need to constrain the aspect ratio in MeasureOverride() too? +// Doc comments + +/// +/// A simple UI control that ensures its children are laid out with a fixed aspect ratio. +/// +internal sealed class AspectRatioPanel : Control +{ + public AspectRatio AspectRatio + { + get; + set + { + field = value; + InvalidateArrange(); + } + } = AspectRatio.One; + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + var givenRatio = finalSize.X / finalSize.Y; + + if (!MathHelper.CloseTo(givenRatio, AspectRatio.Ratio)) + { + if (givenRatio < AspectRatio.Ratio) + { + // Too narrow, derive height from width. + finalSize = finalSize with { Y = finalSize.X / AspectRatio.Ratio }; + } + else + { + // Too wide, derive width from height. + finalSize = finalSize with { X = finalSize.Y * AspectRatio.Ratio }; + } + } + + return base.ArrangeOverride(finalSize); + } +} + +internal struct AspectRatio(float ratio) +{ + public static readonly AspectRatio One = new(1); + + public float Ratio = ratio; + + public static AspectRatio FromWidthHeight(float width, float height) + { + return new AspectRatio(width / height); + } +} diff --git a/Robust.Client/UserInterface/Controls/BaseButton.cs b/Robust.Client/UserInterface/Controls/BaseButton.cs index 38027733b..d590b9619 100644 --- a/Robust.Client/UserInterface/Controls/BaseButton.cs +++ b/Robust.Client/UserInterface/Controls/BaseButton.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Robust.Client.Audio; @@ -116,7 +116,7 @@ namespace Robust.Client.UserInterface.Controls _pressed = value; - if (Group != null) + if (Group != null && _pressed) { UnsetOtherGroupButtons(); } @@ -318,7 +318,10 @@ namespace Robust.Client.UserInterface.Controls if (args.Function == EngineKeyFunctions.UIClick && ToggleMode) { OnToggled?.Invoke(new ButtonToggledEventArgs(Pressed, this, args)); - UnsetOtherGroupButtons(); + if (_pressed) + { + UnsetOtherGroupButtons(); + } } } } diff --git a/Robust.Client/UserInterface/Controls/Label.cs b/Robust.Client/UserInterface/Controls/Label.cs index 3e0ee262a..539ed1b14 100644 --- a/Robust.Client/UserInterface/Controls/Label.cs +++ b/Robust.Client/UserInterface/Controls/Label.cs @@ -27,6 +27,7 @@ namespace Robust.Client.UserInterface.Controls private ReadOnlyMemory _textMemory; private bool _clipText; private AlignMode _align; + private Font? _fontOverride; public Label() { @@ -106,7 +107,16 @@ namespace Robust.Client.UserInterface.Controls [ViewVariables] public VAlignMode VAlign { get; set; } - public Font? FontOverride { get; set; } + public Font? FontOverride + { + get => _fontOverride; + set + { + _fontOverride = value; + _textDimensionCacheValid = false; + InvalidateMeasure(); + } + } private Font ActualFont { diff --git a/Robust.Client/UserInterface/Controls/OptionButton.cs b/Robust.Client/UserInterface/Controls/OptionButton.cs index 0b8af9605..bbcb49771 100644 --- a/Robust.Client/UserInterface/Controls/OptionButton.cs +++ b/Robust.Client/UserInterface/Controls/OptionButton.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Numerics; using Robust.Client.Graphics; -using Robust.Shared.Graphics; +using Robust.Shared.Localization; using Robust.Shared.Maths; using static Robust.Client.UserInterface.Controls.BoxContainer; @@ -20,9 +20,10 @@ namespace Robust.Client.UserInterface.Controls private readonly List _buttonData = new(); private readonly Dictionary _idMap = new(); private readonly Popup _popup; - private readonly BoxContainer _popupVBox; + private readonly BoxContainer _popupContentsBox; private readonly Label _label; private readonly TextureRect _triangle; + private readonly LineEdit _filterBox; public int ItemCount => _buttonData.Count; @@ -39,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls } } private bool _hideTriangle; + private bool _filterable; /// /// StyleClasses to apply to the options that popup when clicking this button. @@ -50,6 +52,17 @@ namespace Robust.Client.UserInterface.Controls public string Prefix { get; set; } = string.Empty; public bool PrefixMargin { get; set; } = true; + public bool Filterable + { + get => _filterable; + set + { + _filterable = value; + _filterBox.Visible = value; + UpdateFilters(); + } + } + public OptionButton() { OptionStyleClasses = new List(); @@ -62,14 +75,26 @@ namespace Robust.Client.UserInterface.Controls }; AddChild(hBox); - _popupVBox = new BoxContainer + var popupVBox = new BoxContainer { - Orientation = LayoutOrientation.Vertical + Orientation = LayoutOrientation.Vertical, Children = + { + (_filterBox = new LineEdit + { + PlaceHolder = Loc.GetString("option-button-filter"), + SelectAllOnFocus = true, + Visible = false, + }), + (_popupContentsBox = new BoxContainer + { + Orientation = LayoutOrientation.Vertical + }) + } }; OptionsScroll = new() { - Children = { _popupVBox }, + Children = { popupVBox }, ReturnMeasure = true, MaxHeight = 300 }; @@ -100,6 +125,11 @@ namespace Robust.Client.UserInterface.Controls Visible = !HideTriangle }; hBox.AddChild(_triangle); + + _filterBox.OnTextChanged += _ => + { + UpdateFilters(); + }; } public void AddItem(Texture icon, string label, int? id = null) @@ -140,13 +170,14 @@ namespace Robust.Client.UserInterface.Controls }; _idMap.Add(id.Value, _buttonData.Count); _buttonData.Add(data); - _popupVBox.AddChild(button); + _popupContentsBox.AddChild(button); if (_buttonData.Count == 1) { Select(0); } ButtonOverride(button); + UpdateFilter(data); } private void TogglePopup(bool show) @@ -164,6 +195,9 @@ namespace Robust.Client.UserInterface.Controls var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY)); Root.ModalRoot.AddChild(_popup); _popup.Open(box); + + if (_filterable) + _filterBox.GrabKeyboardFocus(); } else { @@ -201,7 +235,7 @@ namespace Robust.Client.UserInterface.Controls buttonDatum.Button.OnPressed -= ButtonOnPressed; } _buttonData.Clear(); - _popupVBox.DisposeAllChildren(); + _popupContentsBox.DisposeAllChildren(); SelectedId = 0; } @@ -229,7 +263,7 @@ namespace Robust.Client.UserInterface.Controls var data = _buttonData[idx]; data.Button.OnPressed -= ButtonOnPressed; _idMap.Remove(data.Id); - _popupVBox.RemoveChild(data.Button); + _popupContentsBox.RemoveChild(data.Button); _buttonData.RemoveAt(idx); var newIdx = 0; foreach (var buttonData in _buttonData) @@ -330,6 +364,25 @@ namespace Robust.Client.UserInterface.Controls TogglePopup(false); } + private void UpdateFilters() + { + foreach (var entry in _buttonData) + { + UpdateFilter(entry); + } + } + + private void UpdateFilter(ButtonData data) + { + if (!_filterable) + { + data.Button.Visible = true; + return; + } + + data.Button.Visible = data.Text.Contains(_filterBox.Text, StringComparison.CurrentCultureIgnoreCase); + } + public sealed class ItemSelectedEventArgs : EventArgs { public OptionButton Button { get; } diff --git a/Robust.Client/UserInterface/Controls/OutputPanel.cs b/Robust.Client/UserInterface/Controls/OutputPanel.cs index d68cd9a36..e0d1624bf 100644 --- a/Robust.Client/UserInterface/Controls/OutputPanel.cs +++ b/Robust.Client/UserInterface/Controls/OutputPanel.cs @@ -136,9 +136,14 @@ namespace Robust.Client.UserInterface.Controls AddMessage(msg); } - public void AddMessage(FormattedMessage message) + public void AddMessage(FormattedMessage message, Color? defaultColor = null) { - var entry = new RichTextEntry(message, this, _tagManager, null); + AddMessage(message, RichTextEntry.DefaultTags, defaultColor); + } + + public void AddMessage(FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null) + { + var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor); entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale); @@ -153,7 +158,12 @@ namespace Robust.Client.UserInterface.Controls } } - public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null) + public void SetMessage(Index index, FormattedMessage message, Color? defaultColor = null) + { + SetMessage(index, message, RichTextEntry.DefaultTags, defaultColor); + } + + public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null) { var atBottom = !_scrollDownButton.Visible; var oldEntry = _entries[index]; @@ -266,7 +276,7 @@ namespace Robust.Client.UserInterface.Controls return _getStyleBox()?.MinimumSize ?? Vector2.Zero; } - private void _invalidateEntries() + internal void _invalidateEntries() { _totalContentHeight = 0; var font = _getFont(); @@ -336,6 +346,14 @@ namespace Robust.Client.UserInterface.Controls base.UIScaleChanged(); } + protected override void StylePropertiesChanged() + { + base.StylePropertiesChanged(); + + // Font may have changed. + _invalidateEntries(); + } + internal static float GetScrollSpeed(Font font, float scale) { return font.GetLineHeight(scale) * 2; diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index 755948464..a9fbe0c30 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -67,14 +67,24 @@ namespace Robust.Client.UserInterface.Controls VerticalAlignment = VAlignment.Center; } - public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null) + public void SetMessage(FormattedMessage message, Color? defaultColor = null) + { + SetMessage(message, RichTextEntry.DefaultTags, defaultColor); + } + + public void SetMessage(FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null) { _entry?.RemoveControls(); _entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor); InvalidateMeasure(); } - public void SetMessage(string message, Type[]? tagsAllowed = null, Color? defaultColor = null) + public void SetMessage(string message, Color? defaultColor = null) + { + SetMessage(message, RichTextEntry.DefaultTags, defaultColor); + } + + public void SetMessage(string message, Type[]? tagsAllowed, Color? defaultColor = null) { var msg = new FormattedMessage(); msg.AddText(message); diff --git a/Robust.Client/UserInterface/DummyFileDialogManager.cs b/Robust.Client/UserInterface/DummyFileDialogManager.cs deleted file mode 100644 index 29026fa69..000000000 --- a/Robust.Client/UserInterface/DummyFileDialogManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO; -using System.Threading.Tasks; - -namespace Robust.Client.UserInterface -{ - /// - /// Treats ever file dialog operation as cancelled. - /// - internal sealed class DummyFileDialogManager : IFileDialogManager - { - public Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) - { - return Task.FromResult(null); - } - - public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile( - FileDialogFilters? filters = null, - bool truncate = true, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None) - { - return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null); - } - } -} diff --git a/Robust.Client/UserInterface/FileDialogManager.cs b/Robust.Client/UserInterface/FileDialogManager.cs index 70366dd8a..7921c38f8 100644 --- a/Robust.Client/UserInterface/FileDialogManager.cs +++ b/Robust.Client/UserInterface/FileDialogManager.cs @@ -1,433 +1,77 @@ using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using Robust.Client.Graphics; using Robust.Shared.Console; using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Utility; -namespace Robust.Client.UserInterface +namespace Robust.Client.UserInterface; + +internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManager { - [SuppressMessage("ReSharper", "InconsistentNaming")] - [SuppressMessage("ReSharper", "IdentifierTypo")] - [SuppressMessage("ReSharper", "CommentTypo")] - [SuppressMessage("ReSharper", "StringLiteralTypo")] - internal sealed class FileDialogManager : IFileDialogManager, IPostInjectInit + public async Task OpenFile( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) { - // Uses nativefiledialog to open the file dialogs cross platform. - // On Linux, if the kdialog command is found, it will be used instead. - // TODO: Should we maybe try to avoid running kdialog if the DE isn't KDE? - [Dependency] private readonly IClydeInternal _clyde = default!; - [Dependency] private readonly ILogManager _log = default!; + if ((access & FileAccess.ReadWrite) != access) + throw new ArgumentException("Invalid file access specified"); - private ISawmill _sawmill = default!; + var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None); + if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare) + throw new ArgumentException("Invalid file share specified"); - private bool _kDialogAvailable; - private bool _checkedKDialogAvailable; + string? name; + if (clyde.FileDialogImpl is { } clydeImpl) + name = await clydeImpl.OpenFile(filters); + else + return null; - public async Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) - { - if ((access & FileAccess.ReadWrite) != access) - throw new ArgumentException("Invalid file access specified"); + if (name == null) + return null; - var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None); - if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare) - throw new ArgumentException("Invalid file share specified"); - - string? name; - if (_clyde.FileDialogImpl is { } clydeImpl) - name = await clydeImpl.OpenFile(filters); - else - name = await GetOpenFileName(filters); - - if (name == null) - return null; - - return File.Open(name, FileMode.Open, access, realShare); - } - - private async Task GetOpenFileName(FileDialogFilters? filters) - { - if (await IsKDialogAvailable()) - { - return await OpenFileKDialog(filters); - } - - return await OpenFileNfd(filters); - } - - public async Task<(Stream, bool)?> SaveFile( - FileDialogFilters? filters, - bool truncate = true, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None) - { - if ((access & FileAccess.ReadWrite) != access) - throw new ArgumentException("Invalid file access specified"); - - if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share) - throw new ArgumentException("Invalid file share specified"); - - string? name; - if (_clyde.FileDialogImpl is { } clydeImpl) - name = await clydeImpl.SaveFile(filters); - else - name = await GetSaveFileName(filters); - - if (name == null) - return null; - - try - { - return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true); - } - catch (FileNotFoundException) - { - return (File.Open(name, FileMode.Create, access, share), false); - } - } - - private async Task GetSaveFileName(FileDialogFilters? filters) - { - if (await IsKDialogAvailable()) - { - return await SaveFileKDialog(filters); - } - - return await SaveFileNfd(filters); - } - - private unsafe Task OpenFileNfd(FileDialogFilters? filters) - { - // Have to run it in the thread pool to avoid blocking the main thread. - return RunAsyncMaybe(() => - { - byte* outPath; - - var filterPtr = FormatFiltersNfd(filters); - - sw_nfdresult result; - - try - { - result = sw_NFD_OpenDialog((byte*)filterPtr, null, &outPath); - } - finally - { - if (filterPtr != IntPtr.Zero) - { - Marshal.FreeCoTaskMem(filterPtr); - } - } - - return HandleNfdResult(result, outPath); - }); - } - - private static IntPtr FormatFiltersNfd(FileDialogFilters? filters) - { - if (filters != null) - { - var filterString = string.Join(';', filters.Groups.Select(f => string.Join(',', f.Extensions))); - - return Marshal.StringToCoTaskMemUTF8(filterString); - } - - return IntPtr.Zero; - } - - private unsafe Task SaveFileNfd(FileDialogFilters? filters) - { - // Have to run it in the thread pool to avoid blocking the main thread. - return RunAsyncMaybe(() => - { - byte* outPath; - - var filterPtr = FormatFiltersNfd(filters); - - sw_nfdresult result; - try - { - result = sw_NFD_SaveDialog((byte*) filterPtr, null, &outPath); - } - finally - { - if (filterPtr != IntPtr.Zero) - { - Marshal.FreeCoTaskMem(filterPtr); - } - } - - return HandleNfdResult(result, outPath); - }); - } - - /* - private unsafe Task OpenFolderNfd() - { - // Have to run it in the thread pool to avoid blocking the main thread. - return RunAsyncMaybe(() => - { - byte* outPath; - - var result = sw_NFD_PickFolder(null, &outPath); - - return HandleNfdResult(result, outPath); - }); - } - */ - - // ReSharper disable once MemberCanBeMadeStatic.Local - private Task RunAsyncMaybe(Func action) - { - if (OperatingSystem.IsMacOS()) - { - // macOS seems pretty annoying about having the file dialog opened from the main windowing thread. - // So we are forced to execute this synchronously on the main windowing thread. - // nativefiledialog doesn't provide any form of async API, so this WILL lock up half the client. - var tcs = new TaskCompletionSource(); - _clyde.RunOnWindowThread(() => tcs.SetResult(action())); - - return tcs.Task; - } - else - { - // Luckily, GTK Linux and COM Windows are both happily threaded. Yay! - // * Actual attempts to have multiple file dialogs up at the same time, and the resulting crashes, - // have shown that at least for GTK+ (Linux), just because it can handle being on any thread doesn't mean it handle being on two at the same time. - // Testing system was Ubuntu 20.04. - // COM on Windows might handle this, but honestly, who exactly wants to risk it? - // In particular this could very well be an swnfd issue. - return Task.Run(() => - { - lock (this) - { - return action(); - } - }); - } - } - - private static unsafe string? HandleNfdResult(sw_nfdresult result, byte* outPath) - { - switch (result) - { - case sw_nfdresult.SW_NFD_ERROR: - var errPtr = sw_NFD_GetError(); - throw new Exception(Marshal.PtrToStringUTF8((IntPtr) errPtr)); - - case sw_nfdresult.SW_NFD_OKAY: - var str = Marshal.PtrToStringUTF8((IntPtr) outPath)!; - - sw_NFD_Free(outPath); - return str; - - case sw_nfdresult.SW_NFD_CANCEL: - return null; - - default: - throw new ArgumentOutOfRangeException(); - } - } - - private async Task CheckKDialogSupport() - { - var currentDesktop = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP"); - if (currentDesktop == null || !currentDesktop.Contains("KDE")) - { - return; - } - - try - { - var process = Process.Start( - new ProcessStartInfo - { - FileName = "kdialog", - Arguments = "--version", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }); - - if (process == null) - { - _kDialogAvailable = false; - return; - } - - await process.WaitForExitAsync(); - _kDialogAvailable = process.ExitCode == 0; - - if (_kDialogAvailable) - { - _sawmill.Debug("kdialog available."); - } - } - catch - { - _kDialogAvailable = false; - } - } - - private Task OpenFileKDialog(FileDialogFilters? filters) - { - var filtersFormatted = FormatFiltersKDialog(filters); - - return RunKDialog("--getopenfilename", Environment.GetEnvironmentVariable("HOME")!, filtersFormatted); - } - - private static string FormatFiltersKDialog(FileDialogFilters? filters) - { - var sb = new StringBuilder(); - - if (filters != null && filters.Groups.Count != 0) - { - var first = true; - foreach (var group in filters.Groups) - { - if (!first) - { - sb.Append('|'); - } - - foreach (var extension in @group.Extensions) - { - sb.AppendFormat(".{0} ", extension); - } - - sb.Append('('); - - foreach (var extension in @group.Extensions) - { - sb.AppendFormat("*.{0} ", extension); - } - - sb.Append(')'); - - first = false; - } - - sb.Append("| All Files (*)"); - } - - return sb.ToString(); - } - - private Task SaveFileKDialog(FileDialogFilters? filters) - { - var filtersFormatted = FormatFiltersKDialog(filters); - - return RunKDialog("--getsavefilename", Environment.GetEnvironmentVariable("HOME")!, filtersFormatted); - } - - /* - private Task OpenFolderKDialog() - { - return RunKDialog("--getexistingdirectory"); - } - */ - - private async Task RunKDialog(params string[] options) - { - var startInfo = new ProcessStartInfo - { - FileName = "kdialog", - RedirectStandardOutput = true, - UseShellExecute = false, - StandardOutputEncoding = EncodingHelpers.UTF8 - }; - - foreach (var option in options) - { - startInfo.ArgumentList.Add(option); - } - - if (_clyde.GetX11WindowId() is { } id) - { - startInfo.ArgumentList.Add("--attach"); - startInfo.ArgumentList.Add(id.ToString()); - } - - var process = Process.Start(startInfo); - - DebugTools.AssertNotNull(process); - - await process!.WaitForExitAsync(); - - // Cancel hit. - if (process.ExitCode == 1) - { - return null; - } - - return (await process.StandardOutput.ReadLineAsync())?.Trim(); - } - - private async Task IsKDialogAvailable() - { - if (!OperatingSystem.IsLinux()) - return false; - - if (!_checkedKDialogAvailable) - { - await CheckKDialogSupport(); - _checkedKDialogAvailable = true; - } - - return _kDialogAvailable; - } - - [DllImport("swnfd.dll")] - private static extern unsafe byte* sw_NFD_GetError(); - - [DllImport("swnfd.dll")] - private static extern unsafe sw_nfdresult - sw_NFD_OpenDialog(byte* filterList, byte* defaultPath, byte** outPath); - - [DllImport("swnfd.dll")] - private static extern unsafe sw_nfdresult - sw_NFD_SaveDialog(byte* filterList, byte* defaultPath, byte** outPath); - - /* - [DllImport("swnfd.dll")] - private static extern unsafe sw_nfdresult - sw_NFD_PickFolder(byte* defaultPath, byte** outPath); - */ - - [DllImport("swnfd.dll")] - private static extern unsafe void sw_NFD_Free(void* ptr); - - public void PostInject() - { - _sawmill = _log.GetSawmill("filedialog"); - } - - private enum sw_nfdresult - { - SW_NFD_ERROR, - SW_NFD_OKAY, - SW_NFD_CANCEL, - } + return File.Open(name, FileMode.Open, access, realShare); } - public sealed class OpenFileCommand : LocalizedCommands + public async Task<(Stream, bool)?> SaveFile( + FileDialogFilters? filters, + bool truncate = true, + FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.None) { - public override string Command => "testopenfile"; + if ((access & FileAccess.ReadWrite) != access) + throw new ArgumentException("Invalid file access specified"); - public override async void Execute(IConsoleShell shell, string argStr, string[] args) + if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share) + throw new ArgumentException("Invalid file share specified"); + + string? name; + if (clyde.FileDialogImpl is { } clydeImpl) + name = await clydeImpl.SaveFile(filters); + else + return null; + + if (name == null) + return null; + + try { - var stream = await IoCManager.Resolve().OpenFile(); - stream?.Dispose(); + return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true); + } + catch (FileNotFoundException) + { + return (File.Open(name, FileMode.Create, access, share), false); } } } + +public sealed class OpenFileCommand : LocalizedCommands +{ + public override string Command => "testopenfile"; + + public override async void Execute(IConsoleShell shell, string argStr, string[] args) + { + var stream = await IoCManager.Resolve().OpenFile(); + stream?.Dispose(); + } +} diff --git a/Robust.Client/UserInterface/IClipboardManager.cs b/Robust.Client/UserInterface/IClipboardManager.cs index 7059102ca..0bd9921a2 100644 --- a/Robust.Client/UserInterface/IClipboardManager.cs +++ b/Robust.Client/UserInterface/IClipboardManager.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; namespace Robust.Client.UserInterface { + [NotContentImplementable] public interface IClipboardManager { Task GetText(); diff --git a/Robust.Client/UserInterface/IDebugMonitors.cs b/Robust.Client/UserInterface/IDebugMonitors.cs index 7c36596c0..5e4af4dea 100644 --- a/Robust.Client/UserInterface/IDebugMonitors.cs +++ b/Robust.Client/UserInterface/IDebugMonitors.cs @@ -3,6 +3,7 @@ namespace Robust.Client.UserInterface; /// /// Manages the debug monitors overlay, AKA "F3 screen". /// +[NotContentImplementable] public interface IDebugMonitors { /// diff --git a/Robust.Client/UserInterface/IFileDialogManager.cs b/Robust.Client/UserInterface/IFileDialogManager.cs index 15061b2a4..ab0ef06ea 100644 --- a/Robust.Client/UserInterface/IFileDialogManager.cs +++ b/Robust.Client/UserInterface/IFileDialogManager.cs @@ -11,6 +11,7 @@ namespace Robust.Client.UserInterface /// File dialogs are native to the OS being ran on. /// All operations are asynchronous to prevent locking up the main thread while the user makes his pick. /// + [NotContentImplementable] public interface IFileDialogManager { /// diff --git a/Robust.Client/UserInterface/IUserInterfaceManager.cs b/Robust.Client/UserInterface/IUserInterfaceManager.cs index d11316264..0ad4eb299 100644 --- a/Robust.Client/UserInterface/IUserInterfaceManager.cs +++ b/Robust.Client/UserInterface/IUserInterfaceManager.cs @@ -6,11 +6,13 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared; using Robust.Shared.Audio.Sources; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; namespace Robust.Client.UserInterface { + [NotContentImplementable] public partial interface IUserInterfaceManager { void InitializeTesting(); @@ -157,6 +159,14 @@ namespace Robust.Client.UserInterface /// Render a control and all of its children. /// void RenderControl(IRenderHandle handle, Control control, Vector2i position); + + /// + /// Sawmill for use by controls. + /// + /// + /// Exists so that control don't have to inject dependencies or otherwise obtain an instance just to log errors. + /// + ISawmill ControlSawmill { get; } } public readonly struct PostDrawUIRootEventArgs diff --git a/Robust.Client/UserInterface/RichText/FontPrototype.cs b/Robust.Client/UserInterface/RichText/FontPrototype.cs index 847958fc5..f75f3deaf 100644 --- a/Robust.Client/UserInterface/RichText/FontPrototype.cs +++ b/Robust.Client/UserInterface/RichText/FontPrototype.cs @@ -1,3 +1,4 @@ +using System; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Utility; @@ -10,6 +11,7 @@ public sealed partial class FontPrototype : IPrototype [IdDataField] public string ID { get; private set; } = default!; + [Obsolete("Font prototype is a bad API.")] [DataField("path", required: true)] public ResPath Path { get; private set; } = default!; } diff --git a/Robust.Client/UserInterface/RichText/FontTag.cs b/Robust.Client/UserInterface/RichText/FontTag.cs index 157664c7f..8043c3484 100644 --- a/Robust.Client/UserInterface/RichText/FontTag.cs +++ b/Robust.Client/UserInterface/RichText/FontTag.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; @@ -40,12 +41,31 @@ public sealed class FontTag : IMarkupTagHandler /// Creates the a vector font from the supplied font id.
/// The size of the resulting font will be either the size supplied as a parameter to the tag, the previous font size or 12 ///
+ [Obsolete("Stop using font prototypes")] public static Font CreateFont( Stack contextFontStack, MarkupNode node, IResourceCache cache, IPrototypeManager prototypeManager, string fontId) + { + var size = GetSizeForFontTag(contextFontStack, node); + + var hijack = IoCManager.Resolve(); + if (hijack.Hijack?.Invoke(fontId, size) is { } overriden) + return overriden; + + if (!prototypeManager.TryIndex(fontId, out var prototype)) + prototype = prototypeManager.Index(DefaultFont); + + var fontResource = cache.GetResource(prototype.Path); + return new VectorFont(fontResource, size); + } + + /// + /// Get the desired font size for the given markup node. + /// + public static int GetSizeForFontTag(Stack contextFontStack, MarkupNode node) { var size = DefaultSize; @@ -68,10 +88,6 @@ public sealed class FontTag : IMarkupTagHandler if (node.Attributes.TryGetValue("size", out var sizeParameter)) size = (int) (sizeParameter.LongValue ?? size); - if (!prototypeManager.TryIndex(fontId, out var prototype)) - prototype = prototypeManager.Index(DefaultFont); - - var fontResource = cache.GetResource(prototype.Path); - return new VectorFont(fontResource, size); + return size; } } diff --git a/Robust.Client/UserInterface/RichText/FontTagHijackHolder.cs b/Robust.Client/UserInterface/RichText/FontTagHijackHolder.cs new file mode 100644 index 000000000..23ce0f9cb --- /dev/null +++ b/Robust.Client/UserInterface/RichText/FontTagHijackHolder.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Robust.Client.UserInterface.RichText; + +/// The font to replace the lookup with. Return null to fall back to default behavior. +/// +public delegate Font? FontTagHijack(ProtoId protoId, int size); + +/// +/// Allows replacing font resolution done by +/// +public sealed class FontTagHijackHolder +{ + [Dependency] private readonly IUserInterfaceManager _ui = null!; + + /// + /// Called when a font prototype gets resolved. + /// + public FontTagHijack? Hijack; + + /// + /// Indicate that the results of may have changed, + /// and that engine things relying on it must be updated. + /// + public void HijackUpdated() + { + // This isn't fool-proof, but it's probably good enough. + // Recursively navigate the UI tree and invalidate rich text controls. + var queue = new Queue(); + + foreach (var root in _ui.AllRoots) + { + queue.Enqueue(root); + } + + while (queue.TryDequeue(out var control)) + { + foreach (var child in control.Children) + { + queue.Enqueue(child); + } + + if (control is OutputPanel output) + output._invalidateEntries(); + else if (control is RichTextLabel label) + label.InvalidateMeasure(); + } + } +} diff --git a/Robust.Client/UserInterface/RichText/MarkupTagManager.cs b/Robust.Client/UserInterface/RichText/MarkupTagManager.cs index 107fd4046..93667a7c5 100644 --- a/Robust.Client/UserInterface/RichText/MarkupTagManager.cs +++ b/Robust.Client/UserInterface/RichText/MarkupTagManager.cs @@ -12,11 +12,13 @@ public sealed class MarkupTagManager { [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly ISandboxHelper _sandboxHelper = default!; + [Dependency] private readonly IDependencyCollection _deps = default!; /// /// Tags defined in engine need to be instantiated here because of sandboxing /// - private readonly Dictionary _markupTagTypes = new IMarkupTagHandler[] { + private readonly Dictionary _markupTagTypes = new IMarkupTagHandler[] + { new BoldItalicTag(), new BoldTag(), new BulletTag(), @@ -50,13 +52,13 @@ public sealed class MarkupTagManager if (_engineTypes.Contains(type)) continue; - var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type); + var instance = (IMarkupTagHandler) _sandboxHelper.CreateInstance(type); _markupTagTypes[instance.Name.ToLower()] = instance; } - foreach (var (_, tag) in _markupTagTypes) + foreach (var tag in _markupTagTypes.Values) { - IoCManager.InjectDependencies(tag); + _deps.InjectDependencies(tag); } } diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 0e8898ef0..e21c23833 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -19,6 +19,16 @@ namespace Robust.Client.UserInterface ///
internal struct RichTextEntry { + public static readonly Type[] DefaultTags = + [ + typeof(BoldItalicTag), + typeof(BoldTag), + typeof(BulletTag), + typeof(ColorTag), + typeof(HeadingTag), + typeof(ItalicTag) + ]; + private readonly Color _defaultColor; private readonly Type[]? _tagsAllowed; @@ -41,7 +51,17 @@ namespace Robust.Client.UserInterface public readonly Dictionary? Controls; - public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null) + + public RichTextEntry( + FormattedMessage message, + Control parent, + MarkupTagManager tagManager, + Color? defaultColor = null) : this(message, parent, tagManager, DefaultTags, defaultColor) + { + // RichTextEntry constructor but with DefaultTags + } + + public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed, Color? defaultColor = null) { Message = message; Height = 0; @@ -49,9 +69,14 @@ namespace Robust.Client.UserInterface LineBreaks = default; _defaultColor = defaultColor ?? new(200, 200, 200); _tagsAllowed = tagsAllowed; - Dictionary? tagControls = null; + Controls = GetControls(parent, tagManager); + } + private readonly Dictionary? GetControls(Control parent, MarkupTagManager tagManager) + { + Dictionary? tagControls = null; var nodeIndex = -1; + foreach (var node in Message) { nodeIndex++; @@ -71,7 +96,7 @@ namespace Robust.Client.UserInterface tagControls.Add(nodeIndex, control); } - Controls = tagControls; + return tagControls; } // TODO RICH TEXT @@ -219,6 +244,8 @@ namespace Robust.Client.UserInterface var baseLine = drawBox.TopLeft + new Vector2(0, defaultFont.GetAscent(uiScale) + verticalOffset); var controlYAdvance = 0f; + var spaceRune = new Rune(' '); + var nodeIndex = -1; foreach (var node in Message) { @@ -232,16 +259,25 @@ namespace Robust.Client.UserInterface foreach (var rune in text.EnumerateRunes()) { + bool skipSpaceBaseline = false; + if (lineBreakIndex < LineBreaks.Count && LineBreaks[lineBreakIndex] == globalBreakCounter) { baseLine = new Vector2(drawBox.Left, baseLine.Y + GetLineHeight(font, uiScale, lineHeightScale) + controlYAdvance); controlYAdvance = 0; lineBreakIndex += 1; + + // The baseline calc is kind of messed up, the newline is After the space but the space is being drawn after doing the newline + // Which means if this metric Ends on a space, the next metric will use the wrong baseline when it starts, for some reason .. + if (rune == spaceRune) + skipSpaceBaseline = true; } var advance = font.DrawChar(handle, rune, baseLine, uiScale, color); - baseLine += new Vector2(advance, 0); + + if (!skipSpaceBaseline) + baseLine += new Vector2(advance, 0); globalBreakCounter += 1; } @@ -254,8 +290,13 @@ namespace Robust.Client.UserInterface control.Visible = true; var invertedScale = 1f / uiScale; - control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale); control.Measure(new Vector2(Width, Height)); + control.Arrange(UIBox2.FromDimensions( + baseLine.X * invertedScale, + (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale, + control.DesiredSize.X, + control.DesiredSize.Y + )); var advanceX = control.DesiredPixelSize.X; controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale); baseLine += new Vector2(advanceX, 0); diff --git a/Robust.Client/UserInterface/Themes/UiTheme.cs b/Robust.Client/UserInterface/Themes/UiTheme.cs index 50e16a161..d9aa118bd 100644 --- a/Robust.Client/UserInterface/Themes/UiTheme.cs +++ b/Robust.Client/UserInterface/Themes/UiTheme.cs @@ -34,7 +34,7 @@ public sealed partial class UITheme : IPrototype private ResPath _path; [DataField("colors", readOnly: true)] // This is a prototype, why is this readonly?? - public FrozenDictionary? Colors { get; } + public FrozenDictionary? Colors; public ResPath Path => _path == default ? new ResPath(DefaultPath+"/"+ID) : _path; private void ValidateFilePath(IResourceManager manager) diff --git a/Robust.Client/UserInterface/UriOpener.cs b/Robust.Client/UserInterface/UriOpener.cs index 6d1f2bbdf..948af6395 100644 --- a/Robust.Client/UserInterface/UriOpener.cs +++ b/Robust.Client/UserInterface/UriOpener.cs @@ -7,6 +7,7 @@ namespace Robust.Client.UserInterface /// /// Helper for opening s on the user's machine. /// + [NotContentImplementable] public interface IUriOpener { /// diff --git a/Robust.Client/UserInterface/UserInterfaceManager.Input.cs b/Robust.Client/UserInterface/UserInterfaceManager.Input.cs index b5e9a6c85..274aa2abc 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.Input.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.Input.cs @@ -475,7 +475,7 @@ internal partial class UserInterfaceManager // If we are in a focused control or doing a CanFocus, return true // So that InputManager doesn't propagate events to simulation. - if (!args.CanFocus && KeyboardFocused != null) + if (!args.CanFocus && OnIsUIFocused()) { return true; } @@ -483,6 +483,11 @@ internal partial class UserInterfaceManager return false; } + private bool OnIsUIFocused() + { + return KeyboardFocused != null; + } + /// public void GrabKeyboardFocus(Control control) { diff --git a/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs b/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs index 7e76ecc94..56d96c1c2 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs @@ -2,6 +2,7 @@ using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.Maths; using Robust.Shared.ViewVariables; @@ -97,11 +98,16 @@ internal partial class UserInterfaceManager }, true); } + internal static float CalculateUIScale(float osScale, IConfigurationManager cfg) + { + var cfgScale = cfg.GetCVar(CVars.DisplayUIScale); + return cfgScale == 0 ? osScale : cfgScale; + } + private float CalculateAutoScale(WindowRoot root) { //Grab the OS UIScale or the value set through CVAR debug - var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale); - osScale = osScale == 0f ? root.Window.ContentScale.X : osScale; + var osScale = CalculateUIScale(root.Window.ContentScale.X, _configurationManager); var windowSize = root.Window.RenderTarget.Size; //Only run autoscale if it is enabled, otherwise default to just use OS UIScale diff --git a/Robust.Client/UserInterface/UserInterfaceManager.cs b/Robust.Client/UserInterface/UserInterfaceManager.cs index 5b25c4932..fcdd5834c 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.cs @@ -29,6 +29,11 @@ namespace Robust.Client.UserInterface { internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal { + /// + /// A type that will always be instantiated anyways. + /// + public static readonly Type XamlHotReloadWarmupType = typeof(DropDownDebugConsole); + [Dependency] private readonly IDependencyCollection _rootDependencies = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IFontManager _fontManager = default!; @@ -95,6 +100,7 @@ namespace Robust.Client.UserInterface private Stylesheet? _stylesheet; private ISawmill _sawmillUI = default!; + public ISawmill ControlSawmill { get; private set; } = default!; public event Action? OnKeyBindDown; @@ -129,6 +135,7 @@ namespace Robust.Client.UserInterface disabled: session => _rendering = true)); _inputManager.UIKeyBindStateChanged += OnUIKeyBindStateChanged; + _inputManager.CheckUIIsFocused += OnIsUIFocused; _initThemes(); _stylesheet = new DefaultStylesheet(_resourceCache, this).Stylesheet; @@ -142,6 +149,7 @@ namespace Robust.Client.UserInterface private void _initializeCommon() { _sawmillUI = _logManager.GetSawmill("ui"); + ControlSawmill = _logManager.GetSawmill("ctrl"); RootControl = CreateWindowRoot(_clyde.MainWindow); RootControl.Name = "MainWindowRoot"; @@ -255,6 +263,7 @@ namespace Robust.Client.UserInterface RunMeasure(control); if (!control.IsMeasureValid && control.IsInsideTree) _sawmillUI.Warning($"Control's measure is invalid after measuring. Control: {control}. Parent: {control.Parent}."); + control.InvalidateArrange(); total += 1; } diff --git a/Robust.Client/UserInterface/WordWrap.cs b/Robust.Client/UserInterface/WordWrap.cs index d87bde5e3..d28ca605c 100644 --- a/Robust.Client/UserInterface/WordWrap.cs +++ b/Robust.Client/UserInterface/WordWrap.cs @@ -107,6 +107,14 @@ internal struct WordWrap if (PosX <= _maxSizeX) return; + // Break the "word" at the last word index + if (WordStartBreakIndex.HasValue && oldWordSizePixels != 0) + { + breakLine = WordStartBreakIndex!.Value.index; + MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize); + PosX = WordSizePixels; + } + if (!ForceSplitData.HasValue) { ForceSplitData = (BreakIndexCounter, oldWordSizePixels); diff --git a/Robust.Client/UserInterface/XAML/Proxy/XamlImplementationStorage.cs b/Robust.Client/UserInterface/XAML/Proxy/XamlImplementationStorage.cs index 833895b63..23173d903 100644 --- a/Robust.Client/UserInterface/XAML/Proxy/XamlImplementationStorage.cs +++ b/Robust.Client/UserInterface/XAML/Proxy/XamlImplementationStorage.cs @@ -2,7 +2,10 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using Robust.Shared.Log; +using Robust.Shared.Timing; +using Robust.Shared.Utility; using Robust.Xaml; namespace Robust.Client.UserInterface.XAML.Proxy; @@ -36,6 +39,8 @@ internal sealed class XamlImplementationStorage /// private readonly Dictionary _fileType = new(); + private readonly Dictionary _fileTypeReverse = new(); + /// /// For each type, store the JIT-compiled implementation of Populate. /// @@ -50,6 +55,8 @@ internal sealed class XamlImplementationStorage private readonly ISawmill _sawmill; private readonly XamlJitDelegate _jitDelegate; + private readonly Lock _compileLock = new(); + /// /// Create the storage. /// @@ -102,6 +109,8 @@ internal sealed class XamlImplementationStorage /// an assembly public void Add(Assembly assembly) { + using var _ = _compileLock.EnterScope(); + foreach (var (type, metadata) in TypesWithXamlMetadata(assembly)) { // this can fail, but if it does, that means something is _really_ wrong @@ -132,6 +141,8 @@ internal sealed class XamlImplementationStorage $"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler" ); } + + _fileTypeReverse.Add(type, fileName); } } @@ -145,6 +156,8 @@ internal sealed class XamlImplementationStorage /// public void ForceReloadAll() { + using var _ = _compileLock.EnterScope(); + foreach (var (fileName, fileContent) in _fileContent) { SetImplementation(fileName, fileContent, true); @@ -161,9 +174,19 @@ internal sealed class XamlImplementationStorage /// true if not a no-op public bool CanSetImplementation(string fileName) { + using var _ = _compileLock.EnterScope(); return _fileType.ContainsKey(fileName); } + public MethodInfo? CompileType(Type type) + { + if (_fileTypeReverse.TryGetValue(type, out var fileName)) + return SetImplementation(fileName, _fileContent[fileName], quiet: true); + + _sawmill.Warning($"Type {type} has no XAML file!"); + return null; + } + /// /// Replace the implementation of by JIT-ing /// . @@ -174,12 +197,14 @@ internal sealed class XamlImplementationStorage /// the name of the file whose implementation should be replaced /// the new implementation /// if true, then don't bother to log - public void SetImplementation(string fileName, string fileContent, bool quiet) + public MethodInfo? SetImplementation(string fileName, string fileContent, bool quiet) { + using var _ = _compileLock.EnterScope(); + if (!_fileType.TryGetValue(fileName, out var type)) { _sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents"); - return; + return null; } var uri = @@ -190,12 +215,14 @@ internal sealed class XamlImplementationStorage { _sawmill.Debug($"replacing {fileName} for {type}"); } + var impl = _jitDelegate(type, uri, fileName, fileContent); if (impl != null) { _populateImplementations[type] = impl; } _fileContent[fileName] = fileContent; + return impl; } /// @@ -210,8 +237,12 @@ internal sealed class XamlImplementationStorage { if (!_populateImplementations.TryGetValue(t, out var implementation)) { - // pop out if we never JITed anything - return false; + // JIT if needed. + implementation = CompileType(t); + + // pop out if we never JITed anything/couldn't JIT + if (implementation == null) + return false; } implementation.Invoke(null, [null, o]); diff --git a/Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs b/Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs index 9d4d4dcce..4303deff6 100644 --- a/Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs +++ b/Robust.Client/UserInterface/XAML/Proxy/XamlProxyManager.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; +using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Reflection; @@ -17,6 +20,7 @@ public sealed class XamlProxyManager: IXamlProxyManager ISawmill _sawmill = null!; [Dependency] IReflectionManager _reflectionManager = null!; [Dependency] ILogManager _logManager = null!; + [Dependency] private readonly IConfigurationManager _cfg = null!; XamlImplementationStorage _xamlImplementationStorage = null!; @@ -31,8 +35,21 @@ public sealed class XamlProxyManager: IXamlProxyManager _sawmill = _logManager.GetSawmill("xamlhotreload"); _xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile); - AddAssemblies(); + var preload = _cfg.GetCVar(CVars.UIXamlJitPreload); + + AddAssemblies(reload: preload); _reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); }; + + if (!preload) + { + // Compile any type at all on another thread, so we don't hold up main thread init with loading + // the entire XAML compiler machinery. + // In my testing, it took like 0.5s on debug to run the first XAML compile. Yeah. + ThreadPool.QueueUserWorkItem(_ => + { + _xamlImplementationStorage.CompileType(UserInterfaceManager.XamlHotReloadWarmupType); + }); + } } /// @@ -61,7 +78,7 @@ public sealed class XamlProxyManager: IXamlProxyManager /// Add all the types from all known assemblies, then force-JIT everything /// again. /// - private void AddAssemblies() + private void AddAssemblies(bool reload = true) { foreach (var a in _reflectionManager.Assemblies) { @@ -74,8 +91,8 @@ public sealed class XamlProxyManager: IXamlProxyManager } } - // Always use the JITed versions on debug builds - _xamlImplementationStorage.ForceReloadAll(); + if (reload) + _xamlImplementationStorage.ForceReloadAll(); } /// diff --git a/Robust.Client/Utility/IDiscordRichPresence.cs b/Robust.Client/Utility/IDiscordRichPresence.cs index 2915c2527..ca8ff69e4 100644 --- a/Robust.Client/Utility/IDiscordRichPresence.cs +++ b/Robust.Client/Utility/IDiscordRichPresence.cs @@ -2,6 +2,7 @@ using System; namespace Robust.Client.Utility { + [NotContentImplementable] public interface IDiscordRichPresence: IDisposable { void Initialize(); diff --git a/Robust.Client/Utility/IRand.cs b/Robust.Client/Utility/IRand.cs deleted file mode 100644 index 3de1e299c..000000000 --- a/Robust.Client/Utility/IRand.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Robust.Client.Utility -{ - public interface IRand - { - int Next(); - int Next(int maxValue); - int Next(int minValue, int maxValue); - void NextBytes(byte[] buffer); - double NextDouble(); - } -} diff --git a/Robust.Client/ViewVariables/IClientViewVariablesManager.cs b/Robust.Client/ViewVariables/IClientViewVariablesManager.cs index d11c96f43..46be76c55 100644 --- a/Robust.Client/ViewVariables/IClientViewVariablesManager.cs +++ b/Robust.Client/ViewVariables/IClientViewVariablesManager.cs @@ -2,6 +2,7 @@ using Robust.Shared.ViewVariables; namespace Robust.Client.ViewVariables { + [NotContentImplementable] public interface IClientViewVariablesManager : IViewVariablesManager { /// diff --git a/Robust.Client/ViewVariables/IViewVariableControlFactory.cs b/Robust.Client/ViewVariables/IViewVariableControlFactory.cs index f445acff2..8a6c8b49b 100644 --- a/Robust.Client/ViewVariables/IViewVariableControlFactory.cs +++ b/Robust.Client/ViewVariables/IViewVariableControlFactory.cs @@ -6,6 +6,7 @@ namespace Robust.Client.ViewVariables; /// /// Factory that creates UI controls for viewing variables based on provided property type. /// +[NotContentImplementable] public interface IViewVariableControlFactory { /// diff --git a/Robust.LoaderApi b/Robust.LoaderApi index 86a02eef1..5b467d110 160000 --- a/Robust.LoaderApi +++ b/Robust.LoaderApi @@ -1 +1 @@ -Subproject commit 86a02eef163156fe899eb498acd488e8d7063a0e +Subproject commit 5b467d11005071f420435417927901d11947d5fb diff --git a/Robust.UnitTesting/Packaging/AssetPassMergeTextDirectoriesTest.cs b/Robust.Packaging.Tests/AssetPassMergeTextDirectoriesTest.cs similarity index 96% rename from Robust.UnitTesting/Packaging/AssetPassMergeTextDirectoriesTest.cs rename to Robust.Packaging.Tests/AssetPassMergeTextDirectoriesTest.cs index 85efd5d84..26a99ff08 100644 --- a/Robust.UnitTesting/Packaging/AssetPassMergeTextDirectoriesTest.cs +++ b/Robust.Packaging.Tests/AssetPassMergeTextDirectoriesTest.cs @@ -1,8 +1,7 @@ -using System.Threading.Tasks; -using NUnit.Framework; +using NUnit.Framework; using Robust.Packaging.AssetProcessing.Passes; -namespace Robust.UnitTesting.Packaging; +namespace Robust.Packaging.Tests; [Parallelizable(ParallelScope.All)] [TestFixture] diff --git a/Robust.UnitTesting/Packaging/AssetPassTest.cs b/Robust.Packaging.Tests/AssetPassTest.cs similarity index 95% rename from Robust.UnitTesting/Packaging/AssetPassTest.cs rename to Robust.Packaging.Tests/AssetPassTest.cs index f36b42c04..025c492e8 100644 --- a/Robust.UnitTesting/Packaging/AssetPassTest.cs +++ b/Robust.Packaging.Tests/AssetPassTest.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Robust.Packaging.AssetProcessing; -namespace Robust.UnitTesting.Packaging; +namespace Robust.Packaging.Tests; /// /// Helper class for testing . diff --git a/Robust.UnitTesting/Packaging/AssetPassTestCollector.cs b/Robust.Packaging.Tests/AssetPassTestCollector.cs similarity index 91% rename from Robust.UnitTesting/Packaging/AssetPassTestCollector.cs rename to Robust.Packaging.Tests/AssetPassTestCollector.cs index 21e2124c1..04b3ad76f 100644 --- a/Robust.UnitTesting/Packaging/AssetPassTestCollector.cs +++ b/Robust.Packaging.Tests/AssetPassTestCollector.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NUnit.Framework; +using NUnit.Framework; using Robust.Packaging.AssetProcessing; -namespace Robust.UnitTesting.Packaging; +namespace Robust.Packaging.Tests; /// /// A simple asset pass that stores all files it receives, for introspection by tests. diff --git a/Robust.UnitTesting/Packaging/PackageLoggerNUnit.cs b/Robust.Packaging.Tests/PackageLoggerNUnit.cs similarity index 73% rename from Robust.UnitTesting/Packaging/PackageLoggerNUnit.cs rename to Robust.Packaging.Tests/PackageLoggerNUnit.cs index bb7057d48..acdf361d5 100644 --- a/Robust.UnitTesting/Packaging/PackageLoggerNUnit.cs +++ b/Robust.Packaging.Tests/PackageLoggerNUnit.cs @@ -1,8 +1,6 @@ -using System.IO; -using Robust.Packaging; -using Robust.Shared.Log; +using Robust.Shared.Log; -namespace Robust.UnitTesting.Packaging; +namespace Robust.Packaging.Tests; /// /// Package logger for writing to NUnit's test context. diff --git a/Robust.Packaging.Tests/Robust.Packaging.Tests.csproj b/Robust.Packaging.Tests/Robust.Packaging.Tests.csproj new file mode 100644 index 000000000..b94c8d7ae --- /dev/null +++ b/Robust.Packaging.Tests/Robust.Packaging.Tests.csproj @@ -0,0 +1,22 @@ + + + + + enable + + + + + + + + + + + + + + + + + diff --git a/Robust.Packaging/Robust.Packaging.csproj b/Robust.Packaging/Robust.Packaging.csproj index d6ab2e72e..aa5afc561 100644 --- a/Robust.Packaging/Robust.Packaging.csproj +++ b/Robust.Packaging/Robust.Packaging.csproj @@ -6,6 +6,11 @@ Exe + + + + + diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index c19fa62af..bcdf4f61d 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -45,6 +45,9 @@ public static class Diagnostics public const string IdPrototypeInstantiation = "RA0039"; public const string IdAutoGenStateAttributeMissing = "RA0040"; public const string IdAutoGenStateParamMissing = "RA0041"; + public const string IdPrototypeRedundantType = "RA0042"; + public const string IdPrototypeEndsWithPrototype = "RA0043"; + public const string IdValidateMember = "RA0044"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.UnitTesting/Server/GameObjects/ComponentMapInitTest.cs b/Robust.Server.IntegrationTests/GameObjects/ComponentMapInitTest.cs similarity index 91% rename from Robust.UnitTesting/Server/GameObjects/ComponentMapInitTest.cs rename to Robust.Server.IntegrationTests/GameObjects/ComponentMapInitTest.cs index 56ca5abc3..b38ce755e 100644 --- a/Robust.UnitTesting/Server/GameObjects/ComponentMapInitTest.cs +++ b/Robust.Server.IntegrationTests/GameObjects/ComponentMapInitTest.cs @@ -3,11 +3,13 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Reflection; +using Robust.UnitTesting.Server; +using Is = Robust.UnitTesting.Is; -namespace Robust.UnitTesting.Server.GameObjects; +namespace Robust.Server.IntegrationTests.GameObjects; [TestFixture] -public sealed partial class ComponentMapInitTest +internal sealed partial class ComponentMapInitTest { /// /// Asserts whether a component added after an entity has fully initialized has MapInit called. diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs similarity index 98% rename from Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs rename to Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs index de1404217..87f39f7a0 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Robust.Server.Containers; using Robust.Shared.Containers; @@ -10,11 +8,12 @@ using Robust.Shared.Map; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Robust.UnitTesting.Server; -namespace Robust.UnitTesting.Server.GameObjects.Components +namespace Robust.Server.IntegrationTests.GameObjects.Components { [TestFixture, Parallelizable] - public sealed partial class ContainerTest + internal sealed partial class ContainerTest { private static EntityCoordinates _coords; diff --git a/Robust.UnitTesting/Server/GameObjects/Components/TransformIntegration_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/TransformIntegration_Test.cs similarity index 93% rename from Robust.UnitTesting/Server/GameObjects/Components/TransformIntegration_Test.cs rename to Robust.Server.IntegrationTests/GameObjects/Components/TransformIntegration_Test.cs index 2977025d0..a79bbceee 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/TransformIntegration_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/TransformIntegration_Test.cs @@ -3,11 +3,12 @@ using NUnit.Framework; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.UnitTesting.Server; -namespace Robust.UnitTesting.Server.GameObjects.Components; +namespace Robust.Server.IntegrationTests.GameObjects.Components; [TestFixture] -public sealed class TransformIntegration_Test +internal sealed class TransformIntegration_Test { /// /// Asserts that calling SetWorldPosition while in a container correctly removes the entity from its container. diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs similarity index 99% rename from Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs rename to Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs index a80ddfb00..95b857bb9 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs @@ -1,7 +1,5 @@ -using System.IO; using System.Numerics; using NUnit.Framework; -using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -10,12 +8,13 @@ using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Server.GameObjects.Components +namespace Robust.Server.IntegrationTests.GameObjects.Components { [TestFixture] [TestOf(typeof(TransformComponent))] - sealed class Transform_Test : RobustUnitTest + internal sealed class Transform_Test : RobustUnitTest { public override UnitTestProject Project => UnitTestProject.Server; diff --git a/Robust.UnitTesting/Server/GameObjects/ServerEntityNetworkManagerTest.cs b/Robust.Server.IntegrationTests/GameObjects/ServerEntityNetworkManagerTest.cs similarity index 94% rename from Robust.UnitTesting/Server/GameObjects/ServerEntityNetworkManagerTest.cs rename to Robust.Server.IntegrationTests/GameObjects/ServerEntityNetworkManagerTest.cs index b04550f20..ac3cef5fa 100644 --- a/Robust.UnitTesting/Server/GameObjects/ServerEntityNetworkManagerTest.cs +++ b/Robust.Server.IntegrationTests/GameObjects/ServerEntityNetworkManagerTest.cs @@ -7,9 +7,9 @@ using Robust.Shared.Network.Messages; using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Server.GameObjects +namespace Robust.Server.IntegrationTests.GameObjects { - public sealed class ServerEntityNetworkManagerTest + internal sealed class ServerEntityNetworkManagerTest { [Test] public void TestMessageSort() diff --git a/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs b/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs similarity index 92% rename from Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs rename to Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs index 2bff79e80..45e451301 100644 --- a/Robust.UnitTesting/Server/GameObjects/ThrowingEntityDeletion_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs @@ -1,12 +1,12 @@ -using System.Linq; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.UnitTesting.Server; -namespace Robust.UnitTesting.Server.GameObjects +namespace Robust.Server.IntegrationTests.GameObjects { [TestFixture] - public sealed class ThrowingEntityDeletion_Test + internal sealed class ThrowingEntityDeletion_Test { private ISimulation _sim = default!; diff --git a/Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs b/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/DefaultEntityTest.cs rename to Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/DetachedParentTest.cs b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/DetachedParentTest.cs rename to Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/MissingParentTest.cs b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/MissingParentTest.cs rename to Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/PvsChunkTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/PvsChunkTest.cs rename to Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/PvsPauseTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs similarity index 97% rename from Robust.UnitTesting/Server/GameStates/PvsPauseTest.cs rename to Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs index 4bd6432b9..12e4d6759 100644 --- a/Robust.UnitTesting/Server/GameStates/PvsPauseTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs @@ -166,9 +166,8 @@ public sealed class PvsPauseTest : RobustIntegrationTest AssertEnt(paused: true, detached: false, clientPaused: true); } - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); + client.Post(() => netMan.ClientDisconnect("")); + await RunTicks(); } } diff --git a/Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/PvsReEntryTest.cs rename to Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/PvsResetTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/PvsResetTest.cs rename to Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs diff --git a/Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs similarity index 100% rename from Robust.UnitTesting/Server/GameStates/PvsSystemTests.cs rename to Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs diff --git a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs b/Robust.Server.IntegrationTests/Maps/MapLoaderTest.cs similarity index 100% rename from Robust.UnitTesting/Server/Maps/MapLoaderTest.cs rename to Robust.Server.IntegrationTests/Maps/MapLoaderTest.cs diff --git a/Robust.Server.IntegrationTests/Robust.Server.IntegrationTests.csproj b/Robust.Server.IntegrationTests/Robust.Server.IntegrationTests.csproj new file mode 100644 index 000000000..4cfd5db0b --- /dev/null +++ b/Robust.Server.IntegrationTests/Robust.Server.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + + enable + false + ../bin/Server.IntegrationTests + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Server/ViewVariables/ViewVariablesTraitMembersTest.cs b/Robust.Server.IntegrationTests/ViewVariables/ViewVariablesTraitMembersTest.cs similarity index 100% rename from Robust.UnitTesting/Server/ViewVariables/ViewVariablesTraitMembersTest.cs rename to Robust.Server.IntegrationTests/ViewVariables/ViewVariablesTraitMembersTest.cs diff --git a/Robust.Server.Testing/AssemblyInfo.cs b/Robust.Server.Testing/AssemblyInfo.cs new file mode 100644 index 000000000..dd72c82d3 --- /dev/null +++ b/Robust.Server.Testing/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Robust.Shared.IntegrationTests")] diff --git a/Robust.Server.Testing/Robust.Server.Testing.csproj b/Robust.Server.Testing/Robust.Server.Testing.csproj new file mode 100644 index 000000000..a10bda7b5 --- /dev/null +++ b/Robust.Server.Testing/Robust.Server.Testing.csproj @@ -0,0 +1,24 @@ + + + + + enable + + + + + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.Server.Testing/RobustServerSimulation.cs similarity index 99% rename from Robust.UnitTesting/Server/RobustServerSimulation.cs rename to Robust.Server.Testing/RobustServerSimulation.cs index 8b3a96627..2495fc679 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.Server.Testing/RobustServerSimulation.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; using System.Reflection; using JetBrains.Annotations; using Moq; -using Robust.Client.HWId; using Robust.Server; using Robust.Server.Configuration; using Robust.Server.Console; @@ -34,8 +31,6 @@ using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Controllers; -using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Profiling; @@ -46,9 +41,11 @@ using Robust.Shared.Replays; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Testing; using Robust.Shared.Threading; using Robust.Shared.Timing; +// ReSharper disable once CheckNamespace namespace Robust.UnitTesting.Server { [PublicAPI] @@ -283,6 +280,7 @@ namespace Robust.UnitTesting.Server configMan.LoadCVarsFromAssembly(typeof(Program).Assembly); // Server configMan.LoadCVarsFromAssembly(typeof(ProgramShared).Assembly); // Shared configMan.LoadCVarsFromAssembly(typeof(RobustServerSimulation).Assembly); // Tests + configMan.LoadCVarsFromAssembly(typeof(RTCVars).Assembly); // Tests var logMan = container.Resolve(); logMan.RootSawmill.AddHandler(new TestLogHandler(configMan, "SIM")); diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 774a04c5c..f6e70299d 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -166,8 +166,12 @@ namespace Robust.Server public bool Start(ServerOptions options, Func? logHandlerFactory = null) { Options = options; + _config.Initialize(true); + _config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server + _config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared + if (Options.LoadConfigAndUserData) { string? path = _commandLineArgs?.ConfigFile; @@ -192,9 +196,6 @@ namespace Robust.Server } } - _config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server - _config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared - CVarDefaultOverrides.OverrideServer(_config); _config.OverrideConVars(EnvironmentVariables.GetEnvironmentCVars()); @@ -599,6 +600,19 @@ namespace Robust.Server _logger.Info($"Tickrate changed to: {b} on tick {_time.CurTick}"); }); + _config.OnValueChanged(CVars.GameTimeScale, f => + { + if (!GameTiming.IsTimescaleValid(f)) + { + _logger.Error($"Invalid time scale set: {f}, ignoring"); + return; + } + + _time.TimeScale = f; + + _logger.Info($"Timescale changed to: {f} on tick {_time.CurTick}"); + }, true); + var startOffset = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetTimeStartOffset)); _time.TimeBase = (startOffset, GameTick.First); _time.TickRate = (ushort) _config.GetCVar(CVars.NetTickrate); diff --git a/Robust.Server/Configuration/IServerNetConfigurationManager.cs b/Robust.Server/Configuration/IServerNetConfigurationManager.cs index 038b5800b..2f6b63a9e 100644 --- a/Robust.Server/Configuration/IServerNetConfigurationManager.cs +++ b/Robust.Server/Configuration/IServerNetConfigurationManager.cs @@ -7,6 +7,7 @@ namespace Robust.Server.Configuration; /// A networked configuration manager that controls the replication of /// console variables between client and server. /// +[NotContentImplementable] public interface IServerNetConfigurationManager : INetConfigurationManager { /// diff --git a/Robust.Server/Console/IConGroupController.cs b/Robust.Server/Console/IConGroupController.cs index a0735c242..a96fc0c1c 100644 --- a/Robust.Server/Console/IConGroupController.cs +++ b/Robust.Server/Console/IConGroupController.cs @@ -1,5 +1,6 @@ namespace Robust.Server.Console { + [NotContentImplementable] public interface IConGroupController : IConGroupControllerImplementation { public IConGroupControllerImplementation Implementation { set; } diff --git a/Robust.Server/Console/IServerConsoleHost.cs b/Robust.Server/Console/IServerConsoleHost.cs index bdb3dcfa6..4ce20375e 100644 --- a/Robust.Server/Console/IServerConsoleHost.cs +++ b/Robust.Server/Console/IServerConsoleHost.cs @@ -5,6 +5,7 @@ namespace Robust.Server.Console /// /// The server console shell that executes commands. /// + [NotContentImplementable] public interface IServerConsoleHost : IConsoleHost { /// diff --git a/Robust.Server/Console/ISystemConsoleManager.cs b/Robust.Server/Console/ISystemConsoleManager.cs index 5f5627f1b..e8f3b980e 100644 --- a/Robust.Server/Console/ISystemConsoleManager.cs +++ b/Robust.Server/Console/ISystemConsoleManager.cs @@ -3,6 +3,7 @@ /// /// Wraps the system console. /// + [NotContentImplementable] public interface ISystemConsoleManager { /// diff --git a/Robust.Server/DataMetrics/MetricsManager.cs b/Robust.Server/DataMetrics/MetricsManager.cs index 804ea02fb..88c19ec36 100644 --- a/Robust.Server/DataMetrics/MetricsManager.cs +++ b/Robust.Server/DataMetrics/MetricsManager.cs @@ -30,6 +30,7 @@ namespace Robust.Server.DataMetrics; /// IoC contains an implementation of that can be used to instantiate meters. /// /// +[NotContentImplementable] public interface IMetricsManager { /// diff --git a/Robust.Server/GameObjects/IServerEntityManager.cs b/Robust.Server/GameObjects/IServerEntityManager.cs index 72b02a652..908285675 100644 --- a/Robust.Server/GameObjects/IServerEntityManager.cs +++ b/Robust.Server/GameObjects/IServerEntityManager.cs @@ -5,5 +5,6 @@ namespace Robust.Server.GameObjects /// /// Server side version of the . /// + [NotContentImplementable] public interface IServerEntityManager : IEntityManager, IServerEntityNetworkManager { } } diff --git a/Robust.Server/GameObjects/IServerEntityNetworkManager.cs b/Robust.Server/GameObjects/IServerEntityNetworkManager.cs index d7be92e91..b45837f96 100644 --- a/Robust.Server/GameObjects/IServerEntityNetworkManager.cs +++ b/Robust.Server/GameObjects/IServerEntityNetworkManager.cs @@ -3,6 +3,7 @@ using Robust.Shared.Player; namespace Robust.Server.GameObjects { + [NotContentImplementable] public interface IServerEntityNetworkManager : IEntityNetworkManager { uint GetLastMessageSequence(ICommonSession session); diff --git a/Robust.Server/GameStates/IServerGameStateManager.cs b/Robust.Server/GameStates/IServerGameStateManager.cs index 5a68fa7ef..90235185d 100644 --- a/Robust.Server/GameStates/IServerGameStateManager.cs +++ b/Robust.Server/GameStates/IServerGameStateManager.cs @@ -8,6 +8,7 @@ namespace Robust.Server.GameStates /// /// Engine service that provides creating and dispatching of game states. /// + [NotContentImplementable] public interface IServerGameStateManager { /// diff --git a/Robust.Server/GameStates/PvsSystem.GetStates.cs b/Robust.Server/GameStates/PvsSystem.GetStates.cs index 04afedeb2..87786ceac 100644 --- a/Robust.Server/GameStates/PvsSystem.GetStates.cs +++ b/Robust.Server/GameStates/PvsSystem.GetStates.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -20,11 +22,11 @@ internal sealed partial class PvsSystem /// New entity State for the given entity. private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta) { - var bus = EntityManager.EventBus; var changed = new List(); bool sendCompList = meta.LastComponentRemoved > fromTick; HashSet? netComps = sendCompList ? new() : null; + var stateEv = new ComponentGetState(player, fromTick); foreach (var (netId, component) in meta.NetComponents) { @@ -41,15 +43,15 @@ internal sealed partial class PvsSystem if (component.LastModifiedTick <= fromTick) { - if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(bus, component, player))) + if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(component, player))) netComps!.Add(netId); continue; } - if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(bus, component, player)) + if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(component, player)) continue; - var state = EntityManager.GetComponentState(bus, component, player, fromTick); + var state = ComponentState(entityUid, component, netId, ref stateEv); changed.Add(new ComponentChange(netId, state, component.LastModifiedTick)); if (state != null) @@ -66,13 +68,23 @@ internal sealed partial class PvsSystem return entState; } + private IComponentState? ComponentState(EntityUid uid, IComponent comp, ushort netId, ref ComponentGetState stateEv) + { + DebugTools.Assert(comp.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {comp.GetType()}"); + stateEv.State = null; + _getStateHandlers![netId]?.Invoke(uid, comp, ref Unsafe.As(ref stateEv)); + var state = stateEv.State; + return state; + } + /// /// Variant of that includes all entity data, including data that can be inferred implicitly from the entity prototype. /// private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta) { - var bus = EntityManager.EventBus; + var bus = EntityManager.EventBusInternal; var changed = new List(); + var stateEv = new ComponentGetState(player, GameTick.Zero); HashSet netComps = new(); @@ -86,7 +98,7 @@ internal sealed partial class PvsSystem if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player)) continue; - var state = EntityManager.GetComponentState(bus, component, player, GameTick.Zero); + var state = ComponentState(entityUid, component, netId, ref stateEv); DebugTools.Assert(state is not IComponentDeltaState); changed.Add(new ComponentChange(netId, state, component.LastModifiedTick)); netComps.Add(netId); diff --git a/Robust.Server/GameStates/PvsSystem.Leave.cs b/Robust.Server/GameStates/PvsSystem.Leave.cs index bfb0041e5..37026cf7d 100644 --- a/Robust.Server/GameStates/PvsSystem.Leave.cs +++ b/Robust.Server/GameStates/PvsSystem.Leave.cs @@ -87,6 +87,9 @@ internal sealed partial class PvsSystem catch (Exception e) { _pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages."); +#if !EXCEPTION_TOLERANCE + throw; +#endif } } } diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs index ff3fd0812..d4df28dea 100644 --- a/Robust.Server/GameStates/PvsSystem.Serialize.cs +++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs @@ -48,6 +48,9 @@ internal sealed partial class PvsSystem { var source = i >= 0 ? _sessions[i].Session.ToString() : "replays"; Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}."); +#if !EXCEPTION_TOLERANCE + throw; +#endif } } diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs index e68ff17e2..26ca25493 100644 --- a/Robust.Server/GameStates/PvsSystem.cs +++ b/Robust.Server/GameStates/PvsSystem.cs @@ -14,6 +14,7 @@ using Robust.Server.Replays; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; @@ -105,6 +106,7 @@ internal sealed partial class PvsSystem : EntitySystem private bool _async; private DefaultObjectPool _threadResourcesPool = default!; + private EntityEventBus.DirectedEventHandler?[]? _getStateHandlers; private static readonly Histogram Histogram = Metrics.CreateHistogram("robust_game_state_update_usage", "Amount of time spent processing different parts of the game state update", new HistogramConfiguration @@ -173,6 +175,7 @@ internal sealed partial class PvsSystem : EntitySystem ClearPvsData(); ShutdownDirty(); + _getStateHandlers = null; } public override void Update(float frameTime) @@ -185,6 +188,8 @@ internal sealed partial class PvsSystem : EntitySystem /// internal void SendGameStates(ICommonSession[] players) { + _getStateHandlers ??= EntityManager.EventBusInternal.GetNetCompEventHandlers(); + // Wait for pending jobs and process disconnected players ProcessDisconnections(); diff --git a/Robust.Server/IBaseServer.cs b/Robust.Server/IBaseServer.cs index b9ad666f8..160d70d02 100644 --- a/Robust.Server/IBaseServer.cs +++ b/Robust.Server/IBaseServer.cs @@ -7,6 +7,7 @@ namespace Robust.Server /// /// Top level class that controls the game logic of the server. /// + [NotContentImplementable] public interface IBaseServer { /// diff --git a/Robust.Server/Placement/IPlacementManager.cs b/Robust.Server/Placement/IPlacementManager.cs index fa2d5d815..179699c1e 100644 --- a/Robust.Server/Placement/IPlacementManager.cs +++ b/Robust.Server/Placement/IPlacementManager.cs @@ -4,6 +4,7 @@ using Robust.Shared.Network.Messages; namespace Robust.Server.Placement { + [NotContentImplementable] public interface IPlacementManager { /// diff --git a/Robust.Server/Player/IPlayerInput.cs b/Robust.Server/Player/IPlayerInput.cs deleted file mode 100644 index 0a5ef38ab..000000000 --- a/Robust.Server/Player/IPlayerInput.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Input; -using Robust.Shared.Input.Binding; - -namespace Robust.Server.Player -{ - public interface IPlayerInput - { - BoundKeyState GetKeyState(BoundKeyFunction function); - bool GetKeyStateBool(BoundKeyFunction function); - - InputCmdHandler GetCommand(BoundKeyFunction function); - void SetCommand(BoundKeyFunction function, InputCmdHandler cmdHandler); - } -} diff --git a/Robust.Server/Player/IPlayerManager.cs b/Robust.Server/Player/IPlayerManager.cs index 94f10eaee..0f53202bf 100644 --- a/Robust.Server/Player/IPlayerManager.cs +++ b/Robust.Server/Player/IPlayerManager.cs @@ -6,7 +6,8 @@ namespace Robust.Server.Player; /// /// Manages each players session when connected to the server. /// +[NotContentImplementable] public interface IPlayerManager : ISharedPlayerManager { BoundKeyMap KeyMap { get; } -} \ No newline at end of file +} diff --git a/Robust.Server/Properties/AssemblyInfo.cs b/Robust.Server/Properties/AssemblyInfo.cs index 1e2422615..cf8b15429 100644 --- a/Robust.Server/Properties/AssemblyInfo.cs +++ b/Robust.Server/Properties/AssemblyInfo.cs @@ -3,6 +3,9 @@ [assembly: InternalsVisibleTo("Robust.UnitTesting")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: InternalsVisibleTo("Robust.Benchmarks")] +[assembly: InternalsVisibleTo("Robust.Shared.IntegrationTests")] +[assembly: InternalsVisibleTo("Robust.Server.IntegrationTests")] +[assembly: InternalsVisibleTo("Robust.Server.Testing")] #if NET5_0_OR_GREATER [module: SkipLocalsInit] diff --git a/Robust.Server/Replays/IServerReplayRecordingManager.cs b/Robust.Server/Replays/IServerReplayRecordingManager.cs index 4ba26f075..0a8ac1d6f 100644 --- a/Robust.Server/Replays/IServerReplayRecordingManager.cs +++ b/Robust.Server/Replays/IServerReplayRecordingManager.cs @@ -2,6 +2,7 @@ using Robust.Shared.Replays; namespace Robust.Server.Replays; +[NotContentImplementable] public interface IServerReplayRecordingManager : IReplayRecordingManager { /// diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj index e0a4c209f..11ed9746d 100644 --- a/Robust.Server/Robust.Server.csproj +++ b/Robust.Server/Robust.Server.csproj @@ -32,6 +32,10 @@ + + + + @@ -39,6 +43,7 @@ + diff --git a/Robust.Server/ServerStatus/IStatusHandlerContext.cs b/Robust.Server/ServerStatus/IStatusHandlerContext.cs index 740fb1470..932ce152b 100644 --- a/Robust.Server/ServerStatus/IStatusHandlerContext.cs +++ b/Robust.Server/ServerStatus/IStatusHandlerContext.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Primitives; namespace Robust.Server.ServerStatus { + [NotContentImplementable] public interface IStatusHandlerContext { HttpMethod RequestMethod { get; } diff --git a/Robust.Server/ServerStatus/IStatusHost.cs b/Robust.Server/ServerStatus/IStatusHost.cs index d76ce80b0..5f834ac1f 100644 --- a/Robust.Server/ServerStatus/IStatusHost.cs +++ b/Robust.Server/ServerStatus/IStatusHost.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; namespace Robust.Server.ServerStatus { + [NotContentImplementable] public interface IStatusHost { void Start(); diff --git a/Robust.Server/ServerStatus/IWatchdogApi.cs b/Robust.Server/ServerStatus/IWatchdogApi.cs index 23441727b..e88b370ae 100644 --- a/Robust.Server/ServerStatus/IWatchdogApi.cs +++ b/Robust.Server/ServerStatus/IWatchdogApi.cs @@ -5,6 +5,7 @@ namespace Robust.Server.ServerStatus /// /// API for interacting with SS14.Watchdog. /// + [NotContentImplementable] public interface IWatchdogApi { /// diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index c4e3c06f5..69aa3dcf5 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -13,7 +13,9 @@ using Robust.Roslyn.Shared; namespace Robust.Shared.CompNetworkGenerator { [Generator] +#pragma warning disable RS1042 public class ComponentNetworkGenerator : ISourceGenerator +#pragma warning restore RS1042 { private const string ClassAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute"; private const string MemberAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute"; diff --git a/Robust.UnitTesting/Shared/Configuration/ConfigurationIntegrationTest.cs b/Robust.Shared.IntegrationTests/Configuration/ConfigurationIntegrationTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Configuration/ConfigurationIntegrationTest.cs rename to Robust.Shared.IntegrationTests/Configuration/ConfigurationIntegrationTest.cs index 48c910a71..ee504afbe 100644 --- a/Robust.UnitTesting/Shared/Configuration/ConfigurationIntegrationTest.cs +++ b/Robust.Shared.IntegrationTests/Configuration/ConfigurationIntegrationTest.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.Configuration; +using Robust.Shared.IntegrationTests.Configuration; using Robust.Shared.Log; namespace Robust.UnitTesting.Shared.Configuration; diff --git a/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs b/Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs rename to Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs index c8f1d12d4..ef84ada71 100644 --- a/Robust.UnitTesting/Shared/Configuration/ConfigurationManagerTest.cs +++ b/Robust.Shared.IntegrationTests/Configuration/ConfigurationManagerTest.cs @@ -8,12 +8,12 @@ using Robust.Shared.Network; using Robust.Shared.Replays; using Robust.Shared.Timing; -namespace Robust.UnitTesting.Shared.Configuration +namespace Robust.Shared.IntegrationTests.Configuration { [TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(ConfigurationManager))] - public sealed class ConfigurationManagerTest + internal sealed class ConfigurationManagerTest { [Test] public void TestSubscribeUnsubscribe() diff --git a/Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest.cs b/Robust.Shared.IntegrationTests/ContentPack/ResourceManagerTest.cs similarity index 93% rename from Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest.cs rename to Robust.Shared.IntegrationTests/ContentPack/ResourceManagerTest.cs index e9fe18e54..3fbc18698 100644 --- a/Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest.cs +++ b/Robust.Shared.IntegrationTests/ContentPack/ResourceManagerTest.cs @@ -1,17 +1,17 @@ -using System.IO; using NUnit.Framework; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Utility; +using Robust.UnitTesting.Shared; -namespace Robust.UnitTesting.Shared.ContentPack +namespace Robust.Shared.IntegrationTests.ContentPack { [TestFixture] - public sealed class ResourceManagerTest : RobustUnitTest + internal sealed class ResourceManagerTest : OurRobustUnitTest { private static Stream ZipStream => typeof(ResourceManagerTest).Assembly - .GetManifestResourceStream("Robust.UnitTesting.Shared.ContentPack.ZipTest.zip")!; + .GetManifestResourceStream("Robust.Shared.IntegrationTests.ContentPack.ZipTest.zip")!; private static readonly byte[] Data = { diff --git a/Robust.UnitTesting/Shared/ContentPack/ZipTest.zip b/Robust.Shared.IntegrationTests/ContentPack/ZipTest.zip similarity index 100% rename from Robust.UnitTesting/Shared/ContentPack/ZipTest.zip rename to Robust.Shared.IntegrationTests/ContentPack/ZipTest.zip diff --git a/Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs b/Robust.Shared.IntegrationTests/EngineIntegrationTest_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs rename to Robust.Shared.IntegrationTests/EngineIntegrationTest_Test.cs index 22ea1a58e..00edde8a2 100644 --- a/Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs +++ b/Robust.Shared.IntegrationTests/EngineIntegrationTest_Test.cs @@ -9,7 +9,7 @@ using Robust.Shared.Network; namespace Robust.UnitTesting.Shared { [TestFixture] - public sealed class EngineIntegrationTest_Test : RobustIntegrationTest + internal sealed class EngineIntegrationTest_Test : RobustIntegrationTest { [Test] public void ServerStartsCorrectlyTest() diff --git a/Robust.UnitTesting/Shared/EntityLookup_Test.cs b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntityLookup_Test.cs rename to Robust.Shared.IntegrationTests/EntityLookup_Test.cs index b6c975f6b..cd06309a6 100644 --- a/Robust.UnitTesting/Shared/EntityLookup_Test.cs +++ b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs @@ -14,7 +14,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared { [TestFixture, TestOf(typeof(EntityLookupSystem))] - public sealed class EntityLookupTest + internal sealed class EntityLookupTest { private static readonly MapId MapId = new MapId(1); diff --git a/Robust.UnitTesting/Shared/EntitySerialization/AlwaysPushSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/AlwaysPushSerializationTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/EntitySerialization/AlwaysPushSerializationTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/AlwaysPushSerializationTest.cs index d786d61bd..4abdc7f73 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/AlwaysPushSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/AlwaysPushSerializationTest.cs @@ -11,7 +11,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed class AlwaysPushSerializationTest : RobustIntegrationTest +internal sealed class AlwaysPushSerializationTest : RobustIntegrationTest { private const string Prototype = @" - type: entity diff --git a/Robust.UnitTesting/Shared/EntitySerialization/AutoIncludeSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/AutoIncludeSerializationTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs index 3f99d7e5d..3e4b12c25 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/AutoIncludeSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs @@ -15,7 +15,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class AutoIncludeSerializationTest : RobustIntegrationTest +internal sealed partial class AutoIncludeSerializationTest : RobustIntegrationTest { private const string TestTileDefId = "a"; private const string TestPrototypes = $@" diff --git a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v3.cs b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v3.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v3.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v3.cs index de317d52f..4ec89d6d9 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v3.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v3.cs @@ -14,7 +14,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class BackwardsCompatibilityTest +internal sealed partial class BackwardsCompatibilityTest { /// /// Check that v3 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation. diff --git a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v4.cs b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v4.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v4.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v4.cs index 9b88d36ec..6a3ba9049 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v4.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v4.cs @@ -13,7 +13,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class BackwardsCompatibilityTest +internal sealed partial class BackwardsCompatibilityTest { /// /// Check that v4 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation. diff --git a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v5.cs b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v5.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v5.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v5.cs index 7a44543fa..c473b61c2 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v5.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v5.cs @@ -13,7 +13,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class BackwardsCompatibilityTest +internal sealed partial class BackwardsCompatibilityTest { /// /// Check that v5 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation. diff --git a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v6.cs b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v6.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v6.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v6.cs index 73debe36d..086bab627 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v6.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v6.cs @@ -13,7 +13,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class BackwardsCompatibilityTest +internal sealed partial class BackwardsCompatibilityTest { /// /// Check that v6 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation. diff --git a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v7.cs b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v7.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v7.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v7.cs index c332b3c67..346a54ea5 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/BackwardsCompatibilityTest.v7.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/BackwardsCompatibilityTest.v7.cs @@ -16,7 +16,7 @@ namespace Robust.UnitTesting.Shared.EntitySerialization; /// Test that older file formats can still be loaded. /// [TestFixture] -public sealed partial class BackwardsCompatibilityTest : RobustIntegrationTest +internal sealed partial class BackwardsCompatibilityTest : RobustIntegrationTest { /// /// Check that v7 maps can be loaded. This just re-uses some map files that are generated by other tests, and then diff --git a/Robust.UnitTesting/Shared/EntitySerialization/CategorizationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/EntitySerialization/CategorizationTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs index c7adffa94..5b48ec6c1 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/CategorizationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs @@ -11,7 +11,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class CategorizationTest : RobustIntegrationTest +internal sealed partial class CategorizationTest : RobustIntegrationTest { private const string TestTileDefId = "a"; private const string TestPrototypes = $@" diff --git a/Robust.UnitTesting/Shared/EntitySerialization/LifestageSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/LifestageSerializationTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs index 89c05e801..ab5493f32 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/LifestageSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs @@ -10,7 +10,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class LifestageSerializationTest : RobustIntegrationTest +internal sealed partial class LifestageSerializationTest : RobustIntegrationTest { /// /// Check that whether or not an entity has been paused or map-initialized is preserved across saves & loads. diff --git a/Robust.UnitTesting/Shared/EntitySerialization/MapMergeTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/MapMergeTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs index e09281755..97f2d9420 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/MapMergeTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs @@ -16,7 +16,7 @@ namespace Robust.UnitTesting.Shared.EntitySerialization; /// onto a paused map should pause it. /// [TestFixture] -public sealed partial class MapMergeTest : RobustIntegrationTest +internal sealed partial class MapMergeTest : RobustIntegrationTest { private const string TestTileDefId = "a"; private const string TestPrototypes = $@" diff --git a/Robust.UnitTesting/Shared/EntitySerialization/OrphanSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs similarity index 99% rename from Robust.UnitTesting/Shared/EntitySerialization/OrphanSerializationTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs index fd231a4e3..2cbb7f1db 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/OrphanSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs @@ -13,7 +13,7 @@ using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestCompone namespace Robust.UnitTesting.Shared.EntitySerialization; [TestFixture] -public sealed partial class OrphanSerializationTest : RobustIntegrationTest +internal sealed partial class OrphanSerializationTest : RobustIntegrationTest { private const string TestTileDefId = "a"; private const string TestPrototypes = $@" diff --git a/Robust.UnitTesting/Shared/EntitySerialization/RobustCloneableTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/RobustCloneableTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/EntitySerialization/RobustCloneableTest.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/RobustCloneableTest.cs index 4f1b91db1..a664cedc2 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/RobustCloneableTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/RobustCloneableTest.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; @@ -58,7 +56,7 @@ public sealed partial class RobustCloneableTestComponent : Component public RobustCloneableTestStruct? NullableTestStruct; } -public sealed class RobustCloneableTest() : RobustIntegrationTest +internal sealed class RobustCloneableTest() : RobustIntegrationTest { [Test] public async Task TestClone() diff --git a/Robust.UnitTesting/Shared/EntitySerialization/SerializationTestHelper.cs b/Robust.Shared.IntegrationTests/EntitySerialization/SerializationTestHelper.cs similarity index 100% rename from Robust.UnitTesting/Shared/EntitySerialization/SerializationTestHelper.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/SerializationTestHelper.cs diff --git a/Robust.UnitTesting/Shared/EntitySerialization/TestComponents.cs b/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs similarity index 94% rename from Robust.UnitTesting/Shared/EntitySerialization/TestComponents.cs rename to Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs index 685a939d5..04a751eff 100644 --- a/Robust.UnitTesting/Shared/EntitySerialization/TestComponents.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs @@ -11,7 +11,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.EntitySerialization; [RegisterComponent] -public sealed partial class EntitySaveTestComponent : Component +internal sealed partial class EntitySaveTestComponent : Component { /// /// Give each entity a unique id to identify them across map saves & loads. @@ -46,7 +46,7 @@ public sealed partial class EntitySaveTestComponent : Component /// Dummy tile definition for serializing grids. /// [Prototype("testTileDef")] -public sealed partial class TileDef : ITileDefinition +internal sealed partial class TileDef : ITileDefinition { public ushort TileId { get; set; } public string Name => ID; diff --git a/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/ComponentFactory_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs rename to Robust.Shared.IntegrationTests/GameObjects/ComponentFactory_Tests.cs index eaa4292db..f4eaeaf9f 100644 --- a/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/ComponentFactory_Tests.cs @@ -7,7 +7,7 @@ namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture] [TestOf(typeof(ComponentFactory))] - public sealed partial class ComponentFactory_Tests : RobustUnitTest + internal sealed partial class ComponentFactory_Tests : OurRobustUnitTest { private const string TestComponentName = "A"; private const string LowercaseTestComponentName = "a"; diff --git a/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs rename to Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs index 4e94b264e..28b38f3f7 100644 --- a/Robust.UnitTesting/Shared/GameObjects/ContainerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs @@ -15,7 +15,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.GameObjects { - public sealed class ContainerTests : RobustIntegrationTest + internal sealed class ContainerTests : RobustIntegrationTest { /// /// Tests container states with children that do not exist on the client diff --git a/Robust.UnitTesting/Shared/GameObjects/DeferredEntityDeletionTest.cs b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameObjects/DeferredEntityDeletionTest.cs rename to Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs index 09bdd9216..5bb02e61d 100644 --- a/Robust.UnitTesting/Shared/GameObjects/DeferredEntityDeletionTest.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs @@ -7,7 +7,7 @@ using Robust.Shared.Reflection; namespace Robust.UnitTesting.Shared.GameObjects; -public sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest +internal sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest { // This test ensures that deferred deletion can be used while handling events without issue, and that deleting an // entity after deferring component removal doesn't cause any issues. diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.ComponentEvent.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.ComponentEvent.cs new file mode 100644 index 000000000..4db6224e8 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.ComponentEvent.cs @@ -0,0 +1,270 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.Reflection; +using Robust.UnitTesting.Server; + +namespace Robust.UnitTesting.Shared.GameObjects +{ + internal sealed partial class EntityEventBusTests + { + [Test] + public void SubscribeCompEvent() + { + var (bus, sim, entUid, compInstance) = EntFactory(); + var compFactory = sim.Resolve(); + + // Subscribe + int calledCount = 0; + bus.SubscribeLocalEvent(HandleTestEvent); + bus.LockSubscriptions(); + + // add a component to the system + bus.OnEntityAdded(entUid); + + var reg = compFactory.GetRegistration(CompIdx.Index()); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); + + // Raise + var evntArgs = new TestEvent(5); + bus.RaiseLocalEvent(entUid, evntArgs, true); + + // Assert + Assert.That(calledCount, Is.EqualTo(1)); + void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) + { + calledCount++; + Assert.That(uid, Is.EqualTo(entUid)); + Assert.That(component, Is.EqualTo(compInstance)); + Assert.That(args.TestNumber, Is.EqualTo(5)); + } + + } + + [Test] + public void UnsubscribeCompEvent() + { + var (bus, sim, entUid, compInstance) = EntFactory(); + var compFactory = sim.Resolve(); + + // Subscribe + int calledCount = 0; + bus.SubscribeLocalEvent(HandleTestEvent); + bus.UnsubscribeLocalEvent(); + bus.LockSubscriptions(); + + // add a component to the system + bus.OnEntityAdded(entUid); + + var reg = compFactory.GetRegistration(CompIdx.Index()); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); + + // Raise + var evntArgs = new TestEvent(5); + bus.RaiseLocalEvent(entUid, evntArgs, true); + + // Assert + Assert.That(calledCount, Is.EqualTo(0)); + void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) + { + calledCount++; + } + } + + [Test] + public void SubscribeCompLifeEvent() + { + var (bus, sim, entUid, compInstance) = EntFactory(); + var entMan = sim.Resolve(); + var fact = sim.Resolve(); + + // Subscribe + int calledCount = 0; + bus.SubscribeLocalEvent(HandleTestEvent); + bus.LockSubscriptions(); + + // Raise + bus.RaiseComponentEvent(entUid, compInstance, new ComponentInit()); + + // Assert + Assert.That(calledCount, Is.EqualTo(1)); + void HandleTestEvent(EntityUid uid, MetaDataComponent component, ComponentInit args) + { + calledCount++; + Assert.That(uid, Is.EqualTo(entUid)); + Assert.That(component, Is.EqualTo(compInstance)); + } + } + + [Test] + public void CompEventOrdered() + { + var sim = RobustServerSimulation + .NewSimulation() + .RegisterComponents(f => + { + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + }) + .InitializeInstance(); + + var entMan = sim.Resolve(); + var entUid = entMan.Spawn(); + var instA = entMan.AddComponent(entUid); + var instB = entMan.AddComponent(entUid); + var instC = entMan.AddComponent(entUid); + var bus = entMan.EventBusInternal; + bus.ClearSubscriptions(); + + var fact = sim.Resolve(); + + // Subscribe + var a = false; + var b = false; + var c = false; + + void HandlerA(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(b, Is.False, "A should run before B"); + Assert.That(c, Is.False, "A should run before C"); + + a = true; + } + + void HandlerB(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(c, Is.True, "B should run after C"); + b = true; + } + + void HandlerC(EntityUid uid, Component comp, TestEvent ev) => c = true; + + bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent), before: new []{typeof(OrderBComponent), typeof(OrderCComponent)}); + bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent), after: new []{typeof(OrderCComponent)}); + bus.SubscribeLocalEvent(HandlerC, typeof(OrderCComponent)); + bus.LockSubscriptions(); + + // add a component to the system + bus.OnEntityAdded(entUid); + + var regA = fact.GetRegistration(CompIdx.Index()); + var regB = fact.GetRegistration(CompIdx.Index()); + var regC = fact.GetRegistration(CompIdx.Index()); + + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), regC)); + + // Raise + var evntArgs = new TestEvent(5); + bus.RaiseLocalEvent(entUid, evntArgs, true); + + // Assert + Assert.That(a, Is.True, "A did not fire"); + Assert.That(b, Is.True, "B did not fire"); + Assert.That(c, Is.True, "C did not fire"); + } + + [Test] + public void CompEventLoop() + { + var sim = RobustServerSimulation + .NewSimulation() + .RegisterComponents(f => + { + f.RegisterClass(); + f.RegisterClass(); + }) + .InitializeInstance(); + + var entMan = sim.Resolve(); + var entUid = entMan.Spawn(); + var instA = entMan.AddComponent(entUid); + var instB = entMan.AddComponent(entUid); + var bus = entMan.EventBusInternal; + bus.ClearSubscriptions(); + + var fact = sim.Resolve(); + var regA = fact.GetRegistration(CompIdx.Index()); + var regB = fact.GetRegistration(CompIdx.Index()); + + var handlerACount = 0; + void HandlerA(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(handlerACount, Is.EqualTo(0)); + handlerACount++; + + // add and then remove component B + bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instB, entUid), false, default!, CompIdx.Index())); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + } + + var handlerBCount = 0; + void HandlerB(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(handlerBCount, Is.EqualTo(0)); + handlerBCount++; + + // add and then remove component A + bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instA, entUid), false, default!, CompIdx.Index())); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + } + + bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent)); + bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent)); + bus.LockSubscriptions(); + + // add a component to the system + bus.OnEntityAdded(entUid); + + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + + // Event subscriptions currently use a linked list. + // Currently expect event subscriptions to be raised in order: handlerB -> handlerA + // If a component gets removed and added again, it gets moved back to the front of the linked list. + // I.e., adding and then removing compA changes the linked list order: handlerA -> handlerB + // + // This could in principle cause the event raising code to enter an infinite loop. + // Adding and removing a comp in an event handler may seem silly but: + // - it doesn't have to be the same component if you had a chain of three or more components + // - some event handlers raise other events and can lead to convoluted chains of interactions that might inadvertently trigger something like this. + + // Raise + bus.RaiseLocalEvent(entUid, new TestEvent(0)); + + // Assert + Assert.That(handlerACount, Is.LessThanOrEqualTo(1)); + Assert.That(handlerBCount, Is.LessThanOrEqualTo(1)); + Assert.That(handlerACount+handlerBCount, Is.GreaterThan(0)); + } + + private sealed partial class DummyComponent : Component + { + } + + private sealed partial class OrderAComponent : Component + { + } + + private sealed partial class OrderBComponent : Component + { + } + + private sealed partial class OrderCComponent : Component + { + } + + private sealed class TestEvent : EntityEventArgs + { + public int TestNumber { get; } + + public TestEvent(int testNumber) + { + TestNumber = testNumber; + } + } + } +} diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.OrderedEvents.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.OrderedEvents.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.OrderedEvents.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.OrderedEvents.cs index 5b655d240..ce7dd52c3 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.OrderedEvents.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.OrderedEvents.cs @@ -7,7 +7,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects; -public sealed partial class EntityEventBusTests +internal sealed partial class EntityEventBusTests { // Explanation of what bug this is testing: // Because event ordering is keyed on system type, we have a problem. diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs similarity index 97% rename from Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs index ad8932a61..f2238dee6 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefBroadcastEvents.cs @@ -7,7 +7,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture] - public partial class EntityEventBusTests + internal sealed partial class EntityEventBusTests { [Test] public void SubscribeCompRefBroadcastEvent() @@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.GameObjects } [Reflect(false)] - public sealed class SubscribeCompRefBroadcastSystem : EntitySystem + internal sealed class SubscribeCompRefBroadcastSystem : EntitySystem { public override void Initialize() { diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefDirectedEvents.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefDirectedEvents.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefDirectedEvents.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefDirectedEvents.cs index c2f1ef3dd..ecf246f52 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefDirectedEvents.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefDirectedEvents.cs @@ -7,7 +7,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { - public partial class EntityEventBusTests + internal sealed partial class EntityEventBusTests { [Test] diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefSortedEvents.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefSortedEvents.cs similarity index 80% rename from Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefSortedEvents.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefSortedEvents.cs index 8cdcd8c78..2aff82670 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.RefSortedEvents.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.RefSortedEvents.cs @@ -7,7 +7,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { - public partial class EntityEventBusTests + internal sealed partial class EntityEventBusTests { } diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.SystemEvent.cs similarity index 95% rename from Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.SystemEvent.cs index 6dd43e6fe..1062e04f1 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityEventBusTests.SystemEvent.cs @@ -1,22 +1,27 @@ using System; -using Moq; using NUnit.Framework; using Robust.Shared.GameObjects; -using Robust.Shared.Reflection; +using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture, Parallelizable, TestOf(typeof(EntityEventBus))] - public partial class EntityEventBusTests + internal sealed partial class EntityEventBusTests { + private static (EntityEventBus Bus, ISimulation Sim, EntityUid Uid, MetaDataComponent Comp) EntFactory() + { + var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); + var entMan = sim.Resolve(); + var uid = entMan.Spawn(); + var comp = entMan.MetaQuery.Comp(uid); + var bus = entMan.EventBusInternal; + bus.ClearSubscriptions(); + return (bus, sim, uid, comp); + } + private static EntityEventBus BusFactory() { - var compFacMock = new Mock(); - var entManMock = new Mock(); - var reflectMock = new Mock(); - entManMock.SetupGet(e => e.ComponentFactory).Returns(compFacMock.Object); - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - return bus; + return EntFactory().Bus; } /// @@ -483,15 +488,15 @@ namespace Robust.UnitTesting.Shared.GameObjects Assert.That(c, Is.True, "C did not fire"); } - public sealed class SubA : IEntityEventSubscriber + internal sealed class SubA : IEntityEventSubscriber { } - public sealed class SubB : IEntityEventSubscriber + internal sealed class SubB : IEntityEventSubscriber { } - public sealed class SubC : IEntityEventSubscriber + internal sealed class SubC : IEntityEventSubscriber { } } diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityManagerCopyTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerCopyTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/EntityManagerCopyTests.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityManagerCopyTests.cs index c9e098e9c..bb0803d42 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityManagerCopyTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerCopyTests.cs @@ -8,7 +8,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects; [TestFixture] -public sealed partial class EntityManagerCopyTests +internal sealed partial class EntityManagerCopyTests { [Test] public void CopyComponentGeneric() diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityManager_Components_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManager_Components_Tests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/EntityManager_Components_Tests.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityManager_Components_Tests.cs index 44d735503..3ec3301ae 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityManager_Components_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManager_Components_Tests.cs @@ -14,7 +14,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture, Parallelizable ,TestOf(typeof(EntityManager))] - public sealed partial class EntityManager_Components_Tests + internal sealed partial class EntityManager_Components_Tests { private const string DummyLoadId = "DummyLoad"; private const string DummyLoad = $@" diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityState_Tests.cs similarity index 100% rename from Robust.UnitTesting/Shared/GameObjects/EntityState_Tests.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntityState_Tests.cs diff --git a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs b/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs index 70cee4e87..8cf31f528 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManagerOrderTest.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManagerOrderTest.cs @@ -20,7 +20,7 @@ namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture] [TestOf(typeof(EntitySystemManager))] - public sealed class EntitySystemManagerOrderTest + internal sealed class EntitySystemManagerOrderTest { private sealed class Counter { diff --git a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManager_Tests.cs similarity index 92% rename from Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs rename to Robust.Shared.IntegrationTests/GameObjects/EntitySystemManager_Tests.cs index 4244273de..8a9aa869e 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntitySystemManager_Tests.cs @@ -10,7 +10,7 @@ using Robust.Shared.IoC.Exceptions; namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture, TestOf(typeof(EntitySystemManager))] - public sealed class EntitySystemManager_Tests: RobustUnitTest + internal sealed class EntitySystemManager_Tests: OurRobustUnitTest { public abstract class ESystemBase : IEntitySystem @@ -25,16 +25,16 @@ namespace Robust.UnitTesting.Shared.GameObjects } [Virtual] public class ESystemA : ESystemBase { } - public sealed class ESystemC : ESystemA { } + internal sealed class ESystemC : ESystemA { } public abstract class ESystemBase2 : ESystemBase { } - public sealed class ESystemB : ESystemBase2 { } + internal sealed class ESystemB : ESystemBase2 { } - public sealed class ESystemDepA : ESystemBase + internal sealed class ESystemDepA : ESystemBase { [Dependency] public readonly ESystemDepB ESystemDepB = default!; } - public sealed class ESystemDepB : ESystemBase + internal sealed class ESystemDepB : ESystemBase { [Dependency] public readonly ESystemDepA ESystemDepA = default!; } diff --git a/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs b/Robust.Shared.IntegrationTests/GameObjects/GenericEntityPrint.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs rename to Robust.Shared.IntegrationTests/GameObjects/GenericEntityPrint.cs index c1fc54a88..40fc96c7c 100644 --- a/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/GenericEntityPrint.cs @@ -5,7 +5,7 @@ using System.Text; namespace Robust.UnitTesting.Shared.GameObjects; -public sealed class GenericEntityPrint +internal sealed class GenericEntityPrint { //[Test] public void Print() diff --git a/Robust.UnitTesting/Shared/GameObjects/IEntityManagerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs similarity index 100% rename from Robust.UnitTesting/Shared/GameObjects/IEntityManagerTests.cs rename to Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs diff --git a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs rename to Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs index aca41d0d8..0e90228dd 100644 --- a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs @@ -17,7 +17,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects.Systems { [TestFixture, Parallelizable] - public sealed partial class AnchoredSystemTests + internal sealed partial class AnchoredSystemTests { private const string Prototypes = @" - type: entity diff --git a/Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/TransformSystemTests.cs similarity index 100% rename from Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs rename to Robust.Shared.IntegrationTests/GameObjects/Systems/TransformSystemTests.cs diff --git a/Robust.UnitTesting/Shared/GameObjects/TransformComponent_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameObjects/TransformComponent_Tests.cs rename to Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs index 7e151e089..aeb951a11 100644 --- a/Robust.UnitTesting/Shared/GameObjects/TransformComponent_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs @@ -10,7 +10,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects { [TestFixture] - public sealed class TransformComponent_Tests + internal sealed class TransformComponent_Tests { /// /// Verify that WorldPosition and WorldRotation return the same result as the faster helper method. diff --git a/Robust.UnitTesting/Shared/GameState/AutoNetworkingTest.cs b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameState/AutoNetworkingTest.cs rename to Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs index b6986f61c..56845abec 100644 --- a/Robust.UnitTesting/Shared/GameState/AutoNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; @@ -10,7 +11,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.GameState; -public sealed partial class AutoNetworkingTests : RobustIntegrationTest +internal sealed partial class AutoNetworkingTests : RobustIntegrationTest { /// /// Does basic testing for AutoNetworkedFieldAttribute and AutoGenerateComponentStateAttribute diff --git a/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs b/Robust.Shared.IntegrationTests/GameState/ComponentStateTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs rename to Robust.Shared.IntegrationTests/GameState/ComponentStateTests.cs index 4bf73e4ee..8e2e63948 100644 --- a/Robust.UnitTesting/Shared/GameState/ComponentStateTests.cs +++ b/Robust.Shared.IntegrationTests/GameState/ComponentStateTests.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; @@ -13,7 +14,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.GameState; -public sealed partial class ComponentStateTests : RobustIntegrationTest +internal sealed partial class ComponentStateTests : RobustIntegrationTest { /// /// This tests performs a basic check to ensure that there is no issue with entity states referencing other diff --git a/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs rename to Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs index 46e1be18b..48b050625 100644 --- a/Robust.UnitTesting/Shared/GameState/DeletionNetworkingTests.cs +++ b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs @@ -18,7 +18,7 @@ namespace Robust.UnitTesting.Shared.GameState; /// /// Should help prevent the issue fixed in PR #4044 from reoccurring. /// -public sealed class DeletionNetworkingTests : RobustIntegrationTest +internal sealed class DeletionNetworkingTests : RobustIntegrationTest { [Test] public async Task DeletionNetworkingTest() diff --git a/Robust.UnitTesting/Shared/GameState/NoSharedReferencesTest.cs b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/GameState/NoSharedReferencesTest.cs rename to Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs index 8d812e0c2..9313bede3 100644 --- a/Robust.UnitTesting/Shared/GameState/NoSharedReferencesTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs @@ -7,6 +7,7 @@ using Robust.Shared.Network; using Robust.Shared.Serialization; using System.Linq; using System.Threading.Tasks; +using Robust.Shared.Analyzers; using static Robust.UnitTesting.Shared.GameState.ExampleAutogeneratedComponent; namespace Robust.UnitTesting.Shared.GameState; @@ -14,7 +15,7 @@ namespace Robust.UnitTesting.Shared.GameState; /// /// This is a test of test engine . Not a test of game engine. /// -public sealed partial class NoSharedReferencesTest : RobustIntegrationTest +internal sealed partial class NoSharedReferencesTest : RobustIntegrationTest { /// /// The test performs a basic check to ensure that there is no issue with server's object references leaking to client. diff --git a/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs b/Robust.Shared.IntegrationTests/GameState/VisibilityTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/GameState/VisibilityTest.cs rename to Robust.Shared.IntegrationTests/GameState/VisibilityTest.cs index a411c8117..30e2ef58d 100644 --- a/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/VisibilityTest.cs @@ -13,7 +13,7 @@ using Robust.Shared.Network; namespace Robust.UnitTesting.Shared.GameState; -public sealed partial class VisibilityTest : RobustIntegrationTest +internal sealed partial class VisibilityTest : RobustIntegrationTest { /// /// This tests checks that entity visibility masks are recursively applied to children. diff --git a/Robust.Shared.IntegrationTests/GlobalUsings.cs b/Robust.Shared.IntegrationTests/GlobalUsings.cs new file mode 100644 index 000000000..316ff0ec5 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Is = NUnit.Framework.Is; diff --git a/Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs b/Robust.Shared.IntegrationTests/Input/Binding/CommandBindRegistry_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs rename to Robust.Shared.IntegrationTests/Input/Binding/CommandBindRegistry_Test.cs index ea0093e2e..ba25d901d 100644 --- a/Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs +++ b/Robust.Shared.IntegrationTests/Input/Binding/CommandBindRegistry_Test.cs @@ -10,7 +10,7 @@ using Robust.Shared.Player; namespace Robust.UnitTesting.Shared.Input.Binding { [TestFixture, TestOf(typeof(CommandBindRegistry))] - public sealed class CommandBindRegistry_Test : RobustUnitTest + internal sealed class CommandBindRegistry_Test : OurRobustUnitTest { private sealed class TypeA { } diff --git a/Robust.UnitTesting/Shared/Localization/LoadLocalizationTest.cs b/Robust.Shared.IntegrationTests/Localization/LoadLocalizationTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Localization/LoadLocalizationTest.cs rename to Robust.Shared.IntegrationTests/Localization/LoadLocalizationTest.cs index a2fe3ee03..ced79aaed 100644 --- a/Robust.UnitTesting/Shared/Localization/LoadLocalizationTest.cs +++ b/Robust.Shared.IntegrationTests/Localization/LoadLocalizationTest.cs @@ -10,7 +10,7 @@ using Robust.Shared.Log; namespace Robust.UnitTesting.Shared.Localization; [TestFixture] -public sealed class LoadLocalizationTest : RobustUnitTest +internal sealed class LoadLocalizationTest : OurRobustUnitTest { private const string DuplicateTerm = @" term1 = 1 diff --git a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs b/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Localization/LocalizationTests.cs rename to Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs index 63450749a..e630f0981 100644 --- a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs +++ b/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs @@ -14,7 +14,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Localization { [TestFixture] - internal sealed class LocalizationTests : RobustUnitTest + internal sealed class LocalizationTests : OurRobustUnitTest { protected override Type[] ExtraComponents => new[] {typeof(GrammarComponent)}; diff --git a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs rename to Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs index 353a4ab49..f43ccb44f 100644 --- a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs @@ -10,7 +10,7 @@ using Robust.Shared.Serialization.Manager; namespace Robust.UnitTesting.Shared.Map { [TestFixture, Parallelizable, TestOf(typeof(EntityCoordinates))] - public sealed class EntityCoordinates_Tests : RobustUnitTest + internal sealed class EntityCoordinates_Tests : OurRobustUnitTest { [OneTimeSetUp] public void Setup() diff --git a/Robust.UnitTesting/Shared/Map/GridChunkPartition_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridChunkPartition_Tests.cs similarity index 96% rename from Robust.UnitTesting/Shared/Map/GridChunkPartition_Tests.cs rename to Robust.Shared.IntegrationTests/Map/GridChunkPartition_Tests.cs index 0e06d3ecd..0e88a13a4 100644 --- a/Robust.UnitTesting/Shared/Map/GridChunkPartition_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridChunkPartition_Tests.cs @@ -5,7 +5,7 @@ using Robust.Shared.Maths; namespace Robust.UnitTesting.Shared.Map { [TestFixture, Parallelizable, TestOf(typeof(GridChunkPartition))] - internal sealed class GridChunkPartition_Tests : RobustUnitTest + internal sealed class GridChunkPartition_Tests : OurRobustUnitTest { // origin is top left private static readonly int[] _testMiscTiles = { diff --git a/Robust.UnitTesting/Shared/Map/GridCollision_Test.cs b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/GridCollision_Test.cs rename to Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs index afb9309bb..b3726f199 100644 --- a/Robust.UnitTesting/Shared/Map/GridCollision_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs @@ -10,7 +10,7 @@ using Robust.Shared.Physics.Systems; namespace Robust.UnitTesting.Shared.Map { - public sealed class GridCollision_Test : RobustIntegrationTest + internal sealed class GridCollision_Test : RobustIntegrationTest { [Test] public async Task TestGridsCollide() diff --git a/Robust.UnitTesting/Shared/Map/GridContraction_Test.cs b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Map/GridContraction_Test.cs rename to Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs index 5bd139677..a2eb87253 100644 --- a/Robust.UnitTesting/Shared/Map/GridContraction_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs @@ -8,7 +8,7 @@ using Robust.Shared.Maths; namespace Robust.UnitTesting.Shared.Map { [TestFixture] - public sealed class GridContraction_Test : RobustIntegrationTest + internal sealed class GridContraction_Test : RobustIntegrationTest { [Test] public async Task TestGridDeletes() diff --git a/Robust.UnitTesting/Shared/Map/GridFixtures_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/GridFixtures_Tests.cs rename to Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs index 2e997c3b2..f7374c83b 100644 --- a/Robust.UnitTesting/Shared/Map/GridFixtures_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs @@ -18,7 +18,7 @@ namespace Robust.UnitTesting.Shared.Map; /// [Parallelizable(ParallelScope.All)] [TestFixture] -public sealed class GridFixtures_Tests : RobustIntegrationTest +internal sealed class GridFixtures_Tests : RobustIntegrationTest { /// /// Tests that grid fixtures match what's expected. diff --git a/Robust.UnitTesting/Shared/Map/GridMerge_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/GridMerge_Tests.cs rename to Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs index 903452834..0ff7aa9c4 100644 --- a/Robust.UnitTesting/Shared/Map/GridMerge_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs @@ -13,7 +13,7 @@ namespace Robust.UnitTesting.Shared.Map; [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] -public sealed class GridMerge_Tests +internal sealed class GridMerge_Tests { private ISimulation GetSim() { diff --git a/Robust.UnitTesting/Shared/Map/GridRotation_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/GridRotation_Tests.cs rename to Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs index 373ac2073..d73e86ff9 100644 --- a/Robust.UnitTesting/Shared/Map/GridRotation_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs @@ -10,7 +10,7 @@ using Robust.Shared.Maths; namespace Robust.UnitTesting.Shared.Map { [TestFixture] - public sealed class GridRotation_Tests : RobustIntegrationTest + internal sealed class GridRotation_Tests : RobustIntegrationTest { // Because integration tests are ten billion percent easier we'll just do all the rotation tests here. // These are mainly looking out for situations where the grid is rotated 90 / 180 degrees and we diff --git a/Robust.UnitTesting/Shared/Map/GridSplit_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Map/GridSplit_Tests.cs rename to Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs index 4e946944d..447c4ea94 100644 --- a/Robust.UnitTesting/Shared/Map/GridSplit_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs @@ -12,7 +12,7 @@ namespace Robust.UnitTesting.Shared.Map; [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] -public sealed class GridSplit_Tests +internal sealed class GridSplit_Tests { private ISimulation GetSim() { diff --git a/Robust.UnitTesting/Shared/Map/MapGridMap_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs similarity index 97% rename from Robust.UnitTesting/Shared/Map/MapGridMap_Tests.cs rename to Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs index c953b8ea3..7c6fef1c5 100644 --- a/Robust.UnitTesting/Shared/Map/MapGridMap_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs @@ -10,7 +10,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Map; [TestFixture] -public sealed class MapGridMap_Tests +internal sealed class MapGridMap_Tests { /// /// Asserts FindGrids only returns the map once. diff --git a/Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs similarity index 100% rename from Robust.UnitTesting/Shared/Map/MapGrid_Tests.cs rename to Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs diff --git a/Robust.UnitTesting/Shared/Map/MapManager_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/MapManager_Tests.cs rename to Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs index b7fc1c257..12d4a4604 100644 --- a/Robust.UnitTesting/Shared/Map/MapManager_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs @@ -8,7 +8,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Map { [TestFixture, TestOf(typeof(MapManager))] - public sealed class MapManagerTests + internal sealed class MapManagerTests { private static ISimulation SimulationFactory() { diff --git a/Robust.UnitTesting/Shared/Map/MapPauseTests.cs b/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs similarity index 100% rename from Robust.UnitTesting/Shared/Map/MapPauseTests.cs rename to Robust.Shared.IntegrationTests/Map/MapPauseTests.cs diff --git a/Robust.UnitTesting/Shared/Map/Query_Tests.cs b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/Query_Tests.cs rename to Robust.Shared.IntegrationTests/Map/Query_Tests.cs index 22d229c72..ed6b1f369 100644 --- a/Robust.UnitTesting/Shared/Map/Query_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs @@ -11,7 +11,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Map; [TestFixture] -public sealed class Query_Tests +internal sealed class Query_Tests { private static readonly TestCaseData[] Box2Data = new[] { diff --git a/Robust.UnitTesting/Shared/Map/SingleTileRemoveTest.cs b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Map/SingleTileRemoveTest.cs rename to Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs index 7a2a2b4fb..ada8ffa7d 100644 --- a/Robust.UnitTesting/Shared/Map/SingleTileRemoveTest.cs +++ b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs @@ -13,7 +13,7 @@ using Robust.Shared.Player; namespace Robust.UnitTesting.Shared.Map; -public sealed class GridDeleteSingleTileRemoveTestTest : RobustIntegrationTest +internal sealed class GridDeleteSingleTileRemoveTestTest : RobustIntegrationTest { /// /// Spawns a simple 1-tile grid with an entity on it, and then sets the tile to "space". diff --git a/Robust.UnitTesting/Shared/Networking/DisconnectTest.cs b/Robust.Shared.IntegrationTests/Networking/DisconnectTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Networking/DisconnectTest.cs rename to Robust.Shared.IntegrationTests/Networking/DisconnectTest.cs index cd9e19f82..0bb12a717 100644 --- a/Robust.UnitTesting/Shared/Networking/DisconnectTest.cs +++ b/Robust.Shared.IntegrationTests/Networking/DisconnectTest.cs @@ -16,7 +16,7 @@ using sIPlayerManager = Robust.Server.Player.IPlayerManager; namespace Robust.UnitTesting.Shared.Networking; -public sealed class DisconnectTest : RobustIntegrationTest +internal sealed class DisconnectTest : RobustIntegrationTest { /// /// Check that client disconnection works as expected. This is effectively a test of diff --git a/Robust.Shared.IntegrationTests/OurRobustUnitTest.cs b/Robust.Shared.IntegrationTests/OurRobustUnitTest.cs new file mode 100644 index 000000000..781f43bf2 --- /dev/null +++ b/Robust.Shared.IntegrationTests/OurRobustUnitTest.cs @@ -0,0 +1,11 @@ +using System.Reflection; + +namespace Robust.UnitTesting.Shared; + +internal abstract class OurRobustUnitTest : RobustUnitTest +{ + protected override Assembly[] GetContentAssemblies() + { + return [typeof(OurRobustUnitTest).Assembly]; + } +} diff --git a/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs rename to Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs index a020ad2ae..db24213e2 100644 --- a/Robust.UnitTesting/Shared/Physics/BroadphaseNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs @@ -17,7 +17,7 @@ using Robust.Shared.Player; namespace Robust.UnitTesting.Shared.Physics; -public sealed class BroadphaseNetworkingTest : RobustIntegrationTest +internal sealed class BroadphaseNetworkingTest : RobustIntegrationTest { /// /// Check that the transform/broadphase is properly networked when a player moves to a newly spawned map/grid. diff --git a/Robust.UnitTesting/Shared/Physics/Broadphase_Test.cs b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/Broadphase_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs index d743882d3..cb87f2dba 100644 --- a/Robust.UnitTesting/Shared/Physics/Broadphase_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs @@ -16,7 +16,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class Broadphase_Test +internal sealed class Broadphase_Test { /// /// Tests that spawned static ents properly collide with entities in range. diff --git a/Robust.UnitTesting/Shared/Physics/CollisionPredictionTest.cs b/Robust.Shared.IntegrationTests/Physics/CollisionPredictionTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/CollisionPredictionTest.cs rename to Robust.Shared.IntegrationTests/Physics/CollisionPredictionTest.cs index 4bd87bf6d..5c82d264b 100644 --- a/Robust.UnitTesting/Shared/Physics/CollisionPredictionTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/CollisionPredictionTest.cs @@ -27,7 +27,7 @@ namespace Robust.UnitTesting.Shared.Physics; /// I.e., the assumption is that if a collision results in changes to data on a component, then that data will already /// have been sent to clients in the component's state, so we don't want to "double count" collisions. /// -public sealed class CollisionPredictionTest : RobustIntegrationTest +internal sealed class CollisionPredictionTest : RobustIntegrationTest { private static readonly string Prototypes = @" - type: entity @@ -405,7 +405,7 @@ public sealed class CollisionPredictionTest : RobustIntegrationTest } [RegisterComponent, NetworkedComponent] -public sealed partial class CollisionPredictionTestComponent : Component +internal sealed partial class CollisionPredictionTestComponent : Component { public bool IsTouching; public bool WasTouching; @@ -414,20 +414,20 @@ public sealed partial class CollisionPredictionTestComponent : Component public GameTick StopTick; [Serializable, NetSerializable] - public sealed class State(bool isTouching) : ComponentState + internal sealed class State(bool isTouching) : ComponentState { public bool IsTouching = isTouching; } } [Serializable, NetSerializable] -public sealed class CollisionTestMoveEvent(NetEntity ent, MapCoordinates coords) : EntityEventArgs +internal sealed class CollisionTestMoveEvent(NetEntity ent, MapCoordinates coords) : EntityEventArgs { public NetEntity Ent = ent; public MapCoordinates Coords = coords; } -public sealed class CollisionPredictionTestSystem : EntitySystem +internal sealed class CollisionPredictionTestSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _xform = default!; [Dependency] private readonly IGameTiming _timing = default!; diff --git a/Robust.UnitTesting/Shared/Physics/CollisionWake_Test.cs b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/CollisionWake_Test.cs rename to Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs index 19dad0e3e..a4109827a 100644 --- a/Robust.UnitTesting/Shared/Physics/CollisionWake_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs @@ -10,7 +10,7 @@ using Robust.Shared.Physics.Components; namespace Robust.UnitTesting.Shared.Physics { [TestFixture, TestOf(typeof(CollisionWakeSystem))] - public sealed class CollisionWake_Test : RobustIntegrationTest + internal sealed class CollisionWake_Test : RobustIntegrationTest { private const string Prototype = @" - type: entity diff --git a/Robust.UnitTesting/Shared/Physics/Collision_Test.cs b/Robust.Shared.IntegrationTests/Physics/Collision_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/Collision_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Collision_Test.cs index b74615757..ca615b24e 100644 --- a/Robust.UnitTesting/Shared/Physics/Collision_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Collision_Test.cs @@ -36,7 +36,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class Collision_Test +internal sealed class Collision_Test { [Test] public void TestHardCollidable() diff --git a/Robust.UnitTesting/Shared/Physics/FixtureShape_Test.cs b/Robust.Shared.IntegrationTests/Physics/FixtureShape_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/FixtureShape_Test.cs rename to Robust.Shared.IntegrationTests/Physics/FixtureShape_Test.cs index 841345486..2615babbb 100644 --- a/Robust.UnitTesting/Shared/Physics/FixtureShape_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/FixtureShape_Test.cs @@ -10,7 +10,7 @@ namespace Robust.UnitTesting.Shared.Physics { [TestFixture] [TestOf(typeof(FixtureSystem))] - public sealed class FixtureShape_Test : RobustUnitTest + internal sealed class FixtureShape_Test : OurRobustUnitTest { private FixtureSystem _shapeManager = default!; diff --git a/Robust.UnitTesting/Shared/Physics/Fixtures_Test.cs b/Robust.Shared.IntegrationTests/Physics/Fixtures_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/Fixtures_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Fixtures_Test.cs index 81b8d0e65..f0d3be336 100644 --- a/Robust.UnitTesting/Shared/Physics/Fixtures_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Fixtures_Test.cs @@ -12,7 +12,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class Fixtures_Test +internal sealed class Fixtures_Test { [Test] public void SetDensity() diff --git a/Robust.UnitTesting/Shared/Physics/GridDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/GridDeletion_Test.cs rename to Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs index a902184f3..20b35cdf7 100644 --- a/Robust.UnitTesting/Shared/Physics/GridDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs @@ -17,7 +17,7 @@ namespace Robust.UnitTesting.Shared.Physics; /// Mainly useful for grid dynamic tree. /// [TestFixture] -public sealed class GridDeletion_Test : RobustIntegrationTest +internal sealed class GridDeletion_Test : RobustIntegrationTest { [Test] public async Task GridDeletionTest() diff --git a/Robust.UnitTesting/Shared/Physics/GridMovement_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/GridMovement_Test.cs rename to Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs index 0dddfa2fa..692fad565 100644 --- a/Robust.UnitTesting/Shared/Physics/GridMovement_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs @@ -13,7 +13,7 @@ using Robust.Shared.Physics.Systems; namespace Robust.UnitTesting.Shared.Physics; [TestFixture, TestOf(typeof(SharedBroadphaseSystem))] -public sealed class GridMovement_Test : RobustIntegrationTest +internal sealed class GridMovement_Test : RobustIntegrationTest { [Test] public async Task TestFindGridContacts() diff --git a/Robust.UnitTesting/Shared/Physics/GridReparentVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/GridReparentVelocity_Test.cs rename to Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs index 0e3af63a9..a8c2d11f2 100644 --- a/Robust.UnitTesting/Shared/Physics/GridReparentVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs @@ -14,7 +14,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture, TestOf(typeof(SharedPhysicsSystem))] -public sealed class GridReparentVelocity_Test +internal sealed class GridReparentVelocity_Test { private ISimulation _sim = default!; private IEntitySystemManager _systems = default!; diff --git a/Robust.UnitTesting/Shared/Physics/JointDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/JointDeletion_Test.cs rename to Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs index 91cfa16b6..e7ebeb642 100644 --- a/Robust.UnitTesting/Shared/Physics/JointDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs @@ -14,7 +14,7 @@ using Robust.Shared.Physics.Systems; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class JointDeletion_Test : RobustIntegrationTest +internal sealed class JointDeletion_Test : RobustIntegrationTest { [Test] public async Task JointDeletionTest() diff --git a/Robust.UnitTesting/Shared/Physics/Joints_Test.cs b/Robust.Shared.IntegrationTests/Physics/Joints_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/Joints_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Joints_Test.cs index 7d57cc244..887da6916 100644 --- a/Robust.UnitTesting/Shared/Physics/Joints_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Joints_Test.cs @@ -15,7 +15,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture, TestOf(typeof(JointSystem))] -public sealed class Joints_Test +internal sealed class Joints_Test { [Test] public void JointsRelayTest() diff --git a/Robust.UnitTesting/Shared/Physics/ManifoldManager_Test.cs b/Robust.Shared.IntegrationTests/Physics/ManifoldManager_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/ManifoldManager_Test.cs rename to Robust.Shared.IntegrationTests/Physics/ManifoldManager_Test.cs index f0fc4d027..025fef85c 100644 --- a/Robust.UnitTesting/Shared/Physics/ManifoldManager_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/ManifoldManager_Test.cs @@ -11,7 +11,7 @@ namespace Robust.UnitTesting.Shared.Physics { [TestFixture] [TestOf(typeof(IManifoldManager))] - public sealed class ManifoldManager_Test : RobustUnitTest + internal sealed class ManifoldManager_Test : OurRobustUnitTest { private IManifoldManager _manifoldManager = default!; diff --git a/Robust.UnitTesting/Shared/Physics/MapVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/MapVelocity_Test.cs rename to Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs index 36d3d5cdf..4067f53d1 100644 --- a/Robust.UnitTesting/Shared/Physics/MapVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs @@ -9,7 +9,7 @@ using Robust.Shared.Physics.Systems; namespace Robust.UnitTesting.Shared.Physics { - public sealed class MapVelocity_Test : RobustIntegrationTest + internal sealed class MapVelocity_Test : RobustIntegrationTest { private const string DummyEntity = "Dummy"; diff --git a/Robust.UnitTesting/Shared/Physics/PhysicsComponent_Test.cs b/Robust.Shared.IntegrationTests/Physics/PhysicsComponent_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/PhysicsComponent_Test.cs rename to Robust.Shared.IntegrationTests/Physics/PhysicsComponent_Test.cs index 59352bf2e..70d9b64cd 100644 --- a/Robust.UnitTesting/Shared/Physics/PhysicsComponent_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/PhysicsComponent_Test.cs @@ -16,7 +16,7 @@ namespace Robust.UnitTesting.Shared.Physics { [TestFixture] [TestOf(typeof(PhysicsComponent))] - public sealed class PhysicsComponent_Test : RobustIntegrationTest + internal sealed class PhysicsComponent_Test : RobustIntegrationTest { [Test] public async Task TestPointLinearImpulse() diff --git a/Robust.UnitTesting/Shared/Physics/PhysicsHull_Test.cs b/Robust.Shared.IntegrationTests/Physics/PhysicsHull_Test.cs similarity index 100% rename from Robust.UnitTesting/Shared/Physics/PhysicsHull_Test.cs rename to Robust.Shared.IntegrationTests/Physics/PhysicsHull_Test.cs diff --git a/Robust.UnitTesting/Shared/Physics/RayCast_Test.cs b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/RayCast_Test.cs rename to Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs index caec0b8bc..1672d94c3 100644 --- a/Robust.UnitTesting/Shared/Physics/RayCast_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs @@ -15,7 +15,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class RayCast_Test +internal sealed class RayCast_Test { private static TestCaseData[] _rayCases = { diff --git a/Robust.UnitTesting/Shared/Physics/RecursiveUpdateTest.cs b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/RecursiveUpdateTest.cs rename to Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs index dd914a1ca..4b0613768 100644 --- a/Robust.UnitTesting/Shared/Physics/RecursiveUpdateTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs @@ -11,7 +11,7 @@ using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class RecursiveUpdateTest +internal sealed class RecursiveUpdateTest { /// /// Check that the broadphase updates if a an entity is a child of an entity that is in a container. diff --git a/Robust.UnitTesting/Shared/Physics/ShapeAABB_Test.cs b/Robust.Shared.IntegrationTests/Physics/ShapeAABB_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/ShapeAABB_Test.cs rename to Robust.Shared.IntegrationTests/Physics/ShapeAABB_Test.cs index b184b85a7..5b319b711 100644 --- a/Robust.UnitTesting/Shared/Physics/ShapeAABB_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/ShapeAABB_Test.cs @@ -9,7 +9,7 @@ using Robust.Shared.Physics.Collision.Shapes; namespace Robust.UnitTesting.Shared.Physics { [TestFixture] - public sealed class ShapeAABB_Test : RobustUnitTest + internal sealed class ShapeAABB_Test : OurRobustUnitTest { private Transform _transform; private Transform _rotatedTransform; diff --git a/Robust.UnitTesting/Shared/Physics/Shape_Test.cs b/Robust.Shared.IntegrationTests/Physics/Shape_Test.cs similarity index 94% rename from Robust.UnitTesting/Shared/Physics/Shape_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Shape_Test.cs index 331d12fe2..fe0777787 100644 --- a/Robust.UnitTesting/Shared/Physics/Shape_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Shape_Test.cs @@ -7,7 +7,7 @@ namespace Robust.UnitTesting.Shared.Physics { [TestFixture] [TestOf(typeof(IPhysShape))] - public sealed class Shape_Test : RobustUnitTest + internal sealed class Shape_Test : OurRobustUnitTest { [Test] public void TestPolyNormals() diff --git a/Robust.UnitTesting/Shared/Physics/Stack_Test.cs b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Physics/Stack_Test.cs rename to Robust.Shared.IntegrationTests/Physics/Stack_Test.cs index bf3f292e8..eb90f6634 100644 --- a/Robust.UnitTesting/Shared/Physics/Stack_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs @@ -43,7 +43,7 @@ using Robust.Shared.Physics.Systems; namespace Robust.UnitTesting.Shared.Physics; [TestFixture] -public sealed class PhysicsTestBedTest : RobustIntegrationTest +internal sealed class PhysicsTestBedTest : RobustIntegrationTest { [Test] public async Task TestBoxStack() diff --git a/Robust.Shared.IntegrationTests/Physics/TransformTest.cs b/Robust.Shared.IntegrationTests/Physics/TransformTest.cs new file mode 100644 index 000000000..7b46954c1 --- /dev/null +++ b/Robust.Shared.IntegrationTests/Physics/TransformTest.cs @@ -0,0 +1,40 @@ +using System; +using System.Numerics; +using System.Runtime.Intrinsics; +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Physics; + +namespace Robust.UnitTesting.Shared.Physics; + +[TestFixture] +[TestOf(typeof(Transform))] +internal sealed class TransformTest +{ + private static (Vector2 V, Transform T, Vector2 Exp)[] _vecMulData = + [ + (Vector2.Zero, new Transform(Vector2.Zero, 0), Exp: Vector2.Zero), + (Vector2.Zero, new Transform(Vector2.One, 0), Exp: Vector2.One), + (Vector2.Zero, new Transform(Vector2.One, Angle.FromDegrees(45)), Vector2.One), + (Vector2.One, new Transform(Vector2.Zero, Angle.FromDegrees(45)), new(0, MathF.Sqrt(2))), + (Vector2.One, new Transform(Vector2.One, Angle.FromDegrees(45)), new(1, 1+MathF.Sqrt(2))), + (new Vector2(2f, 1f), new Transform(Vector2.One, Angle.FromDegrees(45)), new(1.707107f, 3.12132f)), + ]; + + [Test] + public void TestVectorMul([ValueSource(nameof(_vecMulData))] (Vector2 V, Transform T, Vector2 Exp) dat) + { + var result = Transform.Mul(dat.T, dat.V); + Assert.That(result, Is.Approximately(dat.Exp, 0.001f)); + } + + [Test] + public void TestVectorMulSimd([ValueSource(nameof(_vecMulData))] (Vector2 V, Transform T, Vector2 Exp) dat) + { + var x = Vector128.Create(dat.V.X); + var y = Vector128.Create(dat.V.Y); + Transform.MulSimd(dat.T, x, y, out var xOut, out var yOut); + Assert.That(xOut, Is.Approximately(Vector128.Create(dat.Exp[0]), 0.0001f)); + Assert.That(yOut, Is.Approximately(Vector128.Create(dat.Exp[1]), 0.0001f)); + } +} diff --git a/Robust.UnitTesting/Shared/Physics/VerticesSimplifier_Test.cs b/Robust.Shared.IntegrationTests/Physics/VerticesSimplifier_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Physics/VerticesSimplifier_Test.cs rename to Robust.Shared.IntegrationTests/Physics/VerticesSimplifier_Test.cs index 2b266c98f..d0ce7acd2 100644 --- a/Robust.UnitTesting/Shared/Physics/VerticesSimplifier_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/VerticesSimplifier_Test.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Physics { [TestFixture, Parallelizable] [TestOf(typeof(IVerticesSimplifier))] - public sealed class VerticesSimplifier_Test : RobustUnitTest + internal sealed class VerticesSimplifier_Test : OurRobustUnitTest { /* * Collinear tests diff --git a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs rename to Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs index e0e90107c..22fa71473 100644 --- a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs @@ -12,7 +12,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.Prototypes { [TestFixture] - public sealed class HotReloadTest : RobustUnitTest + internal sealed class HotReloadTest : OurRobustUnitTest { private const string DummyId = "Dummy"; public const string HotReloadTestComponentOneId = "HotReloadTestOne"; @@ -101,13 +101,13 @@ namespace Robust.UnitTesting.Shared.Prototypes } } - public sealed partial class HotReloadTestOneComponent : Component + internal sealed partial class HotReloadTestOneComponent : Component { [DataField("value")] public int Value { get; private set; } } - public sealed partial class HotReloadTestTwoComponent : Component + internal sealed partial class HotReloadTestTwoComponent : Component { } } diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeManagerCategoriesTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs rename to Robust.Shared.IntegrationTests/Prototypes/PrototypeManagerCategoriesTest.cs index 257373531..c4fd6c01c 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeManagerCategoriesTest.cs @@ -11,7 +11,7 @@ namespace Robust.UnitTesting.Shared.Prototypes; [UsedImplicitly] [TestFixture] -public sealed class PrototypeManagerCategoriesTest : RobustUnitTest +internal sealed class PrototypeManagerCategoriesTest : OurRobustUnitTest { private IPrototypeManager _protoMan = default!; @@ -265,8 +265,8 @@ public sealed class PrototypeManagerCategoriesTest : RobustUnitTest "; } -public sealed partial class AutoCategoryComponent : Component; +internal sealed partial class AutoCategoryComponent : Component; // TODO test-local IReflectionManager // [EntityCategory("Auto")] -// public sealed partial class AttributeAutoCategoryComponent : Component; +// internal sealed partial class AttributeAutoCategoryComponent : Component; diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeManager_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs rename to Robust.Shared.IntegrationTests/Prototypes/PrototypeManager_Test.cs index 7d16de150..4b8ce1354 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeManager_Test.cs @@ -15,7 +15,7 @@ namespace Robust.UnitTesting.Shared.Prototypes { [UsedImplicitly] [TestFixture] - public sealed class PrototypeManager_Test : RobustUnitTest + internal sealed partial class PrototypeManager_Test : OurRobustUnitTest { private const string FakeWrenchProtoId = "wrench"; private const string YamlTesterProtoId = "yamltester"; @@ -151,7 +151,7 @@ namespace Robust.UnitTesting.Shared.Prototypes [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] public string[]? Parents { get; private set; } [AbstractDataField] - public bool Abstract { get; } + public bool Abstract { get; private set; } } public enum YamlTestEnum : byte @@ -223,7 +223,7 @@ namespace Robust.UnitTesting.Shared.Prototypes name: {LoadStringTestDummyId}"; } - public sealed partial class TestBasicPrototypeComponent : Component + internal sealed partial class TestBasicPrototypeComponent : Component { [DataField("foo")] public string Foo = null!; diff --git a/Robust.UnitTesting/Shared/Reflection/ReflectionManager_Test.cs b/Robust.Shared.IntegrationTests/Reflection/ReflectionManager_Test.cs similarity index 90% rename from Robust.UnitTesting/Shared/Reflection/ReflectionManager_Test.cs rename to Robust.Shared.IntegrationTests/Reflection/ReflectionManager_Test.cs index 1dca766f3..47fd71c71 100644 --- a/Robust.UnitTesting/Shared/Reflection/ReflectionManager_Test.cs +++ b/Robust.Shared.IntegrationTests/Reflection/ReflectionManager_Test.cs @@ -6,13 +6,13 @@ using JetBrains.Annotations; namespace Robust.UnitTesting.Shared.Reflection { - public sealed class ReflectionManagerTest : ReflectionManager + internal sealed class ReflectionManagerTest : ReflectionManager { protected override IEnumerable TypePrefixes => new[] { "", "Robust.UnitTesting.", "Robust.Server.", "Robust.Shared." }; } [TestFixture] - public sealed class ReflectionManager_Test : RobustUnitTest + internal sealed class ReflectionManager_Test : OurRobustUnitTest { protected override void OverrideIoC() { @@ -59,12 +59,12 @@ namespace Robust.UnitTesting.Shared.Reflection public interface IReflectionManagerTest { } // These two pass like normal. - public sealed class TestClass1 : IReflectionManagerTest { } - public sealed class TestClass2 : IReflectionManagerTest { } + internal sealed class TestClass1 : IReflectionManagerTest { } + internal sealed class TestClass2 : IReflectionManagerTest { } // These two should both NOT be passed. [Reflect(false)] - public sealed class TestClass3 : IReflectionManagerTest { } + internal sealed class TestClass3 : IReflectionManagerTest { } public abstract class TestClass4 : IReflectionManagerTest { } [Test] @@ -97,14 +97,14 @@ namespace Robust.UnitTesting.Shared.Reflection } } - public sealed class TestGetType1 { } + internal sealed class TestGetType1 { } public abstract class TestGetType2 { } public interface ITestGetType3 { } public enum TestParseEnumReferenceType1 { Value } [UsedImplicitly] - public sealed class TestParseEnumReferenceTypeClass + internal sealed class TestParseEnumReferenceTypeClass { public enum TestParseEnumReferenceType2 { InnerValue } } diff --git a/Robust.UnitTesting/Shared/Resources/ContentFileReadTest.cs b/Robust.Shared.IntegrationTests/Resources/ContentFileReadTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Resources/ContentFileReadTest.cs rename to Robust.Shared.IntegrationTests/Resources/ContentFileReadTest.cs index 7685c4c3d..3842a06e1 100644 --- a/Robust.UnitTesting/Shared/Resources/ContentFileReadTest.cs +++ b/Robust.Shared.IntegrationTests/Resources/ContentFileReadTest.cs @@ -6,7 +6,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Resources; -public sealed class ContentFileReadTest : RobustIntegrationTest +internal sealed class ContentFileReadTest : RobustIntegrationTest { [Test] [TestOf(typeof(ResourceManager))] diff --git a/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj new file mode 100644 index 000000000..8d287bd29 --- /dev/null +++ b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj @@ -0,0 +1,39 @@ + + + + + enable + false + ../bin/Shared.IntegrationTests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Shared/Serialization/CompositionTest.cs b/Robust.Shared.IntegrationTests/Serialization/CompositionTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/CompositionTest.cs rename to Robust.Shared.IntegrationTests/Serialization/CompositionTest.cs index 0e076dec9..a85cd94ea 100644 --- a/Robust.UnitTesting/Shared/Serialization/CompositionTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/CompositionTest.cs @@ -6,7 +6,7 @@ using Robust.Shared.Serialization.Markdown.Mapping; namespace Robust.UnitTesting.Shared.Serialization; [TestFixture] -public sealed partial class CompositionTest : SerializationTest +internal sealed partial class CompositionTest : OurSerializationTest { [DataDefinition] private sealed partial class CompositionTestClass diff --git a/Robust.UnitTesting/Shared/Serialization/DataRecordTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs similarity index 91% rename from Robust.UnitTesting/Shared/Serialization/DataRecordTest.cs rename to Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs index f3f7c8c5e..7f160205d 100644 --- a/Robust.UnitTesting/Shared/Serialization/DataRecordTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs @@ -9,28 +9,28 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization; -public sealed partial class DataRecordTest : SerializationTest +public sealed partial class DataRecordTest : OurSerializationTest { [DataRecord] - public record TwoIntRecord(int aTest, int AnotherTest); + public partial record TwoIntRecord(int aTest, int AnotherTest); [DataRecord] - public record OneByteOneDefaultIntRecord(byte A, int B = 5); + public partial record OneByteOneDefaultIntRecord(byte A, int B = 5); [DataRecord] - public record OneLongRecord(long A); + public partial record OneLongRecord(long A); [DataRecord] - public record OneLongDefaultRecord(long A = 5); + public partial record OneLongDefaultRecord(long A = 5); [DataRecord] - public record OneULongRecord(ulong A); + public partial record OneULongRecord(ulong A); [PrototypeRecord("emptyTestPrototypeRecord")] - public record PrototypeRecord([field: IdDataField] string ID) : IPrototype; + public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototype; [DataRecord] - public record IntStructHolder(IntStruct Struct); + public partial record IntStructHolder(IntStruct Struct); [DataDefinition] public partial struct IntStruct @@ -44,13 +44,13 @@ public sealed partial class DataRecordTest : SerializationTest } [DataRecord] - public record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2); + public partial record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2); [DataRecord] - public record struct DataRecordStruct(IntStruct Struct, string String, int Integer); + public partial record struct DataRecordStruct(IntStruct Struct, string String, int Integer); [DataRecord] - public record struct DataRecordWithProperties + public partial record struct DataRecordWithProperties { public Vector2 Position; public int Foo { get; } @@ -59,7 +59,7 @@ public sealed partial class DataRecordTest : SerializationTest } [DataRecord] - public readonly record struct ReadonlyDataRecord + public readonly partial record struct ReadonlyDataRecord { public readonly Vector2 Position; public int Foo { get; } diff --git a/Robust.UnitTesting/Shared/Serialization/DataStructTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs similarity index 89% rename from Robust.UnitTesting/Shared/Serialization/DataStructTest.cs rename to Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs index 1eb60fff2..d9f62d1cc 100644 --- a/Robust.UnitTesting/Shared/Serialization/DataStructTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs @@ -4,7 +4,7 @@ using Robust.Shared.Serialization.Markdown.Mapping; namespace Robust.UnitTesting.Shared.Serialization; -public sealed partial class DataStructTest : SerializationTest +internal sealed partial class DataStructTest : OurSerializationTest { [DataDefinition] public partial struct DefaultIntDataStruct diff --git a/Robust.UnitTesting/Shared/Serialization/IncludeTest.cs b/Robust.Shared.IntegrationTests/Serialization/IncludeTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Serialization/IncludeTest.cs rename to Robust.Shared.IntegrationTests/Serialization/IncludeTest.cs index e71d9c484..c0b99f93c 100644 --- a/Robust.UnitTesting/Shared/Serialization/IncludeTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/IncludeTest.cs @@ -8,7 +8,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization; [TestFixture] -public sealed partial class IncludeTest : RobustUnitTest +internal sealed partial class IncludeTest : OurRobustUnitTest { [DataDefinition] private sealed partial class ReadWriteTestDataDefinition diff --git a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs b/Robust.Shared.IntegrationTests/Serialization/InheritanceSerializationTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs rename to Robust.Shared.IntegrationTests/Serialization/InheritanceSerializationTest.cs index 6a24fbd6f..cbc725b36 100644 --- a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/InheritanceSerializationTest.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -12,7 +13,7 @@ namespace Robust.UnitTesting.Shared.Serialization { [TestFixture] [TestOf(typeof(DataDefinition))] - public sealed partial class InheritanceSerializationTest : RobustUnitTest + internal sealed partial class InheritanceSerializationTest : OurRobustUnitTest { private const string BaseEntityId = "BaseEntity"; private const string InheritorEntityId = "InheritorEntityId"; @@ -97,7 +98,7 @@ namespace Robust.UnitTesting.Shared.Serialization [DataField("inheritorField")] public string? InheritorField; } - public sealed partial class TestFinalComponent : TestInheritorComponent + internal sealed partial class TestFinalComponent : TestInheritorComponent { [DataField("finalField")] public string? FinalField; diff --git a/Robust.UnitTesting/Shared/Serialization/NetSerializableAttribute_Test.cs b/Robust.Shared.IntegrationTests/Serialization/NetSerializableAttribute_Test.cs similarity index 92% rename from Robust.UnitTesting/Shared/Serialization/NetSerializableAttribute_Test.cs rename to Robust.Shared.IntegrationTests/Serialization/NetSerializableAttribute_Test.cs index dcf12a38f..5b7608c85 100644 --- a/Robust.UnitTesting/Shared/Serialization/NetSerializableAttribute_Test.cs +++ b/Robust.Shared.IntegrationTests/Serialization/NetSerializableAttribute_Test.cs @@ -7,7 +7,7 @@ using Robust.Shared.Serialization; namespace Robust.UnitTesting.Shared.Serialization { [TestFixture] - sealed class NetSerializableAttribute_Test : RobustUnitTest + internal sealed class NetSerializableAttribute_Test : OurRobustUnitTest { private IReflectionManager _reflection = default!; diff --git a/Robust.UnitTesting/Shared/Serialization/NetSerializer_Test.cs b/Robust.Shared.IntegrationTests/Serialization/NetSerializer_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Serialization/NetSerializer_Test.cs rename to Robust.Shared.IntegrationTests/Serialization/NetSerializer_Test.cs index 3f8dac33e..ed7954d58 100644 --- a/Robust.UnitTesting/Shared/Serialization/NetSerializer_Test.cs +++ b/Robust.Shared.IntegrationTests/Serialization/NetSerializer_Test.cs @@ -13,7 +13,7 @@ namespace Robust.UnitTesting.Shared.Serialization // Tests NetSerializer itself because we have specific modifications. // e.g. (at the time of writing) list serialization being more compact. [Parallelizable(ParallelScope.All)] - public sealed class NetSerializer_Test + internal sealed class NetSerializer_Test { public static readonly List?[] ListValues = { diff --git a/Robust.Shared.IntegrationTests/Serialization/OurSerializationTest.cs b/Robust.Shared.IntegrationTests/Serialization/OurSerializationTest.cs new file mode 100644 index 000000000..6dfa44913 --- /dev/null +++ b/Robust.Shared.IntegrationTests/Serialization/OurSerializationTest.cs @@ -0,0 +1,11 @@ +using System.Reflection; + +namespace Robust.UnitTesting.Shared.Serialization; + +public abstract class OurSerializationTest : SerializationTest +{ + protected override Assembly[] GetContentAssemblies() + { + return [typeof(OurSerializationTest).Assembly, ..base.GetContentAssemblies()]; + } +} diff --git a/Robust.UnitTesting/Shared/Serialization/PropertyAndFieldDefinitionTest.cs b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/PropertyAndFieldDefinitionTest.cs rename to Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs index 601251c8a..641358f25 100644 --- a/Robust.UnitTesting/Shared/Serialization/PropertyAndFieldDefinitionTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs @@ -12,7 +12,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Serialization { - public sealed partial class PropertyAndFieldDefinitionTest : SerializationTest + internal sealed partial class PropertyAndFieldDefinitionTest : OurSerializationTest { private const string GetOnlyPropertyName = "GetOnlyProperty"; private const string GetOnlyPropertyFieldTargetedName = "GetOnlyPropertyFieldTargeted"; @@ -110,7 +110,7 @@ namespace Robust.UnitTesting.Shared.Serialization } [Robust.Shared.Serialization.Manager.Attributes.DataDefinition] - public sealed partial class PropertyAndFieldDefinitionTestDefinition + internal sealed partial class PropertyAndFieldDefinitionTestDefinition { [DataField(GetOnlyPropertyName)] public int GetOnlyProperty { get; private set; } diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationPriorityTest.cs similarity index 92% rename from Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationPriorityTest.cs index 80817aeb6..57b74aed0 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationPriorityTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationPriorityTest.cs @@ -13,7 +13,7 @@ using YamlDotNet.RepresentationModel; namespace Robust.UnitTesting.Shared.Serialization { - public sealed class SerializationPriorityTest : RobustUnitTest + internal sealed class SerializationPriorityTest : OurRobustUnitTest { [Test] public void Test() @@ -41,7 +41,7 @@ namespace Robust.UnitTesting.Shared.Serialization } } - public sealed partial class PriorityTestComponent : Component, ISerializationHooks + internal sealed partial class PriorityTestComponent : Component, ISerializationHooks { public readonly List Strings = new() {string.Empty, string.Empty, string.Empty}; diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationShutdownTest.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationShutdownTest.cs similarity index 85% rename from Robust.UnitTesting/Shared/Serialization/SerializationShutdownTest.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationShutdownTest.cs index b43dce2aa..6504e626d 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationShutdownTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationShutdownTest.cs @@ -4,7 +4,7 @@ namespace Robust.UnitTesting.Shared.Serialization { [TestFixture] [NonParallelizable] - public sealed class SerializationShutdownTest : SerializationTest + internal sealed class SerializationShutdownTest : OurSerializationTest { [Test] public void SerializationInitializeShutdownInitializeTest() diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/CommunitaryLungTest.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/CommunitaryLungTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/CommunitaryLungTest.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/CommunitaryLungTest.cs index dfd68cba5..1c1be2151 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/CommunitaryLungTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/CommunitaryLungTest.cs @@ -5,7 +5,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; [TestFixture] -public sealed partial class CommunitaryLungTest : SerializationTest +internal sealed partial class CommunitaryLungTest : OurSerializationTest { [Test] public void Test() diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.Resources.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.Resources.cs similarity index 98% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.Resources.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.Resources.cs index 6cd1911cd..bd8dc7d9d 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.Resources.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.Resources.cs @@ -12,7 +12,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; -public sealed partial class DataDefinitionTests +internal sealed partial class DataDefinitionTests { private const int SerializerReturnInt = 424; private static readonly DataDummyStruct SerializerReturnStruct = new (){ Value = "SerializerReturn" }; @@ -22,7 +22,7 @@ public sealed partial class DataDefinitionTests private static SequenceDataNode SerializerSequenceDataNode => new (SerializerValueDataNode); private static MappingDataNode SerializerMappingDataNode => new (){{"data", SerializerValueDataNode}}; - public sealed class DataDefinitionValueCustomTypeSerializer + internal sealed class DataDefinitionValueCustomTypeSerializer : ITypeSerializer, ITypeSerializer, ITypeSerializer, @@ -123,7 +123,7 @@ public sealed partial class DataDefinitionTests } } - public sealed class DataDefinitionSequenceCustomTypeSerializer + internal sealed class DataDefinitionSequenceCustomTypeSerializer : ITypeSerializer, ITypeSerializer, ITypeSerializer, @@ -227,7 +227,7 @@ public sealed partial class DataDefinitionTests } } - public sealed class DataDefinitionMappingCustomTypeSerializer + internal sealed class DataDefinitionMappingCustomTypeSerializer : ITypeSerializer, ITypeSerializer, ITypeSerializer, @@ -361,7 +361,7 @@ public sealed partial class DataDefinitionTests } [CopyByRef] - public sealed class DataDummyClass : ISelfSerialize, IEquatable + internal sealed class DataDummyClass : ISelfSerialize, IEquatable { public string Value = string.Empty; public void Deserialize(string value) diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.cs index f2840126d..7397ba02c 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/DataDefinitionTests.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/DataDefinitionTests.cs @@ -11,7 +11,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; -public sealed partial class DataDefinitionTests : SerializationTest +internal sealed partial class DataDefinitionTests : OurSerializationTest { //todo test that no references are wrongfully copied @@ -23,7 +23,7 @@ public sealed partial class DataDefinitionTests : SerializationTest //copy: null <> cts(cc(+nt), c)/regular(c) [DataDefinition] - public sealed partial class DataDefTestDummy + internal sealed partial class DataDefTestDummy { [DataField("a")] public int a = Int32.MaxValue; [DataField("b")] public DataDummyStruct b = new(){Value = "default"}; diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.Resources.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.Resources.cs similarity index 99% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.Resources.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.Resources.cs index a997865ad..17ba62734 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.Resources.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.Resources.cs @@ -15,7 +15,7 @@ namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; /// /// Class holding all the resources needed for . I opted to move them all in here because i wanted the TestFixture to not look so messy /// -public sealed partial class ManagerTests : ISerializationContext +internal sealed partial class ManagerTests : ISerializationContext { private static ValueDataNode SerializerRanDataNode => new ("SerializerRan"); private static ValueDataNode SerializerRanCustomDataNode => new ("SerializerRanCustom"); diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.cs index a538b6a73..93701b236 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ManagerTests.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ManagerTests.cs @@ -12,7 +12,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; [TestFixture] -public sealed partial class ManagerTests : SerializationTest +internal sealed partial class ManagerTests : OurSerializationTest { /* legend: NT => Nullable Type diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ReadValueProviderTests.cs b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ReadValueProviderTests.cs similarity index 93% rename from Robust.UnitTesting/Shared/Serialization/SerializationTests/ReadValueProviderTests.cs rename to Robust.Shared.IntegrationTests/Serialization/SerializationTests/ReadValueProviderTests.cs index 7279aed0c..eb7da26f9 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTests/ReadValueProviderTests.cs +++ b/Robust.Shared.IntegrationTests/Serialization/SerializationTests/ReadValueProviderTests.cs @@ -9,7 +9,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization.SerializationTests; -public sealed partial class ReadValueProviderTests : SerializationTest +internal sealed partial class ReadValueProviderTests : OurSerializationTest { //test for: datadefinition (value, mapping), selfserialize @@ -31,12 +31,12 @@ public sealed partial class ReadValueProviderTests : SerializationTest } [DataDefinition] - public sealed partial class DataDefinitionValueProviderTestDummy : IBaseInterface + internal sealed partial class DataDefinitionValueProviderTestDummy : IBaseInterface { [DataField("data")] public string Data = string.Empty; } - public sealed class OtherDataDefinitionValueProviderTestDummy : IBaseInterface{} + internal sealed class OtherDataDefinitionValueProviderTestDummy : IBaseInterface{} private interface IBaseInterface {} diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/AngleSerializerTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/AngleSerializerTest.cs index 8d91188f0..67795ba89 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/AngleSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/AngleSerializerTest.cs @@ -10,7 +10,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(AngleSerializer))] - public sealed class AngleSerializerTest : SerializationTest + internal sealed class AngleSerializerTest : OurSerializationTest { private static readonly TestCaseData[] _source = new[] { diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ArraySerializerTest.cs similarity index 93% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ArraySerializerTest.cs index 327c72783..8242c53dd 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ArraySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ArraySerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(ListSerializers<>))] - public sealed class ArraySerializerTest : SerializationTest + internal sealed class ArraySerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Box2SerializerTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Box2SerializerTest.cs index 9b44d8db0..a2a1bdffb 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Box2SerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Box2SerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(Box2Serializer))] - public sealed class Box2SerializerTest : SerializationTest + internal sealed class Box2SerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ColorSerializerTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ColorSerializerTest.cs index 798757bc2..3a40d25a5 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ColorSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ColorSerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(ColorSerializer))] - public sealed class ColorSerializerTest : SerializationTest + internal sealed class ColorSerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs similarity index 93% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs index 91ba30337..8c3eed5ac 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -17,7 +17,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(ComponentRegistrySerializer))] - public sealed class ComponentRegistrySerializerTest : SerializationTest + internal sealed class ComponentRegistrySerializerTest : OurSerializationTest { protected override Type[]? ExtraComponents => new[] {typeof(TestComponent)}; @@ -52,7 +52,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers } } - public sealed partial class TestComponent : Component + internal sealed partial class TestComponent : Component { } } diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs index f09235695..8dcbe29cc 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/AbstractDictionarySerializerTest.cs @@ -15,7 +15,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers.Custom; [TestFixture] [TestOf(typeof(AbstractDictionarySerializer<>))] -public sealed partial class AbstractDictionarySerializerTest : RobustUnitTest +internal sealed partial class AbstractDictionarySerializerTest : OurRobustUnitTest { private const string TestYaml = @" SealedTestTypeA: @@ -40,7 +40,7 @@ SealedTestTypeB: MappingDataNode, AbstractDictionarySerializer> (node); - Assert.That(validation.GetErrors().Count(), Is.EqualTo(0)); + Assert.That(validation.GetErrors(), Is.Empty); var data = seri.Read, MappingDataNode, diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs index 30ac3bbb3..8d6431c74 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/FlagSerializerTest.cs @@ -12,7 +12,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers.Custom { [TestFixture] [TestOf(typeof(FlagSerializer<>))] - public sealed partial class FlagSerializerTest : SerializationTest + internal sealed partial class FlagSerializerTest : OurSerializationTest { [Test] public void SingleFlagTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs index 027a874b6..b25e294a3 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs @@ -18,7 +18,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers.Custom.Prototy { [TestFixture] [TestOf(typeof(PrototypeIdListSerializer<>))] - public sealed class PrototypeIdListSerializerTest : SerializationTest + internal sealed class PrototypeIdListSerializerTest : OurSerializationTest { private static readonly string TestEntityId = $"{nameof(PrototypeIdListSerializerTest)}Dummy"; @@ -141,7 +141,7 @@ entitiesImmutableList: } [DataDefinition] - public sealed partial class PrototypeIdListSerializerTestDataDefinition + internal sealed partial class PrototypeIdListSerializerTestDataDefinition { [DataField("entitiesList", customTypeSerializer: typeof(PrototypeIdListSerializer))] public List EntitiesList = new(); diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DateTimeSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DateTimeSerializerTest.cs similarity index 92% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/DateTimeSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DateTimeSerializerTest.cs index 39853217f..c4fd951b6 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DateTimeSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DateTimeSerializerTest.cs @@ -9,7 +9,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers; [TestFixture] [TestOf(typeof(DateTimeSerializer))] -internal sealed class DateTimeSerializerTest : SerializationTest +internal sealed class DateTimeSerializerTest : OurSerializationTest { [Test] public void WriteTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DictionarySerializerTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DictionarySerializerTest.cs index d7a33d703..8ac3097b7 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/DictionarySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/DictionarySerializerTest.cs @@ -9,7 +9,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(DictionarySerializer<,>))] - public sealed class DictionarySerializerTest : SerializationTest + internal sealed class DictionarySerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs similarity index 93% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs index 9ce2f7971..8ebee5ad7 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/FormattedMessageSerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers; [TestFixture] [TestOf(typeof(FormattedMessageSerializer))] -public sealed class FormattedMessageSerializerTest : SerializationTest +internal sealed class FormattedMessageSerializerTest : OurSerializationTest { [Test] [TestCase("message")] diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/HashSetSerializerTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/HashSetSerializerTest.cs index d53563c0a..cb0ae7f14 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/HashSetSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/HashSetSerializerTest.cs @@ -9,7 +9,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(HashSetSerializer<>))] - public sealed class HashSetSerializerTest : SerializationTest + internal sealed class HashSetSerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/IntegerSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/IntegerSerializerTest.cs similarity index 90% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/IntegerSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/IntegerSerializerTest.cs index cf2b4fa05..747b3a424 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/IntegerSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/IntegerSerializerTest.cs @@ -4,7 +4,7 @@ using Robust.Shared.Serialization.Markdown.Value; namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] - public sealed class IntegerSerializerTest : SerializationTest + internal sealed class IntegerSerializerTest : OurSerializationTest { [Test] public void IntReadTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ListSerializerTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ListSerializerTest.cs index f792dc3a2..bd3a00194 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ListSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ListSerializerTest.cs @@ -9,7 +9,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(ListSerializers<>))] - public sealed class ListSerializerTest : SerializationTest + internal sealed class ListSerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/RegexSerializerTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/RegexSerializerTest.cs index 5132e52df..180b12b13 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/RegexSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/RegexSerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers { [TestFixture] [TestOf(typeof(RegexSerializer))] - public sealed class RegexSerializerTest : SerializationTest + internal sealed class RegexSerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/SortedSetSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/SortedSetSerializerTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/SortedSetSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/SortedSetSerializerTest.cs index 968eae3bf..246f9f143 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/SortedSetSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/SortedSetSerializerTest.cs @@ -9,7 +9,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers; [TestFixture] [TestOf(typeof(SortedSetSerializer<>))] -public sealed class SortedSetSerializerTest : SerializationTest +internal sealed class SortedSetSerializerTest : OurSerializationTest { [Test] public void SerializationTest() diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/TimespanSerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/TimespanSerializerTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/TypeSerializers/TimespanSerializerTest.cs rename to Robust.Shared.IntegrationTests/Serialization/TypeSerializers/TimespanSerializerTest.cs index c3f145e95..a32ab668b 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/TimespanSerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/TimespanSerializerTest.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers; [TestFixture] [TestOf(typeof(TimespanSerializer))] -internal sealed class TimespanSerializerTest : SerializationTest +internal sealed class TimespanSerializerTest : OurSerializationTest { [Test] public void ReadTest() diff --git a/Robust.UnitTesting/Shared/Serialization/VirtualObjectArrayTest.cs b/Robust.Shared.IntegrationTests/Serialization/VirtualObjectArrayTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Serialization/VirtualObjectArrayTest.cs rename to Robust.Shared.IntegrationTests/Serialization/VirtualObjectArrayTest.cs index c2dc599e9..4eb75d9a1 100644 --- a/Robust.UnitTesting/Shared/Serialization/VirtualObjectArrayTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/VirtualObjectArrayTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NUnit.Framework; +using Robust.Shared.Analyzers; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; @@ -9,7 +10,7 @@ namespace Robust.UnitTesting.Shared.Serialization; /// /// Tests that arrays and lists of virtual/abstract objects can be properly serialized and deserialized. /// -public sealed partial class VirtualObjectArrayTest : SerializationTest +internal sealed partial class VirtualObjectArrayTest : OurSerializationTest { [ImplicitDataDefinitionForInheritors] private abstract partial class BaseTestDataDef { } diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs rename to Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs index b908a8aef..cf34621d0 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/ImmutableListSerializationTest.cs @@ -13,7 +13,7 @@ using YamlDotNet.RepresentationModel; namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { [TestFixture] - public sealed class ImmutableListSerializationTest : RobustUnitTest + internal sealed class ImmutableListSerializationTest : OurRobustUnitTest { [OneTimeSetUp] public void Setup() diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs similarity index 95% rename from Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs rename to Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs index 5b7b95732..ea1565aaa 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs +++ b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypePropertySerialization_Test.cs @@ -13,7 +13,7 @@ using YamlDotNet.RepresentationModel; namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { [TestFixture] - public sealed class TypePropertySerialization_Test : RobustUnitTest + internal sealed class TypePropertySerialization_Test : OurRobustUnitTest { [OneTimeSetUp] public void Setup() @@ -91,7 +91,7 @@ namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests } [RegisterComponent] - public sealed partial class TestComponent : Component + internal sealed partial class TestComponent : Component { [DataField("testType")] public ITestType? TestType { get; set; } } diff --git a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs similarity index 96% rename from Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs rename to Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs index 5c87bbbcd..667b89504 100644 --- a/Robust.UnitTesting/Shared/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs +++ b/Robust.Shared.IntegrationTests/Serialization/YamlObjectSerializerTests/TypeSerialization_Test.cs @@ -10,7 +10,7 @@ using YamlDotNet.RepresentationModel; namespace Robust.UnitTesting.Shared.Serialization.YamlObjectSerializerTests { [TestFixture] - public sealed class TypeSerialization_Test : RobustUnitTest + internal sealed class TypeSerialization_Test : OurRobustUnitTest { [OneTimeSetUp] public void Setup() diff --git a/Robust.UnitTesting/Shared/Spawning/EntitySpawnHelpersTest.cs b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Spawning/EntitySpawnHelpersTest.cs rename to Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs index 0e990bfab..a27470947 100644 --- a/Robust.UnitTesting/Shared/Spawning/EntitySpawnHelpersTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; +using Robust.Shared.Analyzers; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -25,7 +26,7 @@ public abstract partial class EntitySpawnHelpersTest : RobustIntegrationTest protected SharedContainerSystem Container = default!; // Even if unused, content / downstream tests might use this class, so removal would be a breaking change? - protected IMapManager MapMan = default!; + protected IMapManager MapMan = default!; protected EntityUid Map; protected MapId MapId; @@ -102,6 +103,12 @@ public abstract partial class EntitySpawnHelpersTest : RobustIntegrationTest Assert.That(GrandChildBPos.Position, Is.EqualTo(new Vector2(2, 1))); } + [TearDown] + public void TearDown() + { + Server?.Dispose(); + } + /// /// Simple container that can store up to 2 entities. /// diff --git a/Robust.UnitTesting/Shared/Spawning/SpawnInContainerOrDropTest.cs b/Robust.Shared.IntegrationTests/Spawning/SpawnInContainerOrDropTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Spawning/SpawnInContainerOrDropTest.cs rename to Robust.Shared.IntegrationTests/Spawning/SpawnInContainerOrDropTest.cs index 77fb3b7cd..873326292 100644 --- a/Robust.UnitTesting/Shared/Spawning/SpawnInContainerOrDropTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/SpawnInContainerOrDropTest.cs @@ -6,7 +6,7 @@ using Robust.Shared.Map; namespace Robust.UnitTesting.Shared.Spawning; [TestFixture] -public sealed class SpawnInContainerOrDropTest : EntitySpawnHelpersTest +internal sealed class SpawnInContainerOrDropTest : EntitySpawnHelpersTest { [Test] public async Task Test() diff --git a/Robust.UnitTesting/Shared/Spawning/SpawnNextToOrDropTest.cs b/Robust.Shared.IntegrationTests/Spawning/SpawnNextToOrDropTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Spawning/SpawnNextToOrDropTest.cs rename to Robust.Shared.IntegrationTests/Spawning/SpawnNextToOrDropTest.cs index 26458641a..b3bb82fd0 100644 --- a/Robust.UnitTesting/Shared/Spawning/SpawnNextToOrDropTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/SpawnNextToOrDropTest.cs @@ -6,7 +6,7 @@ using Robust.Shared.Map; namespace Robust.UnitTesting.Shared.Spawning; [TestFixture] -public sealed class SpawnNextToOrDropTest : EntitySpawnHelpersTest +internal sealed class SpawnNextToOrDropTest : EntitySpawnHelpersTest { [Test] public async Task Test() diff --git a/Robust.UnitTesting/Shared/Spawning/TrySpawnInContainerTest.cs b/Robust.Shared.IntegrationTests/Spawning/TrySpawnInContainerTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Spawning/TrySpawnInContainerTest.cs rename to Robust.Shared.IntegrationTests/Spawning/TrySpawnInContainerTest.cs index a526c0d1d..19d7ff8d9 100644 --- a/Robust.UnitTesting/Shared/Spawning/TrySpawnInContainerTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/TrySpawnInContainerTest.cs @@ -4,7 +4,7 @@ using NUnit.Framework; namespace Robust.UnitTesting.Shared.Spawning; [TestFixture] -public sealed class TrySpawnInContainerTest : EntitySpawnHelpersTest +internal sealed class TrySpawnInContainerTest : EntitySpawnHelpersTest { [Test] public async Task Test() diff --git a/Robust.UnitTesting/Shared/Spawning/TrySpawnNextToTest.cs b/Robust.Shared.IntegrationTests/Spawning/TrySpawnNextToTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Spawning/TrySpawnNextToTest.cs rename to Robust.Shared.IntegrationTests/Spawning/TrySpawnNextToTest.cs index 130a95f2f..3e837201c 100644 --- a/Robust.UnitTesting/Shared/Spawning/TrySpawnNextToTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/TrySpawnNextToTest.cs @@ -5,7 +5,7 @@ using Robust.Shared.GameObjects; namespace Robust.UnitTesting.Shared.Spawning; [TestFixture] -public sealed class TrySpawnNextToTest : EntitySpawnHelpersTest +internal sealed class TrySpawnNextToTest : EntitySpawnHelpersTest { [Test] public async Task Test() diff --git a/Robust.UnitTesting/Shared/TestRobustSerializerHash.cs b/Robust.Shared.IntegrationTests/TestRobustSerializerHash.cs similarity index 100% rename from Robust.UnitTesting/Shared/TestRobustSerializerHash.cs rename to Robust.Shared.IntegrationTests/TestRobustSerializerHash.cs diff --git a/Robust.UnitTesting/Shared/Timing/GameLoop_Test.cs b/Robust.Shared.IntegrationTests/Timing/GameLoop_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Timing/GameLoop_Test.cs rename to Robust.Shared.IntegrationTests/Timing/GameLoop_Test.cs index 2bdc7ed2f..c3cd7f7b4 100644 --- a/Robust.UnitTesting/Shared/Timing/GameLoop_Test.cs +++ b/Robust.Shared.IntegrationTests/Timing/GameLoop_Test.cs @@ -11,7 +11,7 @@ namespace Robust.UnitTesting.Shared.Timing { [TestFixture] [TestOf(typeof(GameLoop))] - sealed class GameLoop_Test : RobustUnitTest + sealed class GameLoop_Test : OurRobustUnitTest { /// /// With single step enabled, the game loop should run 1 tick and then pause again. diff --git a/Robust.UnitTesting/Shared/Timing/TimerTest.cs b/Robust.Shared.IntegrationTests/Timing/TimerTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Timing/TimerTest.cs rename to Robust.Shared.IntegrationTests/Timing/TimerTest.cs index 17e218380..8d2535351 100644 --- a/Robust.UnitTesting/Shared/Timing/TimerTest.cs +++ b/Robust.Shared.IntegrationTests/Timing/TimerTest.cs @@ -11,7 +11,7 @@ namespace Robust.UnitTesting.Shared.Timing { [TestFixture] [TestOf(typeof(Timer))] - public sealed class TimerTest : RobustUnitTest + internal sealed class TimerTest : OurRobustUnitTest { private LogCatcher _catcher = default!; diff --git a/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ArithmeticTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ArithmeticTest.cs index 660db2a48..7df189656 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ArithmeticTest.cs @@ -7,7 +7,7 @@ using NUnit.Framework; namespace Robust.UnitTesting.Shared.Toolshed; [TestFixture] -public sealed class ArithmeticTest : ToolshedTest +internal sealed class ArithmeticTest : ToolshedTest { [Test] public async Task OrderOfOperations() diff --git a/Robust.UnitTesting/Shared/Toolshed/ErrorHandlingTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ErrorHandlingTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Toolshed/ErrorHandlingTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ErrorHandlingTest.cs index 1b6030897..467b15743 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ErrorHandlingTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ErrorHandlingTest.cs @@ -6,7 +6,7 @@ using Robust.Shared.Toolshed.Errors; namespace Robust.UnitTesting.Shared.Toolshed; [TestFixture] -public sealed class ErrorHandlingTest : ToolshedTest +internal sealed class ErrorHandlingTest : ToolshedTest { [Test] public async Task ExceptionsAreErrors() diff --git a/Robust.UnitTesting/Shared/Toolshed/LocTest.cs b/Robust.Shared.IntegrationTests/Toolshed/LocTest.cs similarity index 97% rename from Robust.UnitTesting/Shared/Toolshed/LocTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/LocTest.cs index 4653556a2..d3b554824 100644 --- a/Robust.UnitTesting/Shared/Toolshed/LocTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/LocTest.cs @@ -8,7 +8,7 @@ using Robust.Shared.Toolshed; namespace Robust.UnitTesting.Shared.Toolshed; [TestFixture] -public sealed class LocTest : ToolshedTest +internal sealed class LocTest : ToolshedTest { [Test] public async Task AllCommandsHaveDescriptions() diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.Shared.IntegrationTests/Toolshed/TestCommands.cs similarity index 72% rename from Robust.UnitTesting/Shared/Toolshed/TestCommands.cs rename to Robust.Shared.IntegrationTests/Toolshed/TestCommands.cs index 6207a7109..6b1c33ac7 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/TestCommands.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Robust.Shared.Analyzers; using Robust.Shared.Console; using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; @@ -12,27 +13,27 @@ namespace Robust.UnitTesting.Shared.Toolshed; // This file just contains a collection of various test commands for use in other tests. [ToolshedCommand] -public sealed class TestVoidCommand : ToolshedCommand +internal sealed class TestVoidCommand : ToolshedCommand { [CommandImplementation] public void Impl() {} } [ToolshedCommand] -public sealed class TestIntCommand : ToolshedCommand +internal sealed class TestIntCommand : ToolshedCommand { [CommandImplementation] public int Impl() => 1; } [ToolshedCommand] -public sealed class TestTypeArgCommand : ToolshedCommand +internal sealed class TestTypeArgCommand : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation] public string Impl() => typeof(T).Name; } [ToolshedCommand] -public sealed class TestMultiTypeArgCommand : ToolshedCommand +internal sealed class TestMultiTypeArgCommand : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; [CommandImplementation] public string Impl(int i) @@ -40,19 +41,19 @@ public sealed class TestMultiTypeArgCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestIntStrArgCommand : ToolshedCommand +internal sealed class TestIntStrArgCommand : ToolshedCommand { [CommandImplementation] public int Impl(int i, string str) => i; } [ToolshedCommand] -public sealed class TestPipedIntCommand : ToolshedCommand +internal sealed class TestPipedIntCommand : ToolshedCommand { [CommandImplementation] public int Impl([PipedArgument] int i) => i; } [ToolshedCommand] -public sealed class TestCustomVarRefParserCommand : ToolshedCommand +internal sealed class TestCustomVarRefParserCommand : ToolshedCommand { [CommandImplementation] public int Impl([CommandArgument(typeof(Parser))] int i) => i; @@ -79,7 +80,7 @@ public sealed class TestCustomVarRefParserCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestOptionalArgsCommand : ToolshedCommand +internal sealed class TestOptionalArgsCommand : ToolshedCommand { [CommandImplementation] public int[] Impl(int x, int y = 0, int z = 1) @@ -87,7 +88,7 @@ public sealed class TestOptionalArgsCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestParamsCollectionCommand : ToolshedCommand +internal sealed class TestParamsCollectionCommand : ToolshedCommand { [CommandImplementation] public int[] Impl(int x, int y = 0, params int[] others) @@ -95,7 +96,7 @@ public sealed class TestParamsCollectionCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestParamsOnlyCommand : ToolshedCommand +internal sealed class TestParamsOnlyCommand : ToolshedCommand { [CommandImplementation] public int[] Impl(params int[] others) @@ -103,12 +104,12 @@ public sealed class TestParamsOnlyCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestCustomParserCommand : ToolshedCommand +internal sealed class TestCustomParserCommand : ToolshedCommand { [CommandImplementation] public int Impl([CommandArgument(typeof(Parser))] int i) => i; - public sealed class Parser : TestCustomVarRefParserCommand.Parser + internal sealed class Parser : TestCustomVarRefParserCommand.Parser { // Disable ValueRef support. // I.e., this parser will not not try to parse variables or blocks @@ -117,28 +118,28 @@ public sealed class TestCustomParserCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestEnumerableInferCommand : ToolshedCommand +internal sealed class TestEnumerableInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] IEnumerable x, T y) => typeof(T); } [ToolshedCommand] -public sealed class TestListInferCommand : ToolshedCommand +internal sealed class TestListInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] List x, T y) => typeof(T); } [ToolshedCommand] -public sealed class TestArrayInferCommand : ToolshedCommand +internal sealed class TestArrayInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] T[] x, T y) => typeof(T); } [ToolshedCommand] -public sealed class TestNestedEnumerableInferCommand : ToolshedCommand +internal sealed class TestNestedEnumerableInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] IEnumerable> x) @@ -149,7 +150,7 @@ public sealed class TestNestedEnumerableInferCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestNestedListInferCommand : ToolshedCommand +internal sealed class TestNestedListInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] List> x) @@ -160,7 +161,7 @@ public sealed class TestNestedListInferCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestNestedArrayInferCommand : ToolshedCommand +internal sealed class TestNestedArrayInferCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public Type Impl([PipedArgument] ProtoId[] x) @@ -171,21 +172,21 @@ public sealed class TestNestedArrayInferCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestArrayCommand : ToolshedCommand +internal sealed class TestArrayCommand : ToolshedCommand { [CommandImplementation] public int[] Impl() => Array.Empty(); } [ToolshedCommand] -public sealed class TestListCommand : ToolshedCommand +internal sealed class TestListCommand : ToolshedCommand { [CommandImplementation] public List Impl() => new(); } [ToolshedCommand] -public sealed class TestEnumerableCommand : ToolshedCommand +internal sealed class TestEnumerableCommand : ToolshedCommand { private static int[] _arr = {1, 3, 3}; @@ -194,21 +195,21 @@ public sealed class TestEnumerableCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestNestedArrayCommand : ToolshedCommand +internal sealed class TestNestedArrayCommand : ToolshedCommand { [CommandImplementation] public ProtoId[] Impl() => []; } [ToolshedCommand] -public sealed class TestNestedListCommand : ToolshedCommand +internal sealed class TestNestedListCommand : ToolshedCommand { [CommandImplementation] public List> Impl() => new(); } [ToolshedCommand] -public sealed class TestNestedEnumerableCommand : ToolshedCommand +internal sealed class TestNestedEnumerableCommand : ToolshedCommand { private static ProtoId[] _arr = []; @@ -217,13 +218,13 @@ public sealed class TestNestedEnumerableCommand : ToolshedCommand } [ToolshedCommand] -public sealed class TestImplicitImplCommand : ToolshedCommand +internal sealed class TestImplicitImplCommand : ToolshedCommand { public int Impl() => 1; } [ToolshedCommand] -public sealed class TestExplicitImplCommand : ToolshedCommand +internal sealed class TestExplicitImplCommand : ToolshedCommand { public int Impl() => 1; diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Bugcheck.cs similarity index 95% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Bugcheck.cs index b444289f7..444074d5b 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Bugcheck.cs @@ -8,7 +8,7 @@ namespace Robust.UnitTesting.Shared.Toolshed; // Find a silly little bug or a goof? // Add a test here to make sure it doesn't come back. -public sealed partial class ToolshedParserTest +internal sealed partial class ToolshedParserTest { // Memorializing the fact I never fixed this shit in BQL reee [Test, TestOf(typeof(Quantity))] diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Core.cs similarity index 98% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Core.cs index 0364c0d40..8251f42b1 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.Core.cs @@ -9,7 +9,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Toolshed; -public sealed partial class ToolshedParserTest +internal sealed partial class ToolshedParserTest { [Test] public async Task AllCoreTypesParseable() diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.cs index e966fe023..94522fb30 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedParserTest.cs @@ -12,7 +12,7 @@ using Robust.Shared.Toolshed.TypeParsers.Math; namespace Robust.UnitTesting.Shared.Toolshed; [TestFixture] -public sealed partial class ToolshedParserTest : ToolshedTest +internal sealed partial class ToolshedParserTest : ToolshedTest { [Test] public async Task SimpleCommandRun() diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTest.cs similarity index 100% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedTest.cs diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTests.cs similarity index 99% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedTests.cs index 38f99237c..24297ea81 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTests.cs @@ -17,7 +17,7 @@ namespace Robust.UnitTesting.Shared.Toolshed; /// Collection of miscellaneous toolshed command tests. /// Several of these were just added ad hoc as bugs arose. /// -public sealed class ToolshedTests : ToolshedTest +internal sealed class ToolshedTests : ToolshedTest { // TODO Robust.UnitTesting // split these into separate [TestCase]s when we have pooling. diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.BugCheck.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.BugCheck.cs similarity index 89% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.BugCheck.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.BugCheck.cs index 740d3f099..0f5d60792 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.BugCheck.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.BugCheck.cs @@ -4,7 +4,7 @@ using NUnit.Framework; namespace Robust.UnitTesting.Shared.Toolshed; -public sealed partial class ToolshedTypesTest +internal sealed partial class ToolshedTypesTest { // Assert that T -> Nullable holds and it's inverse does not. [Test] diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.cs index 0da055b21..5bc9edd4a 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTypesTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedTypesTest.cs @@ -7,7 +7,7 @@ using NUnit.Framework; namespace Robust.UnitTesting.Shared.Toolshed; [TestFixture] -public sealed partial class ToolshedTypesTest : ToolshedTest +internal sealed partial class ToolshedTypesTest : ToolshedTest { // Assert that T -> IEnumerable holds. [Test] diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs b/Robust.Shared.IntegrationTests/Toolshed/ToolshedValidationTest.cs similarity index 83% rename from Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs rename to Robust.Shared.IntegrationTests/Toolshed/ToolshedValidationTest.cs index 4b485ed88..aae556acf 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs +++ b/Robust.Shared.IntegrationTests/Toolshed/ToolshedValidationTest.cs @@ -9,7 +9,7 @@ using Robust.Shared.Utility; namespace Robust.UnitTesting.Shared.Toolshed; -public sealed class ToolshedValidationTest : ToolshedTest +internal sealed class ToolshedValidationTest : ToolshedTest { #if DEBUG [Test] @@ -56,7 +56,7 @@ public sealed class ToolshedValidationTest : ToolshedTest #region InvalidCommands // Not enough type argument parsers [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid1Command : ToolshedCommand +internal sealed class TestInvalid1Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation] public void Impl() {} @@ -64,7 +64,7 @@ public sealed class TestInvalid1Command : ToolshedCommand // too many type argument parsers [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid2Command : ToolshedCommand +internal sealed class TestInvalid2Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; [CommandImplementation] @@ -73,7 +73,7 @@ public sealed class TestInvalid2Command : ToolshedCommand // The generic has to be the LAST entry, not the first [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid3Command : ToolshedCommand +internal sealed class TestInvalid3Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation, TakesPipedTypeAsGeneric] @@ -81,7 +81,7 @@ public sealed class TestInvalid3Command : ToolshedCommand } [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid4Command : ToolshedCommand +internal sealed class TestInvalid4Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation, TakesPipedTypeAsGeneric] @@ -90,7 +90,7 @@ public sealed class TestInvalid4Command : ToolshedCommand // [TakesPipedTypeAsGeneric] without a [PipedArgument] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid5Command : ToolshedCommand +internal sealed class TestInvalid5Command : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public void Impl() {} @@ -98,7 +98,7 @@ public sealed class TestInvalid5Command : ToolshedCommand // Duplicate [PipedArgument] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid6Command : ToolshedCommand +internal sealed class TestInvalid6Command : ToolshedCommand { [CommandImplementation] public void Impl([PipedArgument] int arg1, [PipedArgument] int arg2) {} @@ -106,7 +106,7 @@ public sealed class TestInvalid6Command : ToolshedCommand // Conflicting arguments [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid7Command : ToolshedCommand +internal sealed class TestInvalid7Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandArgument, PipedArgument] int arg1) {} @@ -114,7 +114,7 @@ public sealed class TestInvalid7Command : ToolshedCommand // Conflicting arguments [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid8Command : ToolshedCommand +internal sealed class TestInvalid8Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInvocationContext, PipedArgument] int arg1) {} @@ -122,7 +122,7 @@ public sealed class TestInvalid8Command : ToolshedCommand // wrong [CommandInvocationContext] type [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid9Command : ToolshedCommand +internal sealed class TestInvalid9Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInvocationContext] int arg1) {} @@ -130,7 +130,7 @@ public sealed class TestInvalid9Command : ToolshedCommand // wrong [CommandInverted] type [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid10Command : ToolshedCommand +internal sealed class TestInvalid10Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInverted] int arg1) {} @@ -138,7 +138,7 @@ public sealed class TestInvalid10Command : ToolshedCommand // duplicate [CommandInverted] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid11Command : ToolshedCommand +internal sealed class TestInvalid11Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInverted] bool arg1, [CommandInverted] bool arg2) {} @@ -146,7 +146,7 @@ public sealed class TestInvalid11Command : ToolshedCommand // duplicate [CommandInvocationContext] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid12Command : ToolshedCommand +internal sealed class TestInvalid12Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInvocationContext] IInvocationContext arg1, [CommandInvocationContext] IInvocationContext arg2) {} @@ -154,7 +154,7 @@ public sealed class TestInvalid12Command : ToolshedCommand // Too few type parsers, along with a TakesPipedTypeAsGeneric [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid13Command : ToolshedCommand +internal sealed class TestInvalid13Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation, TakesPipedTypeAsGeneric] @@ -163,7 +163,7 @@ public sealed class TestInvalid13Command : ToolshedCommand // Too many type parsers, along with a TakesPipedTypeAsGeneric [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid14Command : ToolshedCommand +internal sealed class TestInvalid14Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; [CommandImplementation, TakesPipedTypeAsGeneric] @@ -172,7 +172,7 @@ public sealed class TestInvalid14Command : ToolshedCommand // [TakesPipedTypeAsGeneric] on a non-generic metod [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid15Command : ToolshedCommand +internal sealed class TestInvalid15Command : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public void Impl([PipedArgument] int i) {} @@ -180,7 +180,7 @@ public sealed class TestInvalid15Command : ToolshedCommand // type arguments on a non-generic metod [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid16Command : ToolshedCommand +internal sealed class TestInvalid16Command : ToolshedCommand { public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; [CommandImplementation] public void Impl() {} @@ -188,7 +188,7 @@ public sealed class TestInvalid16Command : ToolshedCommand // Duplicate mixed explicit/implicit [CommandInvocationContext] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid17Command : ToolshedCommand +internal sealed class TestInvalid17Command : ToolshedCommand { [CommandImplementation] public void Impl([CommandInvocationContext] IInvocationContext arg1, IInvocationContext arg2) {} @@ -197,7 +197,7 @@ public sealed class TestInvalid17Command : ToolshedCommand // Duplicate implicit [CommandInvocationContext] [ToolshedCommand, Reflect(false)] -public sealed class TestInvalid18Command : ToolshedCommand +internal sealed class TestInvalid18Command : ToolshedCommand { [CommandImplementation] public void Impl(IInvocationContext arg1, IInvocationContext arg2) {} diff --git a/Robust.UnitTesting/Shared/TransformTests/GridTraversalTest.cs b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/TransformTests/GridTraversalTest.cs rename to Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs index 451c75b3e..92401b30d 100644 --- a/Robust.UnitTesting/Shared/TransformTests/GridTraversalTest.cs +++ b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs @@ -8,7 +8,7 @@ using Robust.Shared.Maths; namespace Robust.UnitTesting.Shared.TransformTests; -public sealed class GridTraversalTest : RobustIntegrationTest +internal sealed class GridTraversalTest : RobustIntegrationTest { [Test] public async Task TestSpawnTraversal() diff --git a/Robust.UnitTesting/ApproxEqualityConstraint.cs b/Robust.Shared.Maths.Testing/ApproxEqualityConstraint.cs similarity index 74% rename from Robust.UnitTesting/ApproxEqualityConstraint.cs rename to Robust.Shared.Maths.Testing/ApproxEqualityConstraint.cs index 6d2a32ff0..ea596f627 100644 --- a/Robust.UnitTesting/ApproxEqualityConstraint.cs +++ b/Robust.Shared.Maths.Testing/ApproxEqualityConstraint.cs @@ -1,7 +1,10 @@ using System.Numerics; +using System.Runtime.Intrinsics; +using NUnit.Framework; using NUnit.Framework.Constraints; using Robust.Shared.Maths; +// ReSharper disable once CheckNamespace namespace Robust.UnitTesting { public sealed class ApproxEqualityConstraint : Constraint @@ -59,6 +62,17 @@ namespace Robust.UnitTesting return new ConstraintResult(this, actual, m3x2Expected.EqualsApprox(m3x2Actual)); } + if (Expected is Vector128 vecExpected && actual is Vector128 vecActual) + { + if (Tolerance != null) + { + return new ConstraintResult(this, + actual, + MathHelper.CloseToPercent(vecActual, vecExpected, (float)Tolerance.Value)); + } + + return new ConstraintResult(this, actual, MathHelper.CloseToPercent(vecActual, vecExpected)); + } return new ConstraintResult(this, actual, false); } @@ -74,4 +88,15 @@ namespace Robust.UnitTesting public override string Description => $"approximately {Expected}"; } + + public static class ApproxEqualityConstraintExtensions + { + extension(Is) + { + public static ApproxEqualityConstraint Approximately(object expected, double? tolerance = null) + { + return new ApproxEqualityConstraint(expected, tolerance); + } + } + } } diff --git a/Robust.Shared.Maths.Testing/Robust.Shared.Maths.Testing.csproj b/Robust.Shared.Maths.Testing/Robust.Shared.Maths.Testing.csproj new file mode 100644 index 000000000..5b1334f32 --- /dev/null +++ b/Robust.Shared.Maths.Testing/Robust.Shared.Maths.Testing.csproj @@ -0,0 +1,20 @@ + + + + + enable + false + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Shared/Maths/Angle_Test.cs b/Robust.Shared.Maths.Tests/Angle_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Maths/Angle_Test.cs rename to Robust.Shared.Maths.Tests/Angle_Test.cs index 9e7494f7f..bd3ab4cad 100644 --- a/Robust.UnitTesting/Shared/Maths/Angle_Test.cs +++ b/Robust.Shared.Maths.Tests/Angle_Test.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; using System.Numerics; -using Robust.Shared.Maths; using NUnit.Framework; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(Angle))] - public sealed class Angle_Test + internal sealed class Angle_Test { private const double Epsilon = 1.0e-8; diff --git a/Robust.Shared.Maths.Tests/AssemblyInfo.cs b/Robust.Shared.Maths.Tests/AssemblyInfo.cs new file mode 100644 index 000000000..93a9461c1 --- /dev/null +++ b/Robust.Shared.Maths.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Robust.Shared.Tests")] diff --git a/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs b/Robust.Shared.Maths.Tests/Box2Rotated_Test.cs similarity index 81% rename from Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs rename to Robust.Shared.Maths.Tests/Box2Rotated_Test.cs index dc6207f04..220861725 100644 --- a/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2Rotated_Test.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; using System.Numerics; -using System.Runtime.Intrinsics.X86; using NUnit.Framework; -using Robust.Shared.Maths; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture] [TestOf(typeof(Box2Rotated))] - public sealed class Box2Rotated_Test + internal sealed class Box2Rotated_Test { private static IEnumerable BoxRotations = new[] { @@ -45,7 +42,7 @@ namespace Robust.UnitTesting.Shared.Maths private static readonly float Cos45Deg = MathF.Cos(MathF.PI / 4); private static readonly float Sqrt2 = MathF.Sqrt(2); - private static IEnumerable<(Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected)> CalcBoundingBoxData => + public static IEnumerable<(Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected)> CalcBoundingBoxData => new (Box2, Vector2, Angle, Box2)[] { (new Box2(0, 0, 1, 1), new Vector2(0, 0), 0, new Box2(0, 0, 1, 1)), @@ -54,10 +51,11 @@ namespace Robust.UnitTesting.Shared.Maths (new Box2(0, 0, 1, 1), new Vector2(1, 1), Math.PI, new Box2(1, 1, 2, 2)), (new Box2(1, 1, 2, 2), new Vector2(1, 1), Math.PI/4, new Box2(1 - Cos45Deg, 1, 1 + Cos45Deg, 1 + Sqrt2)), (new Box2(-1, 1, 1, 2), new Vector2(0, 0), -Math.PI/2, new Box2(1, -1, 2, 1)), + (Box2.UnitCentered, new Vector2(1, Sqrt2), Angle.FromDegrees(30), new Box2(0.158069f, -0.993544f, 1.52409f,0.372481f)), }; - private static TestCaseData[] MatrixBox2Cases = new[] - { + private static TestCaseData[] MatrixBox2Cases = + [ new TestCaseData(Matrix3x2.Identity, Box2Rotated.UnitCentered, Box2Rotated.UnitCentered.CalcBoundingBox()), @@ -67,7 +65,15 @@ namespace Robust.UnitTesting.Shared.Maths new TestCaseData(Matrix3x2.CreateTranslation(Vector2.One), Box2Rotated.UnitCentered, new Box2Rotated(new Vector2(0.5f, 0.5f), new Vector2(1.5f, 1.5f)).CalcBoundingBox()), - }; + new TestCaseData(Matrix3x2.CreateTranslation(new Vector2(-1, -Sqrt2)) + * Matrix3Helpers.CreateTransform(new Vector2(1, Sqrt2), Angle.FromDegrees(30)), + new Box2Rotated(Box2.UnitCentered), + new Box2(0.158069f, -0.993544f, 1.52409f, 0.372481f)), + new TestCaseData(Matrix3x2.CreateTranslation(new Vector2(-1, -Sqrt2)) + * Matrix3Helpers.CreateTransform(new Vector2(1, Sqrt2), Angle.FromDegrees(30)), + new Box2Rotated(Box2.UnitCentered, -Angle.FromDegrees(30), new Vector2(1, Sqrt2)), + Box2.UnitCentered) + ]; /// /// Tests that transforming a Box2Rotated into a Box2 works. @@ -75,7 +81,7 @@ namespace Robust.UnitTesting.Shared.Maths [Test, TestCaseSource(nameof(MatrixBox2Cases))] public void TestBox2Matrices(Matrix3x2 matrix, Box2Rotated bounds, Box2 result) { - Assert.That(matrix.TransformBox(bounds), Is.EqualTo(result)); + Assert.That(matrix.TransformBox(bounds), Is.Approximately(result)); } [Test] diff --git a/Robust.UnitTesting/Shared/Maths/Box2_Test.cs b/Robust.Shared.Maths.Tests/Box2_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Maths/Box2_Test.cs rename to Robust.Shared.Maths.Tests/Box2_Test.cs index 505fbc358..80c458fe7 100644 --- a/Robust.UnitTesting/Shared/Maths/Box2_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2_Test.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Numerics; +using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(Box2))] - public sealed class Box2_Test + internal sealed class Box2_Test { private static IEnumerable<(float left, float bottom, float right, float top)> Sources => new (float, float, float, float)[] diff --git a/Robust.UnitTesting/Shared/Maths/Box2i_Test.cs b/Robust.Shared.Maths.Tests/Box2i_Test.cs similarity index 91% rename from Robust.UnitTesting/Shared/Maths/Box2i_Test.cs rename to Robust.Shared.Maths.Tests/Box2i_Test.cs index 5f1a85ceb..c924fc50f 100644 --- a/Robust.UnitTesting/Shared/Maths/Box2i_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2i_Test.cs @@ -1,10 +1,9 @@ using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture, Parallelizable, TestOf(typeof(Box2i))] - sealed class Box2i_Test + internal sealed class Box2i_Test { [Test] public void Box2iUnion() diff --git a/Robust.UnitTesting/Shared/Maths/Circle_Test.cs b/Robust.Shared.Maths.Tests/Circle_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Maths/Circle_Test.cs rename to Robust.Shared.Maths.Tests/Circle_Test.cs index e251d5f82..861912a32 100644 --- a/Robust.UnitTesting/Shared/Maths/Circle_Test.cs +++ b/Robust.Shared.Maths.Tests/Circle_Test.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Numerics; +using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(Circle))] - public sealed class Circle_Test + internal sealed class Circle_Test { private static IEnumerable Coordinates => new float[] { -1, 0, 1 }; diff --git a/Robust.UnitTesting/Shared/ColorUtils_Test.cs b/Robust.Shared.Maths.Tests/ColorUtils_Test.cs similarity index 94% rename from Robust.UnitTesting/Shared/ColorUtils_Test.cs rename to Robust.Shared.Maths.Tests/ColorUtils_Test.cs index f43a35159..3f4be588e 100644 --- a/Robust.UnitTesting/Shared/ColorUtils_Test.cs +++ b/Robust.Shared.Maths.Tests/ColorUtils_Test.cs @@ -1,11 +1,10 @@ -using Robust.Shared.Maths; -using NUnit.Framework; +using NUnit.Framework; -namespace Robust.UnitTesting.Shared +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] - public sealed class ColorUtils_Test + internal sealed class ColorUtils_Test { [Test] public void TestInterpolateBetween() diff --git a/Robust.UnitTesting/Shared/Maths/Color_Test.cs b/Robust.Shared.Maths.Tests/Color_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Maths/Color_Test.cs rename to Robust.Shared.Maths.Tests/Color_Test.cs index 593a94981..1554d0cc6 100644 --- a/Robust.UnitTesting/Shared/Maths/Color_Test.cs +++ b/Robust.Shared.Maths.Tests/Color_Test.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Robust.Shared.Maths; +using NUnit.Framework; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(Color))] - public sealed class Color_Test + internal sealed class Color_Test { static IEnumerable BytesSource = new byte[] { diff --git a/Robust.UnitTesting/Shared/Maths/Direction_Test.cs b/Robust.Shared.Maths.Tests/Direction_Test.cs similarity index 95% rename from Robust.UnitTesting/Shared/Maths/Direction_Test.cs rename to Robust.Shared.Maths.Tests/Direction_Test.cs index b55a3b62c..ad019de77 100644 --- a/Robust.UnitTesting/Shared/Maths/Direction_Test.cs +++ b/Robust.Shared.Maths.Tests/Direction_Test.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using System.Numerics; -using Robust.Shared.Maths; +using System.Numerics; using NUnit.Framework; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] diff --git a/Robust.UnitTesting/Shared/Maths/Matrix3_Test.cs b/Robust.Shared.Maths.Tests/Matrix3_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Maths/Matrix3_Test.cs rename to Robust.Shared.Maths.Tests/Matrix3_Test.cs index a6016f3a6..c5b2fa9dd 100644 --- a/Robust.UnitTesting/Shared/Maths/Matrix3_Test.cs +++ b/Robust.Shared.Maths.Tests/Matrix3_Test.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture] [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestOf(typeof(Matrix3x2))] - public sealed class Matrix3_Test + internal sealed class Matrix3_Test { private static readonly TestCaseData[] Rotations = new TestCaseData[] { diff --git a/Robust.UnitTesting/Shared/Maths/NumericsHelpers_Test.cs b/Robust.Shared.Maths.Tests/NumericsHelpers_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/Maths/NumericsHelpers_Test.cs rename to Robust.Shared.Maths.Tests/NumericsHelpers_Test.cs index 575338755..8abb2c7fb 100644 --- a/Robust.UnitTesting/Shared/Maths/NumericsHelpers_Test.cs +++ b/Robust.Shared.Maths.Tests/NumericsHelpers_Test.cs @@ -1,18 +1,16 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.Intrinsics.X86; -using NUnit.Framework; -using Robust.Shared.Maths; -using Microsoft.DotNet.RemoteExecutor; +using System.Diagnostics; using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using Microsoft.DotNet.RemoteExecutor; +using NUnit.Framework; +using Robust.UnitTesting; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(NumericsHelpers))] - public sealed class NumericsHelpers_Test + internal sealed class NumericsHelpers_Test { #region Utils diff --git a/Robust.UnitTesting/Shared/Maths/Ray_Test.cs b/Robust.Shared.Maths.Tests/Ray_Test.cs similarity index 87% rename from Robust.UnitTesting/Shared/Maths/Ray_Test.cs rename to Robust.Shared.Maths.Tests/Ray_Test.cs index 71c523b51..05713c6ce 100644 --- a/Robust.UnitTesting/Shared/Maths/Ray_Test.cs +++ b/Robust.Shared.Maths.Tests/Ray_Test.cs @@ -1,14 +1,13 @@ using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; using Robust.Shared.Physics; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture] [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestOf(typeof(Ray))] - sealed class Ray_Test + internal sealed class Ray_Test { [Test] public void RayIntersectsBoxTest() diff --git a/Robust.Shared.Maths.Tests/Robust.Shared.Maths.Tests.csproj b/Robust.Shared.Maths.Tests/Robust.Shared.Maths.Tests.csproj new file mode 100644 index 000000000..b38a407f6 --- /dev/null +++ b/Robust.Shared.Maths.Tests/Robust.Shared.Maths.Tests.csproj @@ -0,0 +1,24 @@ + + + + + enable + + + + + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/Shared/Maths/SimdHelpersTest.cs b/Robust.Shared.Maths.Tests/SimdHelpersTest.cs similarity index 91% rename from Robust.UnitTesting/Shared/Maths/SimdHelpersTest.cs rename to Robust.Shared.Maths.Tests/SimdHelpersTest.cs index c511bcf02..d4c81ab63 100644 --- a/Robust.UnitTesting/Shared/Maths/SimdHelpersTest.cs +++ b/Robust.Shared.Maths.Tests/SimdHelpersTest.cs @@ -1,8 +1,7 @@ using System.Runtime.Intrinsics; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths; +namespace Robust.Shared.Maths.Tests; [TestFixture] [Parallelizable] diff --git a/Robust.UnitTesting/Shared/Maths/UIBox2_Test.cs b/Robust.Shared.Maths.Tests/UIBox2_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Maths/UIBox2_Test.cs rename to Robust.Shared.Maths.Tests/UIBox2_Test.cs index 9caabaf85..b3941482d 100644 --- a/Robust.UnitTesting/Shared/Maths/UIBox2_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2_Test.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Numerics; +using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(UIBox2))] - public sealed class UIBox2_Test + internal sealed class UIBox2_Test { private static IEnumerable<(float left, float top, float right, float bottom)> Sources => new (float, float, float, float)[] { diff --git a/Robust.UnitTesting/Shared/Maths/UIBox2i_Test.cs b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs similarity index 97% rename from Robust.UnitTesting/Shared/Maths/UIBox2i_Test.cs rename to Robust.Shared.Maths.Tests/UIBox2i_Test.cs index e13dba3c8..604d2c403 100644 --- a/Robust.UnitTesting/Shared/Maths/UIBox2i_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using NUnit.Framework; -using Robust.Shared.Maths; +using NUnit.Framework; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(UIBox2i))] - public sealed class UIBox2i_Test + internal sealed class UIBox2i_Test { private static IEnumerable<(int left, int top, int right, int bottom)> Sources => new (int, int, int, int)[] { diff --git a/Robust.UnitTesting/Shared/Maths/Vector2_Test.cs b/Robust.Shared.Maths.Tests/Vector2_Test.cs similarity index 96% rename from Robust.UnitTesting/Shared/Maths/Vector2_Test.cs rename to Robust.Shared.Maths.Tests/Vector2_Test.cs index a39b2fd66..273beb189 100644 --- a/Robust.UnitTesting/Shared/Maths/Vector2_Test.cs +++ b/Robust.Shared.Maths.Tests/Vector2_Test.cs @@ -1,13 +1,12 @@ using System.Numerics; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture] [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestOf(typeof(Vector2))] - public sealed class Vector2_Test + internal sealed class Vector2_Test { [Test] [Sequential] diff --git a/Robust.UnitTesting/Shared/Maths/Vector2u_Test.cs b/Robust.Shared.Maths.Tests/Vector2u_Test.cs similarity index 82% rename from Robust.UnitTesting/Shared/Maths/Vector2u_Test.cs rename to Robust.Shared.Maths.Tests/Vector2u_Test.cs index 6dcee9b91..4372ccec3 100644 --- a/Robust.UnitTesting/Shared/Maths/Vector2u_Test.cs +++ b/Robust.Shared.Maths.Tests/Vector2u_Test.cs @@ -1,13 +1,12 @@ using System.Text.Json; using NUnit.Framework; -using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Maths +namespace Robust.Shared.Maths.Tests { [TestFixture] [Parallelizable] [TestOf(typeof(Vector2u))] - public sealed class Vector2u_Test + internal sealed class Vector2u_Test { // This test basically only exists because RSI loading needs it. [Test] diff --git a/Robust.Shared.Maths/Box2.cs b/Robust.Shared.Maths/Box2.cs index 6468ffe57..f7c8db88a 100644 --- a/Robust.Shared.Maths/Box2.cs +++ b/Robust.Shared.Maths/Box2.cs @@ -43,7 +43,7 @@ namespace Robust.Shared.Maths [FieldOffset(sizeof(float) * 2)] public Vector2 TopRight; [NonSerialized] - [FieldOffset(sizeof(float) * 0)] public System.Numerics.Vector4 AsVector4; + [FieldOffset(sizeof(float) * 0)] public Vector4 AsVector4; public readonly Vector2 BottomRight { @@ -87,7 +87,7 @@ namespace Robust.Shared.Maths public readonly Vector2 Center { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => BottomLeft + Size * .5f; + get => (BottomLeft + TopRight) * .5f; } public readonly Vector2 Extents @@ -495,7 +495,7 @@ namespace Robust.Shared.Maths return new Vector2(cx, cy); } - public bool EqualsApprox(Box2 other) + public readonly bool EqualsApprox(Box2 other) { return MathHelper.CloseToPercent(Left, other.Left) && MathHelper.CloseToPercent(Bottom, other.Bottom) @@ -503,7 +503,7 @@ namespace Robust.Shared.Maths && MathHelper.CloseToPercent(Top, other.Top); } - public bool EqualsApprox(Box2 other, double tolerance) + public readonly bool EqualsApprox(Box2 other, double tolerance) { return MathHelper.CloseToPercent(Left, other.Left, tolerance) && MathHelper.CloseToPercent(Bottom, other.Bottom, tolerance) diff --git a/Robust.Shared.Maths/Box2Rotated.cs b/Robust.Shared.Maths/Box2Rotated.cs index 79fdd4f4a..0b84b6d55 100644 --- a/Robust.Shared.Maths/Box2Rotated.cs +++ b/Robust.Shared.Maths/Box2Rotated.cs @@ -33,7 +33,31 @@ namespace Robust.Shared.Maths public readonly Vector2 BottomLeft => Origin + Rotation.RotateVec(Box.BottomLeft - Origin); public readonly Vector2 Center => Origin + Rotation.RotateVec((Box.BottomLeft + Box.TopRight)/2 - Origin); - public Matrix3x2 Transform => Matrix3Helpers.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation); + public readonly Matrix3x2 Transform + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Equivalent to Matrix3Helpers.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation) + // but ~20% faster + var angle = (float) Rotation; + var sin = MathF.Sin(angle); + var cos = MathF.Cos(angle); + var cos1 = 1 - cos; + var dx = cos1 * Origin.X + sin * Origin.Y; + var dy = - sin * Origin.X + cos1 * Origin.Y; + + return new Matrix3x2 + { + M11 = cos, + M12 = sin, + M21 = -sin, + M22 = cos, + M31 = dx, + M32 = dy, + }; + } + } public Box2Rotated(Vector2 bottomLeft, Vector2 topRight) : this(new Box2(bottomLeft, topRight)) @@ -71,6 +95,19 @@ namespace Robust.Shared.Maths /// Calculates the smallest AABB that will encompass the rotated box. The AABB is in local space. /// public readonly Box2 CalcBoundingBox() + { + GetVertices(out var x, out var y); + var aabb = SimdHelpers.GetAABB(x, y); + return Unsafe.As, Box2>(ref aabb); + } + + /// + /// Applies the transformation to the box's corners and returns the coordinates in two simd vectors. + /// + /// The corners are ordered clockwise, starting from what was the bottom left corner prior to the transformation. + /// This is effectively a specialized variant of a transform method that avoids having to use construct the matrix via + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly void GetVertices(out Vector128 x, out Vector128 y) { var boxVec = Unsafe.As>(ref Unsafe.AsRef(in Box)); @@ -80,37 +117,11 @@ namespace Robust.Shared.Maths var cos = Vector128.Create((float) Math.Cos(Rotation)); var sin = Vector128.Create((float) Math.Sin(Rotation)); - var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2)); - var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1)); + var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 2, 2, 0)) - originX; + var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 1, 3, 3)) - originY; - allX -= originX; - allY -= originY; - - var modX = allX * cos - allY * sin; - var modY = allX * sin + allY * cos; - - allX = modX + originX; - allY = modY + originY; - - // lrlr = vector containing [left right left right] - Vector128 lbrt; - - if (Sse.IsSupported) - { - var lrlr = SimdHelpers.MinMaxHorizontalSse(allX); - var btbt = SimdHelpers.MinMaxHorizontalSse(allY); - lbrt = Sse.UnpackLow(lrlr, btbt); - } - else - { - var l = SimdHelpers.MinHorizontal128(allX); - var b = SimdHelpers.MinHorizontal128(allY); - var r = SimdHelpers.MaxHorizontal128(allX); - var t = SimdHelpers.MaxHorizontal128(allY); - lbrt = SimdHelpers.MergeRows128(l, b, r, t); - } - - return Unsafe.As, Box2>(ref lbrt); + x = boxX * cos - boxY * sin + originX; + y = boxX * sin + boxY * cos + originY; } public readonly bool Contains(Vector2 worldPoint) @@ -133,7 +144,21 @@ namespace Robust.Shared.Maths /// public readonly bool Equals(Box2Rotated other) { - return Box.Equals(other.Box) && Rotation.Equals(other.Rotation); + return Box.Equals(other.Box) && Rotation.Equals(other.Rotation) && Origin.Equals(other.Origin); + } + + public readonly bool EqualsApprox(Box2Rotated other) + { + return Box.EqualsApprox(other.Box) + && Rotation.EqualsApprox(other.Rotation) + && Origin.EqualsApprox(other.Origin); + } + + public readonly bool EqualsApprox(Box2Rotated other, double tolerance) + { + return Box.EqualsApprox(other.Box, tolerance) + && Rotation.EqualsApprox(other.Rotation, tolerance) + && Origin.EqualsApprox(other.Origin, tolerance); } /// diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs index 96d3d7bff..7174b7e18 100644 --- a/Robust.Shared.Maths/Color.cs +++ b/Robust.Shared.Maths/Color.cs @@ -107,7 +107,6 @@ namespace Robust.Shared.Maths /// The alpha component of the new Color structure. public Color(byte r, byte g, byte b, byte a = 255) { - Unsafe.SkipInit(out this); R = r / (float) byte.MaxValue; G = g / (float) byte.MaxValue; B = b / (float) byte.MaxValue; diff --git a/Robust.Shared.Maths/MathHelper.cs b/Robust.Shared.Maths/MathHelper.cs index 6d4ae8381..36cc27755 100644 --- a/Robust.Shared.Maths/MathHelper.cs +++ b/Robust.Shared.Maths/MathHelper.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace Robust.Shared.Maths { @@ -579,6 +580,17 @@ namespace Robust.Shared.Maths return Math.Abs(a - b) <= epsilon; } + /// + /// Returns whether two floating point numbers are within of eachother + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CloseToPercent(Vector128 a, Vector128 b, float percentage = .00001f) + { + var epsilon = Vector128.Max(Vector128.Max(Vector128.Abs(a), Vector128.Abs(b)) * Vector128.Create(percentage), + Vector128.Create(percentage)); + var result = Vector128.LessThanOrEqual(Vector128.Abs(a - b), epsilon); + return Vector128.EqualsAll(result.AsInt32(), Vector128.AllBitsSet); + } #endregion CloseToPercent #region CloseTo diff --git a/Robust.Shared.Maths/Matrix3Helpers.cs b/Robust.Shared.Maths/Matrix3Helpers.cs index 3062a5fc1..8df6f1c2b 100644 --- a/Robust.Shared.Maths/Matrix3Helpers.cs +++ b/Robust.Shared.Maths/Matrix3Helpers.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; - namespace Robust.Shared.Maths; public static class Matrix3Helpers @@ -33,38 +32,52 @@ public static class Matrix3Helpers } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Box2 TransformBox(this Matrix3x2 refFromBox, Box2Rotated box) + public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2Rotated box) { return (box.Transform * refFromBox).TransformBox(box.Box); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void TransformBox( + this Matrix3x2 refFromBox, + in Box2Rotated box, + out Vector128 x, + out Vector128 y) + { + (box.Transform * refFromBox).TransformBox(box.Box, out x, out y); + } + public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2 box) { - // Do transformation on all 4 corners of the box at once. - // Then min/max the results to get the new AABB. + TransformBox(refFromBox, box, out var x, out var y); + var aabb = SimdHelpers.GetAABB(x, y); + return Unsafe.As, Box2>(ref aabb); + } + /// + /// Applies a transformation matrix to all of a box's corners and returns their coordinates in two simd vectors. + /// + /// The corners are ordered clockwise, starting from what was the bottom left corner prior to the transformation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void TransformBox( + this Matrix3x2 refFromBox, + in Box2 box, + out Vector128 x, + out Vector128 y) + { var boxVec = Unsafe.As>(ref Unsafe.AsRef(in box)); // Convert box into list of X and Y values for each of the 4 corners - var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2)); - var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1)); + var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 2, 2, 0)); + var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 1, 3, 3)); // Transform coordinates - var modX = allX * Vector128.Create(refFromBox.M11); - var modY = allX * Vector128.Create(refFromBox.M12); - modX += allY * Vector128.Create(refFromBox.M21); - modY += allY * Vector128.Create(refFromBox.M22); - modX += Vector128.Create(refFromBox.M31); - modY += Vector128.Create(refFromBox.M32); - - // Get bounding box by finding the min and max X and Y values. - var l = SimdHelpers.MinHorizontal128(modX); - var b = SimdHelpers.MinHorizontal128(modY); - var r = SimdHelpers.MaxHorizontal128(modX); - var t = SimdHelpers.MaxHorizontal128(modY); - - var lbrt = SimdHelpers.MergeRows128(l, b, r, t); - return Unsafe.As, Box2>(ref lbrt); + x = Vector128.Create(refFromBox.M31) + + boxX * Vector128.Create(refFromBox.M11) + + boxY * Vector128.Create(refFromBox.M21); + y = Vector128.Create(refFromBox.M32) + + boxX * Vector128.Create(refFromBox.M12) + + boxY * Vector128.Create(refFromBox.M22); } /// @@ -76,6 +89,23 @@ public static class Matrix3Helpers return new Angle(Math.Atan2(t.M12, t.M11)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix3x2 CreateTransform(float posX, float posY, double angle) + { + // returns a matrix that is equivalent to returning CreateRotation(angle) * CreateTranslation(posX, posY) + var sin = (float) Math.Sin(angle); + var cos = (float) Math.Cos(angle); + return new Matrix3x2 + { + M11 = cos, + M21 = -sin, + M31 = posX, + M12 = sin, + M22 = cos, + M32 = posY, + }; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateTransform(float posX, float posY, double angle, float scaleX = 1, float scaleY = 1) { diff --git a/Robust.Shared.Maths/Properties/AssemblyInfo.cs b/Robust.Shared.Maths/Properties/AssemblyInfo.cs index bbbbaab83..2503abafd 100644 --- a/Robust.Shared.Maths/Properties/AssemblyInfo.cs +++ b/Robust.Shared.Maths/Properties/AssemblyInfo.cs @@ -4,9 +4,13 @@ [module: SkipLocalsInit] #endif +[assembly: InternalsVisibleTo("Robust.Shared")] +[assembly: InternalsVisibleTo("Robust.Server")] [assembly: InternalsVisibleTo("Robust.Client")] [assembly: InternalsVisibleTo("Robust.UnitTesting")] +[assembly: InternalsVisibleTo("Robust.Shared.Maths.Tests")] #if DEVELOPMENT +[assembly: InternalsVisibleTo("Robust.Benchmarks")] [assembly: InternalsVisibleTo("Content.Benchmarks")] #endif diff --git a/Robust.Shared.Maths/SimdHelpers.cs b/Robust.Shared.Maths/SimdHelpers.cs index b89ca5d97..b950c58b6 100644 --- a/Robust.Shared.Maths/SimdHelpers.cs +++ b/Robust.Shared.Maths/SimdHelpers.cs @@ -9,34 +9,6 @@ namespace Robust.Shared.Maths /// internal static class SimdHelpers { - /// A vector with the horizontal minimum and maximum values arranged as { min max min max} . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector128 MinMaxHorizontalSse(Vector128 input) - { - var tmp = Sse.Shuffle(input, input, 0b00_01_10_11); - var min = Sse.Min(tmp, input); - var max = Sse.Max(tmp, input); - tmp = Sse.Shuffle(min, max, 0b01_00_00_01); - min = Sse.Min(tmp, min); - max = Sse.Max(tmp, max); - tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :( - return Sse.Shuffle(tmp, tmp, 0b11_00_11_00); - } - - /// A vector with the horizontal minimum and maximum values arranged as { max min max min} . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector128 MaxMinHorizontalSse(Vector128 input) - { - var tmp = Sse.Shuffle(input, input, 0b00_01_10_11); - var min = Sse.Min(tmp, input); - var max = Sse.Max(tmp, input); - tmp = Sse.Shuffle(min, max, 0b01_00_00_01); - min = Sse.Min(tmp, min); - max = Sse.Max(tmp, max); - tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :( - return Sse.Shuffle(tmp, tmp, 0b00_11_00_11); - } - /// The min value is broadcast to the whole vector. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 MinHorizontal128(Vector128 v) @@ -79,6 +51,84 @@ namespace Robust.Shared.Maths return n + d; } + #region GetAABB + + /// + /// This computes the bounding box given a set of 4 coordinates specified via 2 simd vectors. + /// This effectively computes the horizontal min & max of both of the given vectors. + /// + /// + /// Returns a simd vector that can be directly cast to a . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 GetAABB(Vector128 x, Vector128 y) + { + return Avx.IsSupported ? GetAABBAvx(x, y) : GetAABBSlow(x, y); + } + + /// + /// This computes the bounding box given a set of 4 coordinates specified via 2 simd vectors. + /// This effectively computes the horizontal min & max of both of the given vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 GetAABBAvx(Vector128 x, Vector128 y) + { + // This can be turned into a 256 bit version that only needs 4 min/max instead of 6 + // But the performance difference seems negligible. + + // x = [x0, x1, x2, x3] + // y = [y0, y1, y2, y3] + + var xmin = Vector128.Shuffle(x, Vector128.Create(1, 0, 3, 2)); + xmin = Sse.Min(xmin, x); + // xmin = [min(x0,x1), min(x0,x1), min(x2,x3), min(x2,x3)] + + var ymin = Vector128.Shuffle(y, Vector128.Create(1, 0, 3, 2)); + ymin = Sse.Min(ymin, y); + // ymin = [min(y0,y1), min(y0,x1), min(y2,y3), min(y2,y3)] + + var xymin = Sse41.Blend(xmin, ymin, 0b_1_0_1_0); + // xymin = [min(x0,x1), min(y0,y1), min(x2,x3), min(y2,y3)] + + var xyminPermuted = Avx.Permute(xymin, 0b_00_00_11_10); + // xymin_permuted = [min(x2,x3), min(y2,y3), ..., ... ] + + var min = Sse.Min(xymin, xyminPermuted); + // min = [min(x0,x1,x2,x3), min(y0,y1,y2,y3), ..., ... ] + + var xmax = Vector128.Shuffle(x, Vector128.Create(1, 0, 3, 2)); + xmax = Sse.Max(xmax, x); + // xmax = [max(x0,x1), max(x0,x1), max(x2,x3), max(x2,x3)] + + var ymax = Vector128.Shuffle(y, Vector128.Create(1, 0, 3, 2)); + ymax = Sse.Max(ymax, y); + // ymax = [max(y0,y1), max(y0,y1), max(y2,y3), max(y2,y3)] + + var xymax = Sse41.Blend(xmax, ymax, 0b_1_0_1_0); + // xymax = [max(x0,x1), max(y0,y1), max(x2,x3), max(y2,y3)] + + var xymaxPermuted = Avx.Permute(xymax, 0b_01_00_00_00); + // xymax_permuted = [.., .., max(x0,x1), max(y0,y1) ] + + var max = Sse.Max(xymax, xymaxPermuted); + // max = [.., .., max(x0,x1,x2,x3), max(y0,y1,y2,y3) ] + + // result = [min(x0,x1,x2,x3), min(y0,y1,y2,y3), max(x0,x1,x2,x3), max(y0,y1,y2,y3) ] + return Sse41.Blend(min, max, 0b_1_1_0_0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 GetAABBSlow(Vector128 x, Vector128 y) + { + var l = MinHorizontal128(x); + var b = MinHorizontal128(y); + var r = MaxHorizontal128(x); + var t = MaxHorizontal128(y); + return MergeRows128(l, b, r, t); + } + + #endregion + // Given the following vectors: // x: X X X X // y: Y Y Y Y diff --git a/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj b/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj index db95bfeb2..e9d3e0ab5 100644 --- a/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj +++ b/Robust.Shared.Scripting/Robust.Shared.Scripting.csproj @@ -11,12 +11,14 @@ + + diff --git a/Robust.Shared.Testing/AssemblyInfo.cs b/Robust.Shared.Testing/AssemblyInfo.cs new file mode 100644 index 000000000..f75958c78 --- /dev/null +++ b/Robust.Shared.Testing/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Robust.Server.Testing")] +[assembly: InternalsVisibleTo("Robust.UnitTesting")] diff --git a/Robust.UnitTesting/RTCVars.cs b/Robust.Shared.Testing/RTCVars.cs similarity index 92% rename from Robust.UnitTesting/RTCVars.cs rename to Robust.Shared.Testing/RTCVars.cs index 540bc52b9..d2826c8c3 100644 --- a/Robust.UnitTesting/RTCVars.cs +++ b/Robust.Shared.Testing/RTCVars.cs @@ -2,6 +2,7 @@ using Robust.Shared.Configuration; using Robust.Shared.Log; +// ReSharper disable once CheckNamespace namespace Robust.UnitTesting { // ReSharper disable once InconsistentNaming diff --git a/Robust.Shared.Testing/Robust.Shared.Testing.csproj b/Robust.Shared.Testing/Robust.Shared.Testing.csproj new file mode 100644 index 000000000..0103c7bfa --- /dev/null +++ b/Robust.Shared.Testing/Robust.Shared.Testing.csproj @@ -0,0 +1,21 @@ + + + + + enable + false + + + + + + + + + + + + + + + diff --git a/Robust.UnitTesting/TestLogHandler.cs b/Robust.Shared.Testing/TestLogHandler.cs similarity index 97% rename from Robust.UnitTesting/TestLogHandler.cs rename to Robust.Shared.Testing/TestLogHandler.cs index f52d1ba8c..199a95e0d 100644 --- a/Robust.UnitTesting/TestLogHandler.cs +++ b/Robust.Shared.Testing/TestLogHandler.cs @@ -1,11 +1,10 @@ -using System; using System.Diagnostics; -using System.IO; using NUnit.Framework; using Robust.Shared.Configuration; using Robust.Shared.Log; using Serilog.Events; +// ReSharper disable once CheckNamespace namespace Robust.UnitTesting { public sealed class TestLogHandler : ILogHandler diff --git a/Robust.UnitTesting/TestingModLoader.cs b/Robust.Shared.Testing/TestingModLoader.cs similarity index 92% rename from Robust.UnitTesting/TestingModLoader.cs rename to Robust.Shared.Testing/TestingModLoader.cs index b03a9f432..ae605eff3 100644 --- a/Robust.UnitTesting/TestingModLoader.cs +++ b/Robust.Shared.Testing/TestingModLoader.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; +using System.Reflection; using Robust.Shared.ContentPack; using Robust.Shared.Utility; -namespace Robust.UnitTesting +namespace Robust.Shared.Testing { internal sealed class TestingModLoader : BaseModLoader, IModLoaderInternal { diff --git a/Robust.UnitTesting/TestingParallelManager.cs b/Robust.Shared.Testing/TestingParallelManager.cs similarity index 73% rename from Robust.UnitTesting/TestingParallelManager.cs rename to Robust.Shared.Testing/TestingParallelManager.cs index 0a88eff86..da672cf1c 100644 --- a/Robust.UnitTesting/TestingParallelManager.cs +++ b/Robust.Shared.Testing/TestingParallelManager.cs @@ -1,7 +1,6 @@ -using System; -using System.Threading; using Robust.Shared.Threading; +// ReSharper disable once CheckNamespace namespace Robust.UnitTesting; /// @@ -58,6 +57,24 @@ public sealed class TestingParallelManager : IParallelManagerInternal return ev.WaitHandle; } + public void ProcessNow(IParallelBulkRobustJob jobs, int amount) + { + jobs.ExecuteRange(0, amount); + } + + public void ProcessSerialNow(IParallelBulkRobustJob jobs, int amount) + { + jobs.ExecuteRange(0, amount); + } + + public WaitHandle Process(IParallelBulkRobustJob jobs, int amount) + { + ProcessSerialNow(jobs, amount); + var ev = new ManualResetEventSlim(); + ev.Set(); + return ev.WaitHandle; + } + public void Initialize() { } diff --git a/Robust.UnitTesting/Shared/Audio/AudioGainVolume_Test.cs b/Robust.Shared.Tests/Audio/AudioGainVolume_Test.cs similarity index 96% rename from Robust.UnitTesting/Shared/Audio/AudioGainVolume_Test.cs rename to Robust.Shared.Tests/Audio/AudioGainVolume_Test.cs index bd6a0ca3d..5932b4018 100644 --- a/Robust.UnitTesting/Shared/Audio/AudioGainVolume_Test.cs +++ b/Robust.Shared.Tests/Audio/AudioGainVolume_Test.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Robust.Shared.Audio.Systems; using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Audio; +namespace Robust.Shared.Tests.Audio; [TestFixture] public sealed class AudioGainVolume_Test diff --git a/Robust.UnitTesting/Shared/Collections/OverflowDictionary_Test.cs b/Robust.Shared.Tests/Collections/OverflowDictionary_Test.cs similarity index 93% rename from Robust.UnitTesting/Shared/Collections/OverflowDictionary_Test.cs rename to Robust.Shared.Tests/Collections/OverflowDictionary_Test.cs index 7d7eb7503..840761367 100644 --- a/Robust.UnitTesting/Shared/Collections/OverflowDictionary_Test.cs +++ b/Robust.Shared.Tests/Collections/OverflowDictionary_Test.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Robust.Shared.Collections; -namespace Robust.UnitTesting.Shared.Collections; +namespace Robust.Shared.Tests.Collections; [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture, TestOf(typeof(OverflowQueue<>))] -public sealed class OverflowDictionary_Test +internal sealed class OverflowDictionary_Test { private static IEnumerable<(int size, int iterations)> TestParams => new[] { diff --git a/Robust.UnitTesting/Shared/Collections/OverflowQueue_Test.cs b/Robust.Shared.Tests/Collections/OverflowQueue_Test.cs similarity index 91% rename from Robust.UnitTesting/Shared/Collections/OverflowQueue_Test.cs rename to Robust.Shared.Tests/Collections/OverflowQueue_Test.cs index 9a10fdc02..49e7b48c2 100644 --- a/Robust.UnitTesting/Shared/Collections/OverflowQueue_Test.cs +++ b/Robust.Shared.Tests/Collections/OverflowQueue_Test.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using NUnit.Framework; using Robust.Shared.Collections; -namespace Robust.UnitTesting.Shared.Collections; +namespace Robust.Shared.Tests.Collections; [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture, TestOf(typeof(OverflowQueue<>))] -public sealed class OverflowQueue_Test +internal sealed class OverflowQueue_Test { private static IEnumerable<(int size, int iterations)> TestParams => new[] { diff --git a/Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs b/Robust.Shared.Tests/Collections/RingBufferListTest.cs similarity index 76% rename from Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs rename to Robust.Shared.Tests/Collections/RingBufferListTest.cs index 950bb3f24..cbb8f5464 100644 --- a/Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs +++ b/Robust.Shared.Tests/Collections/RingBufferListTest.cs @@ -1,11 +1,11 @@ using NUnit.Framework; using Robust.Shared.Collections; -namespace Robust.UnitTesting.Shared.Collections; +namespace Robust.Shared.Tests.Collections; [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture, TestOf(typeof(RingBufferList<>))] -public sealed class RingBufferListTest +internal sealed class RingBufferListTest { [Test] public void TestBasicAdd() @@ -15,7 +15,7 @@ public sealed class RingBufferListTest list.Add(2); list.Add(3); - Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3})); + Assert.That(list, Is.EquivalentTo(new[] {1, 2, 3})); } [Test] @@ -34,8 +34,8 @@ public sealed class RingBufferListTest { // Ensure wrapping properly happened and we didn't expand. // (one slot is wasted by nature of implementation) - Assert.That(list.Capacity, NUnit.Framework.Is.EqualTo(6)); - Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] { 2, 3, 4, 5, 6 })); + Assert.That(list.Capacity, Is.EqualTo(6)); + Assert.That(list, Is.EquivalentTo(new[] { 2, 3, 4, 5, 6 })); }); } @@ -58,7 +58,7 @@ public sealed class RingBufferListTest list.Add(5); list.Remove(4); - Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5})); + Assert.That(list, Is.EquivalentTo(new[] {1, 2, 3, 5})); } [Test] @@ -76,7 +76,7 @@ public sealed class RingBufferListTest list.Add(5); list.Remove(3); - Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 4, 5})); + Assert.That(list, Is.EquivalentTo(new[] {1, 2, 4, 5})); } [Test] @@ -90,6 +90,6 @@ public sealed class RingBufferListTest list.Add(5); list.Remove(4); - Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5})); + Assert.That(list, Is.EquivalentTo(new[] {1, 2, 3, 5})); } } diff --git a/Robust.UnitTesting/Shared/ContentPack/AssemblyTypeCheckerParsingTest.cs b/Robust.Shared.Tests/ContentPack/AssemblyTypeCheckerParsingTest.cs similarity index 96% rename from Robust.UnitTesting/Shared/ContentPack/AssemblyTypeCheckerParsingTest.cs rename to Robust.Shared.Tests/ContentPack/AssemblyTypeCheckerParsingTest.cs index 9ef5a4ad2..a892924cc 100644 --- a/Robust.UnitTesting/Shared/ContentPack/AssemblyTypeCheckerParsingTest.cs +++ b/Robust.Shared.Tests/ContentPack/AssemblyTypeCheckerParsingTest.cs @@ -4,11 +4,11 @@ using NUnit.Framework; using Pidgin; using static Robust.Shared.ContentPack.AssemblyTypeChecker; -namespace Robust.UnitTesting.Shared.ContentPack +namespace Robust.Shared.Tests.ContentPack { [Parallelizable(ParallelScope.All)] [TestFixture] - public sealed class AssemblyTypeCheckerParsingTest + internal sealed class AssemblyTypeCheckerParsingTest { [Test] public void TestMethod() diff --git a/Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest2.cs b/Robust.Shared.Tests/ContentPack/ResourceManagerTest2.cs similarity index 96% rename from Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest2.cs rename to Robust.Shared.Tests/ContentPack/ResourceManagerTest2.cs index 3e4faceba..96ca1a306 100644 --- a/Robust.UnitTesting/Shared/ContentPack/ResourceManagerTest2.cs +++ b/Robust.Shared.Tests/ContentPack/ResourceManagerTest2.cs @@ -1,5 +1,4 @@ -using System; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.IoC; @@ -7,12 +6,12 @@ using Robust.Shared.Log; using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.ContentPack; +namespace Robust.Shared.Tests.ContentPack; [TestFixture] [TestOf(typeof(ResourceManager))] [Parallelizable(ParallelScope.All)] -public sealed class ResourceManagerTest2 +internal sealed class ResourceManagerTest2 { [Test] public void TestMemoryRootReadFiles() diff --git a/Robust.UnitTesting/Shared/IoC/DependencyCollectionTest.cs b/Robust.Shared.Tests/IoC/DependencyCollectionTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/IoC/DependencyCollectionTest.cs rename to Robust.Shared.Tests/IoC/DependencyCollectionTest.cs index 503413426..cb2f340e0 100644 --- a/Robust.UnitTesting/Shared/IoC/DependencyCollectionTest.cs +++ b/Robust.Shared.Tests/IoC/DependencyCollectionTest.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Robust.Shared.IoC; -namespace Robust.UnitTesting.Shared.IoC; +namespace Robust.Shared.Tests.IoC; [TestFixture] [TestOf(typeof(DependencyCollection))] diff --git a/Robust.UnitTesting/Shared/IoC/IoCManager_Test.cs b/Robust.Shared.Tests/IoC/IoCManager_Test.cs similarity index 99% rename from Robust.UnitTesting/Shared/IoC/IoCManager_Test.cs rename to Robust.Shared.Tests/IoC/IoCManager_Test.cs index f56cce14a..5b3d9684e 100644 --- a/Robust.UnitTesting/Shared/IoC/IoCManager_Test.cs +++ b/Robust.Shared.Tests/IoC/IoCManager_Test.cs @@ -1,17 +1,16 @@ -using System; using Moq; using NUnit.Framework; using Robust.Shared.Analyzers; using Robust.Shared.IoC; using Robust.Shared.IoC.Exceptions; -namespace Robust.UnitTesting.Shared.IoC +namespace Robust.Shared.Tests.IoC { /// /// This fixture CAN NOT be parallelized, because is a static singleton. /// [TestFixture, TestOf(typeof(IoCManager))] - public sealed class IoCManager_Test + internal sealed class IoCManager_Test { [OneTimeSetUp] public void OneTimeSetup() diff --git a/Robust.UnitTesting/Shared/Localization/FormatErrorTest.cs b/Robust.Shared.Tests/Localization/FormatErrorTest.cs similarity index 93% rename from Robust.UnitTesting/Shared/Localization/FormatErrorTest.cs rename to Robust.Shared.Tests/Localization/FormatErrorTest.cs index 99a6b318a..6cec78b17 100644 --- a/Robust.UnitTesting/Shared/Localization/FormatErrorTest.cs +++ b/Robust.Shared.Tests/Localization/FormatErrorTest.cs @@ -1,13 +1,12 @@ -using System; -using Linguini.Syntax.Parser.Error; +using Linguini.Syntax.Parser.Error; using NUnit.Framework; using Robust.Shared.Localization; -namespace Robust.UnitTesting.Shared.Localization; +namespace Robust.Shared.Tests.Localization; [TestFixture] [Parallelizable] -public sealed class TestFormatErrors +internal sealed class TestFormatErrors { // Error spans a single line private const string ResSingle = "err1 = $user)"; diff --git a/Robust.UnitTesting/Shared/Map/MapBitmask_Tests.cs b/Robust.Shared.Tests/Map/MapBitmask_Tests.cs similarity index 86% rename from Robust.UnitTesting/Shared/Map/MapBitmask_Tests.cs rename to Robust.Shared.Tests/Map/MapBitmask_Tests.cs index 20e67d6e1..941ea3541 100644 --- a/Robust.UnitTesting/Shared/Map/MapBitmask_Tests.cs +++ b/Robust.Shared.Tests/Map/MapBitmask_Tests.cs @@ -1,12 +1,11 @@ -using System.Numerics; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Map; +namespace Robust.Shared.Tests.Map; [TestFixture] -public sealed class MapBitmask_Tests +internal sealed class MapBitmask_Tests { private static readonly TestCaseData[] Cases = new[] { diff --git a/Robust.UnitTesting/Shared/Networking/NetDisconnectMessageTest.cs b/Robust.Shared.Tests/Networking/NetDisconnectMessageTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Networking/NetDisconnectMessageTest.cs rename to Robust.Shared.Tests/Networking/NetDisconnectMessageTest.cs index 0e55e8d0f..61a35797d 100644 --- a/Robust.UnitTesting/Shared/Networking/NetDisconnectMessageTest.cs +++ b/Robust.Shared.Tests/Networking/NetDisconnectMessageTest.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Robust.Shared.Network; -namespace Robust.UnitTesting.Shared.Networking; +namespace Robust.Shared.Tests.Networking; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/Robust.UnitTesting/Shared/Physics/B2DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs similarity index 94% rename from Robust.UnitTesting/Shared/Physics/B2DynamicTree_Test.cs rename to Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs index 5a26d4c04..c632cb87d 100644 --- a/Robust.UnitTesting/Shared/Physics/B2DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; using System.Numerics; using NUnit.Framework; using Robust.Shared.Maths; using Robust.Shared.Physics; -namespace Robust.UnitTesting.Shared.Physics +namespace Robust.Shared.Tests.Physics { [TestFixture] [TestOf(typeof(B2DynamicTree<>))] - public sealed class B2DynamicTree_Test + internal sealed class B2DynamicTree_Test { private static Box2[] aabbs1 = { diff --git a/Robust.UnitTesting/Shared/Physics/DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Physics/DynamicTree_Test.cs rename to Robust.Shared.Tests/Physics/DynamicTree_Test.cs index 85510939c..e73458a8f 100644 --- a/Robust.UnitTesting/Shared/Physics/DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs @@ -1,16 +1,14 @@ -using System; -using System.Linq; using System.Numerics; using NUnit.Framework; using Robust.Shared.Maths; using Robust.Shared.Physics; -namespace Robust.UnitTesting.Shared.Physics +namespace Robust.Shared.Tests.Physics { [TestFixture] [TestOf(typeof(DynamicTree<>))] - public sealed class DynamicTree_Test + internal sealed class DynamicTree_Test { private static Box2[] aabbs1 = diff --git a/Robust.Shared.Tests/Physics/Shapes/SlimPolygonTest.cs b/Robust.Shared.Tests/Physics/Shapes/SlimPolygonTest.cs new file mode 100644 index 000000000..34f940515 --- /dev/null +++ b/Robust.Shared.Tests/Physics/Shapes/SlimPolygonTest.cs @@ -0,0 +1,124 @@ +using System.Numerics; +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Maths.Tests; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Shapes; +using Robust.UnitTesting; + +namespace Robust.Shared.Tests.Physics.Shapes; + +[TestFixture] +[TestOf(typeof(SlimPolygon))] +public sealed class SlimPolygonTest +{ + /// + /// Check that Slim and normal Polygon are equals + /// + [Test] + public void TestSlim() + { + var slim = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); + + var poly = new Polygon(Box2.UnitCentered.Translated(Vector2.One)); + + Assert.That(slim.Equals(poly)); + } + + [Test] + public void TestAABB() + { + var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); + + Assert.That(shape.ComputeAABB(Transform.Empty, 0), Is.EqualTo(Box2.UnitCentered.Translated(Vector2.One))); + } + + [Test] + public void TestBox2() + { + var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); + Assert.That(shape._vertices.AsSpan.ToArray(), Is.EqualTo(new Vector2[] + { + new Vector2(0.5f, 0.5f), + new Vector2(1.5f, 0.5f), + new Vector2(1.5f, 1.5f), + new Vector2(0.5f, 1.5f), + })); + } + + public static IEnumerable<(Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected)> CalcBoundingBoxData + => Box2Rotated_Test.CalcBoundingBoxData; + + [Test] + public void TestBox2Rotated([ValueSource(nameof(CalcBoundingBoxData))] (Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected) dat) + { + var box = new Box2Rotated(dat.baseBox, dat.rotation, dat.origin); + var shape = new SlimPolygon(box); + + Assert.That(shape._vertices._00, Is.Approximately(box.BottomLeft, 0.0001f)); + Assert.That(shape._vertices._01, Is.Approximately(box.BottomRight, 0.0001f)); + Assert.That(shape._vertices._02, Is.Approximately(box.TopRight, 0.0001f)); + Assert.That(shape._vertices._03, Is.Approximately(box.TopLeft, 0.0001f)); + } + + [Test] + public void TestBox2RotatedBounds([ValueSource(nameof(CalcBoundingBoxData))](Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected) dat) + { + var box = new Box2Rotated(dat.baseBox, dat.rotation, dat.origin); + var shape = new SlimPolygon(box); + var aabb = shape.ComputeAABB(Transform.Empty, 0); + Assert.That(aabb, Is.Approximately(dat.expected)); + } + + [Test] + public void TestTransformConstructor([ValueSource(nameof(CalcBoundingBoxData))] (Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected) dat) + { + var box = new Box2Rotated(dat.baseBox, dat.rotation, dat.origin); + var shape = new SlimPolygon(box.Box, box.Transform, out var bounds); + + Assert.That(shape._vertices._00, Is.Approximately(box.BottomLeft, 0.0001f)); + Assert.That(shape._vertices._01, Is.Approximately(box.BottomRight, 0.0001f)); + Assert.That(shape._vertices._02, Is.Approximately(box.TopRight, 0.0001f)); + Assert.That(shape._vertices._03, Is.Approximately(box.TopLeft, 0.0001f)); + Assert.That(box.CalcBoundingBox(), Is.Approximately(bounds, 0.0001f)); + } + + [Test] + public void TestTransformRotatedConstructor([ValueSource(nameof(CalcBoundingBoxData))](Box2 baseBox, Vector2 origin, Angle rotation, Box2 expected) dat) + { + var box = new Box2Rotated(dat.baseBox, dat.rotation, dat.origin); + Matrix3x2.Invert(box.Transform, out var inverse); + var shape = new SlimPolygon(box, inverse, out var bounds); + + Assert.That(shape._vertices._00, Is.Approximately(dat.baseBox.BottomLeft, 0.0001f)); + Assert.That(shape._vertices._01, Is.Approximately(dat.baseBox.BottomRight, 0.0001f)); + Assert.That(shape._vertices._02, Is.Approximately(dat.baseBox.TopRight, 0.0001f)); + Assert.That(shape._vertices._03, Is.Approximately(dat.baseBox.TopLeft, 0.0001f)); + Assert.That(dat.baseBox, Is.Approximately(bounds, 0.0001f)); + } + + [Test] + public void TestComputeAABB() + { + var box = new Box2Rotated(Box2.UnitCentered, Angle.FromDegrees(45), Vector2.One); + var shape = new SlimPolygon(box); + Assert.That(shape._vertices._00, Is.Approximately(box.BottomLeft, 0.0001f)); + Assert.That(shape._vertices._01, Is.Approximately(box.BottomRight, 0.0001f)); + Assert.That(shape._vertices._02, Is.Approximately(box.TopRight, 0.0001f)); + Assert.That(shape._vertices._03, Is.Approximately(box.TopLeft, 0.0001f)); + + // AABB of a 45 degree rotated unit box will be enlarged by a factor of sqrt(2) + var transform = Transform.Empty; + var expected = Box2.UnitCentered.Translated(new Vector2(1, 1 - MathF.Sqrt(2))).Scale(Vector2.One * MathF.Sqrt(2)); + var aabb = shape.ComputeAABB(transform, 0); + Assert.That(aabb, Is.Approximately(expected, 0.0001f)); + Assert.That(aabb.Size, Is.Approximately(Vector2.One * MathF.Sqrt(2), 0.0001f)); + + // But if we pass a 45 degree rotation into ComputeAABB, the box will not be enlarged. + transform = new Transform(Vector2.Zero, Angle.FromDegrees(45)); + expected = Box2.UnitCentered.Translated(new Vector2(1, MathF.Sqrt(2) - 1)); + aabb = shape.ComputeAABB(transform, 0); + Assert.That(aabb, Is.Approximately(expected, 0.0001f)); + Assert.That(aabb.Size, Is.Approximately(Vector2.One, 0.0001f)); + } +} diff --git a/Robust.UnitTesting/Shared/Prototypes/MultiRootGraphTest.cs b/Robust.Shared.Tests/Prototypes/MultiRootGraphTest.cs similarity index 95% rename from Robust.UnitTesting/Shared/Prototypes/MultiRootGraphTest.cs rename to Robust.Shared.Tests/Prototypes/MultiRootGraphTest.cs index 8344f276e..6b166694b 100644 --- a/Robust.UnitTesting/Shared/Prototypes/MultiRootGraphTest.cs +++ b/Robust.Shared.Tests/Prototypes/MultiRootGraphTest.cs @@ -1,12 +1,10 @@ -using System; -using System.Linq; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.Prototypes; -namespace Robust.UnitTesting.Shared.Prototypes; +namespace Robust.Shared.Tests.Prototypes; [TestFixture] -public sealed class MultiRootGraphTest +internal sealed class MultiRootGraphTest { private const string Id1 = "id1"; private const string Id2 = "id2"; diff --git a/Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs b/Robust.Shared.Tests/Random/RandomExtensionsTests.cs similarity index 95% rename from Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs rename to Robust.Shared.Tests/Random/RandomExtensionsTests.cs index a93bb274c..96b561d14 100644 --- a/Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs +++ b/Robust.Shared.Tests/Random/RandomExtensionsTests.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Moq; using NUnit.Framework; using Robust.Shared.Collections; using Robust.Shared.Random; -namespace Robust.UnitTesting.Shared.Random; +namespace Robust.Shared.Tests.Random; /// Instantiable tests for . [TestFixture] -public sealed class RandomExtensionsGetItemsWithListTests : RandomExtensionsTests> +internal sealed class RandomExtensionsGetItemsWithListTests : RandomExtensionsTests> { /// protected override IList CreateCollection() @@ -23,7 +20,7 @@ public sealed class RandomExtensionsGetItemsWithListTests : RandomExtensionsTest /// Instantiable tests for . [TestFixture] -public sealed class RandomExtensionsGetItemsWithSpanTests : RandomExtensionsTests +internal sealed class RandomExtensionsGetItemsWithSpanTests : RandomExtensionsTests { /// protected override string[] CreateCollection() @@ -40,7 +37,7 @@ public sealed class RandomExtensionsGetItemsWithSpanTests : RandomExtensionsTest /// Instantiable tests for . [TestFixture] -public sealed class RandomExtensionsGetItemsWithValueListTests : RandomExtensionsTests> +internal sealed class RandomExtensionsGetItemsWithValueListTests : RandomExtensionsTests> { /// protected override ValueList CreateCollection() @@ -53,7 +50,7 @@ public sealed class RandomExtensionsGetItemsWithValueListTests : RandomExtension } [TestFixture] -public abstract class RandomExtensionsTests +internal abstract class RandomExtensionsTests { protected IRobustRandom _underlyingRandom = default!; diff --git a/Robust.UnitTesting/Shared/Resources/WritableDirProviderTest.cs b/Robust.Shared.Tests/Resources/WritableDirProviderTest.cs similarity index 92% rename from Robust.UnitTesting/Shared/Resources/WritableDirProviderTest.cs rename to Robust.Shared.Tests/Resources/WritableDirProviderTest.cs index 37fba8caa..892836231 100644 --- a/Robust.UnitTesting/Shared/Resources/WritableDirProviderTest.cs +++ b/Robust.Shared.Tests/Resources/WritableDirProviderTest.cs @@ -1,14 +1,12 @@ -using System; -using System.IO; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.ContentPack; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Resources +namespace Robust.Shared.Tests.Resources { [TestFixture] [TestOf(typeof(WritableDirProvider))] - public sealed class WritableDirProviderTest + internal sealed class WritableDirProviderTest { private string _testDirPath = default!; private DirectoryInfo _testDir = default!; diff --git a/Robust.Shared.Tests/RichText/FormattedStringTest.cs b/Robust.Shared.Tests/RichText/FormattedStringTest.cs new file mode 100644 index 000000000..123bb9ade --- /dev/null +++ b/Robust.Shared.Tests/RichText/FormattedStringTest.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using Robust.Shared.RichText; + +namespace Robust.UnitTesting.Shared.RichText; + +[Parallelizable(ParallelScope.All)] +[TestOf(typeof(FormattedString))] +[TestFixture] +internal sealed class FormattedStringTest +{ + /// + /// Test that permissive parsing properly normalizes & passes through markup, as appropriate. + /// + [Test] + [TestCase("", ExpectedResult = "")] + [TestCase("foobar", ExpectedResult = "foobar")] + [TestCase("[whaaaaaa", ExpectedResult = "\\[whaaaaaa")] + [TestCase("\\[whaaaaaa", ExpectedResult = "\\[whaaaaaa")] + [TestCase("[whaaaaaa]wow[/whaaaaaa]", ExpectedResult = "[whaaaaaa]wow[/whaaaaaa]")] + [TestCase("[whaaaaaa]\\[womp[/whaaaaaa]", ExpectedResult = "[whaaaaaa]\\[womp[/whaaaaaa]")] + public static string TestPermissiveNormalize(string input) + { + return FormattedString.FromMarkupPermissive(input).Markup; + } + + [Test] + [TestCase("")] + [TestCase("real")] + [TestCase("[whaaaaaa]wow[/whaaaaaa]")] + public static void TestStrictParse(string input) + { + var str = FormattedString.FromMarkup(input); + + Assert.That(str.Markup, Is.EqualTo(input)); + } + + [Test] + [TestCase("", ExpectedResult = "")] + [TestCase("real", ExpectedResult = "real")] + [TestCase("[real", ExpectedResult = "\\[real")] + [TestCase("\\", ExpectedResult = @"\\")] + public static string TestFromPlainText(string input) + { + return FormattedString.FromPlainText(input).Markup; + } + + [Test] + [TestCase("[whaaaaaawow")] + [TestCase("[whaaaaaawow val=\"]")] + public static void TestStrictThrows(string input) + { + Assert.That(() => FormattedString.FromMarkup(input), Throws.ArgumentException); + } +} diff --git a/Robust.Shared.Tests/Robust.Shared.Tests.csproj b/Robust.Shared.Tests/Robust.Shared.Tests.csproj new file mode 100644 index 000000000..13e223e20 --- /dev/null +++ b/Robust.Shared.Tests/Robust.Shared.Tests.csproj @@ -0,0 +1,30 @@ + + + + + enable + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Robust.Shared.Tests/Serialization/NetSerializerBitArrayTest.cs b/Robust.Shared.Tests/Serialization/NetSerializerBitArrayTest.cs new file mode 100644 index 000000000..7e2ea35be --- /dev/null +++ b/Robust.Shared.Tests/Serialization/NetSerializerBitArrayTest.cs @@ -0,0 +1,115 @@ +using System.Collections; +using System.Text; +using NetSerializer; +using NUnit.Framework; +using Robust.Shared.Serialization; + +namespace Robust.Shared.Tests.Serialization; + +[Parallelizable(ParallelScope.All)] +[TestFixture, TestOf(typeof(NetBitArraySerializer))] +internal sealed class NetSerializerBitArrayTest +{ + // Test that BitArray serialization matches the behavior before .NET 10 + // This test can be removed in future RT versions. + + [Test] + [TestCase("little creature, have you ever heard about gay people?", + "AgAP2KWjxw7YlYOyDOSVi8YO6smrxgXAoIvmDsqByfcN6oGp5g7KyYOCDcqFk8cMwIST9g3q0YPyDMLlg4IOyr2Dxw3K/QHgBg==")] + [TestCase("", "AgABAA==")] + public void Test(string testData, string expected) + { + var bitData = Encoding.UTF8.GetBytes(testData); + var bitArray = new BitArray(bitData); + + var serializer = new Serializer([typeof(BitArray)], + new Settings + { + CustomTypeSerializers = [new NetBitArraySerializer()] + }); + + var stream = new MemoryStream(); + serializer.Serialize(stream, bitArray); + + var base64 = Convert.ToBase64String(stream.ToArray()); + + TestContext.Out.WriteLine(base64); + + Assert.That(base64, Is.EqualTo(expected)); + + stream.Position = 0; + var newBitArray = (BitArray)serializer.Deserialize(stream); + + Assert.That(newBitArray, Is.EquivalentTo(bitArray)); + } + + [Test] + public void Unset() + { + var bitArray = new BitArray(16); + + var serializer = new Serializer([typeof(BitArray)], + new Settings + { + CustomTypeSerializers = [new NetBitArraySerializer()] + }); + + var stream = new MemoryStream(); + serializer.Serialize(stream, bitArray); + + var base64 = Convert.ToBase64String(stream.ToArray()); + + TestContext.Out.WriteLine(base64); + + Assert.That(base64, Is.EqualTo("AgACACA=")); + + stream.Position = 0; + var newBitArray = (BitArray)serializer.Deserialize(stream); + + Assert.That(newBitArray, Is.EquivalentTo(bitArray)); + } + + [Test] + public void TestClass() + { + var obj = new FooBar + { + Real = 0x3005, + Heck = "omg", + Wawa = new BitArray("I miss my wife"u8.ToArray()) + }; + + var serializer = new Serializer([typeof(BitArray), typeof(FooBar)], + new Settings + { + CustomTypeSerializers = [new NetBitArraySerializer()] + }); + + var stream = new MemoryStream(); + serializer.Serialize(stream, obj); + + var base64 = Convert.ToBase64String(stream.ToArray()); + + TestContext.Out.WriteLine(base64); + + Assert.That(base64, Is.EqualTo("AgQDb21nisABAwAFkoHplg3mzYPSDfKBuZcNzJUD4AE=")); + + stream.Position = 0; + var newObject = (FooBar)serializer.Deserialize(stream); + + Assert.Multiple(() => + { + Assert.That(newObject.Real, Is.EqualTo(obj.Real)); + Assert.That(newObject.Heck, Is.EqualTo(obj.Heck)); + Assert.That(newObject.Wawa, Is.EquivalentTo(obj.Wawa)); + }); + } + + [Serializable] + private sealed class FooBar + { + public required int Real; + public required string Heck; + public required BitArray Wawa; + } +} diff --git a/Robust.Shared.Tests/Serialization/NetSerializerFormattedStringTest.cs b/Robust.Shared.Tests/Serialization/NetSerializerFormattedStringTest.cs new file mode 100644 index 000000000..28d9cccca --- /dev/null +++ b/Robust.Shared.Tests/Serialization/NetSerializerFormattedStringTest.cs @@ -0,0 +1,75 @@ +using NetSerializer; +using NUnit.Framework; +using Robust.Shared.RichText; +using Robust.Shared.Serialization; + +namespace Robust.Shared.Tests.Serialization; + +[Parallelizable(ParallelScope.All)] +[TestFixture, TestOf(typeof(NetFormattedStringSerializer))] +internal sealed class NetSerializerFormattedStringTest +{ + [Test] + [TestCase("")] + [TestCase("real")] + [TestCase("[i]heck[/i]")] + public void TestBasic(string markup) + { + var serializer = MakeSerializer(); + + var str = FormattedString.FromMarkup(markup); + + var stream = new MemoryStream(); + serializer.Serialize(stream, str); + stream.Position = 0; + + var deserialized = (FormattedString) serializer.Deserialize(stream); + + Assert.That(deserialized, NUnit.Framework.Is.EqualTo(str)); + } + + /// + /// Test that the on-wire representation of a is the same as a regular string. + /// This is to ensure is a valid test. + /// + [Test] + [TestCase("")] + [TestCase("real")] + [TestCase("[i]heck[/i]")] + public void TestEqualToString(string markup) + { + var serializer = MakeSerializer(); + + var stream = new MemoryStream(); + serializer.SerializeDirect(stream, markup); + stream.Position = 0; + + serializer.DeserializeDirect(stream, out FormattedString str); + + Assert.That(str.Markup, Is.EqualTo(markup)); + } + + /// + /// Test that deserialization fails if a malicious client sends broken markup. + /// + [Test] + public void TestInvalid() + { + var serializer = MakeSerializer(); + + var stream = new MemoryStream(); + serializer.SerializeDirect(stream, "[wahoooo"); + stream.Position = 0; + + Assert.That(() => serializer.DeserializeDirect(stream, out FormattedString _), Throws.Exception); + } + + private static Serializer MakeSerializer() + { + return new Serializer([typeof(FormattedString)], + new Settings + { + CustomTypeSerializers = [new NetFormattedStringSerializer()] + }); + } +} diff --git a/Robust.UnitTesting/Shared/Timing/GameTiming_Test.cs b/Robust.Shared.Tests/Timing/GameTiming_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Timing/GameTiming_Test.cs rename to Robust.Shared.Tests/Timing/GameTiming_Test.cs index 735fa6dd0..1391a69c1 100644 --- a/Robust.UnitTesting/Shared/Timing/GameTiming_Test.cs +++ b/Robust.Shared.Tests/Timing/GameTiming_Test.cs @@ -1,14 +1,13 @@ -using System; -using System.Reflection; +using System.Reflection; using Moq; using NUnit.Framework; using Robust.Shared.Timing; -namespace Robust.UnitTesting.Shared.Timing +namespace Robust.Shared.Tests.Timing { [TestFixture] [TestOf(typeof(GameTiming))] - sealed class GameTiming_Test + internal sealed class GameTiming_Test { /// /// Checks that IGameTiming.RealTime returns the real(wall) uptime since the stopwatch was started. diff --git a/Robust.UnitTesting/Shared/Utility/CaseConversionTest.cs b/Robust.Shared.Tests/Utility/CaseConversionTest.cs similarity index 89% rename from Robust.UnitTesting/Shared/Utility/CaseConversionTest.cs rename to Robust.Shared.Tests/Utility/CaseConversionTest.cs index 7ec501d21..56b97964b 100644 --- a/Robust.UnitTesting/Shared/Utility/CaseConversionTest.cs +++ b/Robust.Shared.Tests/Utility/CaseConversionTest.cs @@ -1,12 +1,12 @@ using NUnit.Framework; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [Parallelizable(ParallelScope.All)] [TestFixture] [TestOf(typeof(CaseConversion))] - public sealed class CaseConversionTest + internal sealed class CaseConversionTest { [Test] [TestCase("FooBar", ExpectedResult = "foo-bar")] diff --git a/Robust.UnitTesting/Shared/Utility/CollectionExtensions_Test.cs b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs similarity index 85% rename from Robust.UnitTesting/Shared/Utility/CollectionExtensions_Test.cs rename to Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs index c95265cf5..eeabd0c76 100644 --- a/Robust.UnitTesting/Shared/Utility/CollectionExtensions_Test.cs +++ b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] - public sealed class CollectionExtensions_Test + internal sealed class CollectionExtensions_Test { [Test] public void RemoveSwapTest() diff --git a/Robust.UnitTesting/Shared/Utility/CommandParsing_Test.cs b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs similarity index 90% rename from Robust.UnitTesting/Shared/Utility/CommandParsing_Test.cs rename to Robust.Shared.Tests/Utility/CommandParsing_Test.cs index 9524ee6a0..b99089f2b 100644 --- a/Robust.UnitTesting/Shared/Utility/CommandParsing_Test.cs +++ b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs @@ -1,13 +1,12 @@ -using System.Collections.Generic; using NUnit.Framework; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] [TestOf(typeof(CommandParsing))] - public sealed class CommandParsing_Test + internal sealed class CommandParsing_Test { [TestCase("foo bar", new[] {"foo", "bar"})] [TestCase("foo bar", new[] {"foo", "bar"})] diff --git a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs b/Robust.Shared.Tests/Utility/FormattedMessage_Test.cs similarity index 82% rename from Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs rename to Robust.Shared.Tests/Utility/FormattedMessage_Test.cs index cf22363d8..56df9f21c 100644 --- a/Robust.UnitTesting/Shared/Utility/FormattedMessage_Test.cs +++ b/Robust.Shared.Tests/Utility/FormattedMessage_Test.cs @@ -1,14 +1,13 @@ -using System.Linq; using NUnit.Framework; using Robust.Shared.Maths; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [Parallelizable(ParallelScope.All)] [TestFixture] [TestOf(typeof(FormattedMessage))] - public sealed class FormattedMessage_Test + internal sealed class FormattedMessage_Test { [Test] public static void TestParseMarkup() @@ -83,5 +82,18 @@ namespace Robust.UnitTesting.Shared.Utility message.EnumerateRunes(), Is.EquivalentTo(message.ToString().EnumerateRunes())); } + + /// + /// Test that the given formatted message string provides equal result when output & parsed again. + /// + [Test] + [TestCase("\\[whaaaaa")] + public static void TestRoundTrip(string markup) + { + var message = FormattedMessage.FromMarkupOrThrow(markup); + var secondMessage = FormattedMessage.FromMarkupOrThrow(message.ToMarkup()); + + Assert.That(secondMessage, NUnit.Framework.Is.EqualTo(message)); + } } } diff --git a/Robust.UnitTesting/Shared/Utility/MarkupNodeTest.cs b/Robust.Shared.Tests/Utility/MarkupNodeTest.cs similarity index 94% rename from Robust.UnitTesting/Shared/Utility/MarkupNodeTest.cs rename to Robust.Shared.Tests/Utility/MarkupNodeTest.cs index b65ea186c..d7c8aa1c6 100644 --- a/Robust.UnitTesting/Shared/Utility/MarkupNodeTest.cs +++ b/Robust.Shared.Tests/Utility/MarkupNodeTest.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using Robust.Shared.Maths; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility; +namespace Robust.Shared.Tests.Utility; [TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(MarkupNode))] -public sealed class MarkupNodeTest +internal sealed class MarkupNodeTest { [Test] [SuppressMessage("Assertion", "NUnit2009:The same value has been provided as both the actual and the expected argument")] diff --git a/Robust.UnitTesting/Shared/Utility/MathHelper_Test.cs b/Robust.Shared.Tests/Utility/MathHelper_Test.cs similarity index 98% rename from Robust.UnitTesting/Shared/Utility/MathHelper_Test.cs rename to Robust.Shared.Tests/Utility/MathHelper_Test.cs index a95c642d6..258275d5d 100644 --- a/Robust.UnitTesting/Shared/Utility/MathHelper_Test.cs +++ b/Robust.Shared.Tests/Utility/MathHelper_Test.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Numerics; +using System.Numerics; using NUnit.Framework; using Robust.Shared.Maths; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [TestFixture, Parallelizable] [TestOf(typeof(MathHelper))] - public sealed class MathHelper_Test + internal sealed class MathHelper_Test { public static IEnumerable<(long val, long result)> LongNextPowerOfTwoData = new (long, long)[] { diff --git a/Robust.UnitTesting/Shared/Utility/NullableHelper_Test.cs b/Robust.Shared.Tests/Utility/NullableHelper_Test.cs similarity index 96% rename from Robust.UnitTesting/Shared/Utility/NullableHelper_Test.cs rename to Robust.Shared.Tests/Utility/NullableHelper_Test.cs index 6fd267266..2a11dd903 100644 --- a/Robust.UnitTesting/Shared/Utility/NullableHelper_Test.cs +++ b/Robust.Shared.Tests/Utility/NullableHelper_Test.cs @@ -3,11 +3,11 @@ using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [TestFixture] [TestOf(typeof(NullableHelper))] - public sealed class NullableHelper_Test + internal sealed class NullableHelper_Test { [SetUp] public void Setup() diff --git a/Robust.Shared.Tests/Utility/PrettyPrint_Test.cs b/Robust.Shared.Tests/Utility/PrettyPrint_Test.cs new file mode 100644 index 000000000..086163aae --- /dev/null +++ b/Robust.Shared.Tests/Utility/PrettyPrint_Test.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using Robust.Shared.Utility; + +// ReSharper disable once CheckNamespace +namespace Robust.Shared.TestPrettyPrint; + +public sealed class Foo +{ + override public string ToString() { return "ACustomFooRep"; } +} + +public sealed class Bar {} + +[TestFixture] +[Parallelizable(ParallelScope.Fixtures | ParallelScope.All)] +[TestOf(typeof(PrettyPrint))] +internal sealed class PrettyPrint_Test +{ + private static IEnumerable<(object val, string expectedRep, string expectedTypeRep)> TestCases { get; } = new (object, string, string)[] + { + (new Foo(), "ACustomFooRep", "R.Sh.TestPrettyPrint.Foo"), + (new Robust.Shared.TestPrettyPrint.Bar(), "R.Sh.TestPrettyPrint.Bar", ""), + }; + + [Test] + public void Test([ValueSource(nameof(TestCases))] (object value, string expectedRep, string expectedTypeRep) data) + { + Assert.That(PrettyPrint.PrintUserFacingWithType(data.value, out var typeRep), Is.EqualTo(data.expectedRep)); + Assert.That(typeRep, Is.EqualTo(data.expectedTypeRep)); + } +} diff --git a/Robust.UnitTesting/Shared/Utility/ResPathTest.cs b/Robust.Shared.Tests/Utility/ResPathTest.cs similarity index 98% rename from Robust.UnitTesting/Shared/Utility/ResPathTest.cs rename to Robust.Shared.Tests/Utility/ResPathTest.cs index d4a84fedc..bb46211fc 100644 --- a/Robust.UnitTesting/Shared/Utility/ResPathTest.cs +++ b/Robust.Shared.Tests/Utility/ResPathTest.cs @@ -1,13 +1,12 @@ -using System; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility; +namespace Robust.Shared.Tests.Utility; [TestFixture] [Parallelizable(ParallelScope.Fixtures | ParallelScope.All)] [TestOf(typeof(ResPath))] -public sealed class ResPathTest +internal sealed class ResPathTest { [Test] [TestCase("foo", ExpectedResult = "")] diff --git a/Robust.UnitTesting/Shared/Utility/TextRope_Test.cs b/Robust.Shared.Tests/Utility/TextRope_Test.cs similarity index 96% rename from Robust.UnitTesting/Shared/Utility/TextRope_Test.cs rename to Robust.Shared.Tests/Utility/TextRope_Test.cs index 1902ddd5e..8ced388f1 100644 --- a/Robust.UnitTesting/Shared/Utility/TextRope_Test.cs +++ b/Robust.Shared.Tests/Utility/TextRope_Test.cs @@ -1,14 +1,12 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using NUnit.Framework; +using NUnit.Framework; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility; +namespace Robust.Shared.Tests.Utility; [TestFixture] [TestOf(typeof(Rope))] [Parallelizable(ParallelScope.All)] -public static class TextRope_Test +internal static class TextRope_Test { [Test] public static void TestCalcWeight() diff --git a/Robust.Shared.Tests/Utility/TypeAbbreviations_Test.cs b/Robust.Shared.Tests/Utility/TypeAbbreviations_Test.cs new file mode 100644 index 000000000..70417e558 --- /dev/null +++ b/Robust.Shared.Tests/Utility/TypeAbbreviations_Test.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using Robust.Shared.Utility; + +// ReSharper disable once CheckNamespace +namespace Robust.Shared.TestTypeAbbreviation; + +public sealed class Foo {} + +public sealed class Bar {} + +[TestFixture] +[Parallelizable(ParallelScope.Fixtures | ParallelScope.All)] +[TestOf(typeof(TypeAbbreviation))] +public sealed class TypeAbbreviations_Test +{ + private static IEnumerable<(string name, string expected)> NameTestCases { get; } = new[] + { + ("Robust.Shared.GameObjects.Foo", "R.Sh.GO.Foo"), + ("Robust.Client.GameObjects.Foo", "R.C.GO.Foo"), + ("Content.Client.GameObjects.Foo", "C.C.GO.Foo"), + ("Robust.Shared.Maths.Vector2", "R.Sh.M.Vector2"), + ("System.Collections.Generic.List", "S.C.G.List"), + ("System.Math", "S.Math"), + }; + + [Test] + public void Test([ValueSource(nameof(NameTestCases))] (string name, string expected) data) + { + Assert.That(TypeAbbreviation.Abbreviate(data.name), Is.EqualTo(data.expected)); + } + + + private static IEnumerable<(Type type, string expected)> TypeTestCases { get; } = new[] + { + ( typeof(Robust.Shared.TestTypeAbbreviation.Foo) + , "R.Sh.TestTypeAbbreviation.Foo`1[R.Sh.TestTypeAbbreviation.Bar]" + ), + (typeof(Robust.Shared.TestTypeAbbreviation.Bar), "R.Sh.TestTypeAbbreviation.Bar"), + }; + + [Test] + public void Test([ValueSource(nameof(TypeTestCases))] (Type type, string expected) data) + { + Assert.That(TypeAbbreviation.Abbreviate(data.type), Is.EqualTo(data.expected)); + } +} diff --git a/Robust.UnitTesting/Shared/Utility/TypeHelpers_Test.cs b/Robust.Shared.Tests/Utility/TypeHelpers_Test.cs similarity index 94% rename from Robust.UnitTesting/Shared/Utility/TypeHelpers_Test.cs rename to Robust.Shared.Tests/Utility/TypeHelpers_Test.cs index 2842259a3..f0e8d6cba 100644 --- a/Robust.UnitTesting/Shared/Utility/TypeHelpers_Test.cs +++ b/Robust.Shared.Tests/Utility/TypeHelpers_Test.cs @@ -1,8 +1,9 @@ using System.Diagnostics.CodeAnalysis; using NUnit.Framework; +using Robust.Shared.Analyzers; using Robust.Shared.Utility; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [TestFixture] [TestOf(typeof(TypeHelpers))] diff --git a/Robust.UnitTesting/Shared/Utility/YamlHelpers_Test.cs b/Robust.Shared.Tests/Utility/YamlHelpers_Test.cs similarity index 84% rename from Robust.UnitTesting/Shared/Utility/YamlHelpers_Test.cs rename to Robust.Shared.Tests/Utility/YamlHelpers_Test.cs index 83141059d..d4b8c6610 100644 --- a/Robust.UnitTesting/Shared/Utility/YamlHelpers_Test.cs +++ b/Robust.Shared.Tests/Utility/YamlHelpers_Test.cs @@ -1,14 +1,13 @@ -using System; -using System.Globalization; +using System.Globalization; using NUnit.Framework; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; -namespace Robust.UnitTesting.Shared.Utility +namespace Robust.Shared.Tests.Utility { [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] [TestFixture] - public sealed class YamlHelpers_Test : RobustUnitTest + public sealed class YamlHelpers_Test { [Test] [SetCulture("fr-FR")] diff --git a/Robust.Shared/Analyzers/NotContentImplementable.cs b/Robust.Shared/Analyzers/NotContentImplementable.cs new file mode 100644 index 000000000..0eedf32bc --- /dev/null +++ b/Robust.Shared/Analyzers/NotContentImplementable.cs @@ -0,0 +1,13 @@ +using System; + +namespace Robust.Shared.Analyzers; + +/// +/// Marker attribute specifying that content must not implement this interface itself. +/// +/// +/// Interfaces with this attribute may have members added by the engine at any time, +/// so implementing them yourself would not be API-stable. +/// +[AttributeUsage(AttributeTargets.Interface)] +public sealed class NotContentImplementableAttribute : Attribute; diff --git a/Robust.Shared/Analyzers/ValidateMemberAttribute.cs b/Robust.Shared/Analyzers/ValidateMemberAttribute.cs new file mode 100644 index 000000000..7b2481922 --- /dev/null +++ b/Robust.Shared/Analyzers/ValidateMemberAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Robust.Shared.Analyzers; + +/// +/// Verifies that a string parameter matches the name +/// of a member of the first type argument. +/// +/// +/// This just does a string comparison with the member name. +/// An identically-named member on a different class will be +/// considered valid. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ValidateMemberAttribute : Attribute; diff --git a/Robust.Shared/AssemblyInfo.cs b/Robust.Shared/AssemblyInfo.cs index 2154102ed..1120f96dd 100644 --- a/Robust.Shared/AssemblyInfo.cs +++ b/Robust.Shared/AssemblyInfo.cs @@ -12,6 +12,12 @@ [assembly: InternalsVisibleTo("Robust.Benchmarks")] [assembly: InternalsVisibleTo("Robust.Client.WebView")] [assembly: InternalsVisibleTo("Robust.Packaging")] +[assembly: InternalsVisibleTo("Robust.Shared.Tests")] +[assembly: InternalsVisibleTo("Robust.Server.IntegrationTests")] +[assembly: InternalsVisibleTo("Robust.Server.Testing")] +[assembly: InternalsVisibleTo("Robust.Shared.Testing")] +[assembly: InternalsVisibleTo("Robust.Client.IntegrationTests")] +[assembly: InternalsVisibleTo("Robust.Shared.IntegrationTests")] #if NET5_0_OR_GREATER [module: SkipLocalsInit] diff --git a/Robust.Shared/Asynchronous/TaskManager.cs b/Robust.Shared/Asynchronous/TaskManager.cs index 709b46df5..d18a68d4a 100644 --- a/Robust.Shared/Asynchronous/TaskManager.cs +++ b/Robust.Shared/Asynchronous/TaskManager.cs @@ -50,6 +50,7 @@ namespace Robust.Shared.Asynchronous private static readonly SendOrPostCallback _runCallback = o => { ((Action?)o)?.Invoke(); }; } + [NotContentImplementable] public interface ITaskManager { void Initialize(); diff --git a/Robust.Shared/Audio/AudioPresetPrototype.cs b/Robust.Shared/Audio/AudioPresetPrototype.cs index 5a6d85882..219d3dc06 100644 --- a/Robust.Shared/Audio/AudioPresetPrototype.cs +++ b/Robust.Shared/Audio/AudioPresetPrototype.cs @@ -13,7 +13,7 @@ namespace Robust.Shared.Audio; public sealed partial class AudioPresetPrototype : IPrototype { [IdDataField] - public string ID { get; } = default!; + public string ID { get; private set; } = default!; /// /// Should the engine automatically create an auxiliary audio effect slot for this. diff --git a/Robust.Shared/Audio/Effects/IAudioEffect.cs b/Robust.Shared/Audio/Effects/IAudioEffect.cs index 4b5cdcaba..98e381a19 100644 --- a/Robust.Shared/Audio/Effects/IAudioEffect.cs +++ b/Robust.Shared/Audio/Effects/IAudioEffect.cs @@ -4,6 +4,7 @@ using Robust.Shared.Maths; namespace Robust.Shared.Audio.Effects; +[NotContentImplementable] public interface IAudioEffect { /// diff --git a/Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs b/Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs index 47e67ea94..14aa47380 100644 --- a/Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs +++ b/Robust.Shared/Audio/Effects/IAuxiliaryAudio.cs @@ -2,6 +2,7 @@ using System; namespace Robust.Shared.Audio.Effects; +[NotContentImplementable] public interface IAuxiliaryAudio : IDisposable { /// diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 8cc9314a5..82b730d28 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -455,7 +455,7 @@ public abstract partial class SharedAudioSystem : EntitySystem if (uid == null || !Resolve(uid.Value, ref component, false)) return null; - if (!Timing.IsFirstTimePredicted || (_netManager.IsClient && !IsClientSide(uid.Value))) + if (_netManager.IsClient && !IsClientSide(uid.Value)) return null; QueueDel(uid); diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index de40da172..c987fbf9f 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -788,6 +788,12 @@ namespace Robust.Shared public static readonly CVarDef GameAutoPauseEmpty = CVarDef.Create("game.auto_pause_empty", true, CVar.SERVERONLY); + /// + /// Scales the game simulation time. Higher values make the game slower. + /// + public static readonly CVarDef GameTimeScale = + CVarDef.Create("game.time_scale", 1f, CVar.REPLICATED | CVar.SERVER); + /* * LOG */ @@ -1280,13 +1286,6 @@ namespace Robust.Shared * PHYSICS */ - /// - /// How much to expand broadphase checking for. This is useful for cross-grid collisions. - /// Performance impact if additional broadphases are being checked. - /// - public static readonly CVarDef BroadphaseExpand = - CVarDef.Create("physics.broadphase_expand", 2f, CVar.ARCHIVE | CVar.REPLICATED); - /// /// The target minimum ticks per second on the server. /// This is used for substepping and will help with clipping/physics issues and such. @@ -1836,6 +1835,15 @@ namespace Robust.Shared /// public static readonly CVarDef CfgCheckUnused = CVarDef.Create("cfg.check_unused", true); + /// + /// Storage for CVars that should be rolled back next client startup. + /// + /// + /// This CVar is utilized through 's rollback functionality. + /// + internal static readonly CVarDef + CfgRollbackData = CVarDef.Create("cfg.rollback_data", "", CVar.ARCHIVE); + /* * Network Resource Manager */ @@ -1914,6 +1922,51 @@ namespace Robust.Shared /// By default, this is Space Station 14's sln, but it can be any file at the same root level. /// public static readonly CVarDef XamlHotReloadMarkerName = - CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY); + CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.slnx", CVar.CLIENTONLY); + + /// + /// If true, all XAML UIs will be JITed for hot reload on client startup. + /// If false, they will be JITed on demand. + /// + public static readonly CVarDef UIXamlJitPreload = + CVarDef.Create("ui.xaml_jit_preload", false, CVar.CLIENTONLY); + + /* + * FONT + */ + + /// + /// If false, disable system font support. + /// + public static readonly CVarDef FontSystem = + CVarDef.Create("font.system", true, CVar.CLIENTONLY); + + /// + /// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API. + /// + public static readonly CVarDef FontWindowsDownloadable = + CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE); + + /* + * LOADING + */ + + /// + /// Whether to show explicit loading bar during client initialization. + /// + public static readonly CVarDef LoadingShowBar = + CVarDef.Create("loading.show_bar", true, CVar.CLIENTONLY); + +#if TOOLS + private const bool DefaultShowDebug = true; +#else + private const bool DefaultShowDebug = false; +#endif + + /// + /// Whether to show "debug" info in the loading screen. + /// + public static readonly CVarDef LoadingShowDebug = + CVarDef.Create("loading.show_debug", DefaultShowDebug, CVar.CLIENTONLY); } } diff --git a/Robust.Shared/Configuration/CVar.cs b/Robust.Shared/Configuration/CVar.cs index 858af95b3..479c55eba 100644 --- a/Robust.Shared/Configuration/CVar.cs +++ b/Robust.Shared/Configuration/CVar.cs @@ -9,38 +9,44 @@ namespace Robust.Shared.Configuration public enum CVar : short { /// - /// No special flags. + /// No special flags. /// NONE = 0, /// - /// Debug vars that are considered 'cheating' to change. + /// Debug vars that are considered 'cheating' to change. /// CHEAT = 1, /// - /// Only the server can change this variable. + /// Only the server can change this variable. /// SERVER = 2, /// - /// This can only be changed when not connected to a server. + /// This can only be changed when not connected to a server. /// NOT_CONNECTED = 4, /// - /// Changing this var syncs between clients and server. + /// Changing this var syncs between clients and server. /// + /// + /// Should only ever be used on shared CVars. + /// REPLICATED = 8, /// - /// Non-default values are saved to the configuration file. + /// Non-default values are saved to the configuration file. /// ARCHIVE = 16, /// - /// Changing this var on the server notifies all clients, does nothing client-side. + /// Changing this var on the server notifies all clients, does nothing client-side. /// + /// + /// Should only ever be used on shared CVars. + /// NOTIFY = 32, /// @@ -60,15 +66,15 @@ namespace Robust.Shared.Configuration CLIENTONLY = 128, /// - /// CVar contains sensitive data that should not be accidentally leaked. + /// CVar contains sensitive data that should not be accidentally leaked. /// /// - /// This currently hides the content of the cvar in the "cvar" command completions. + /// This currently hides the content of the cvar in the "cvar" command completions. /// CONFIDENTIAL = 256, /// - /// Only the client can change this variable. + /// Only the client can change this variable. /// CLIENT = 512, } diff --git a/Robust.Shared/Configuration/CVarDef.cs b/Robust.Shared/Configuration/CVarDef.cs index ed2320a41..d9bb66707 100644 --- a/Robust.Shared/Configuration/CVarDef.cs +++ b/Robust.Shared/Configuration/CVarDef.cs @@ -3,11 +3,29 @@ using JetBrains.Annotations; namespace Robust.Shared.Configuration { + /// + /// Abstract base class for . You shouldn't inherit this yourself and may be looking for + /// + /// public abstract class CVarDef { + /// + /// The default value of this CVar when no override is specified by configuration or the user. + /// public object DefaultValue { get; } + /// + /// Flags for this CVar. + /// public CVar Flags { get; } + /// + /// The name of this CVar. This needs to contain only printable characters. + /// Periods '.' are reserved. Everything before the last period is a nested table identifier, + /// everything after is the CVar name in the TOML document. + /// public string Name { get; } + /// + /// The description of this CVar. + /// public string? Desc { get; } private protected CVarDef(string name, object defaultValue, CVar flags, string? desc) @@ -18,6 +36,14 @@ namespace Robust.Shared.Configuration Desc = desc; } + /// + /// Creates a new CVar definition, for use in -annotated classes. + /// + /// See . + /// See . + /// See . + /// See . + /// The type of the CVar, which can be any of: bool, int, long, float, string, any enum, and ushort. public static CVarDef Create( string name, T defaultValue, @@ -28,6 +54,12 @@ namespace Robust.Shared.Configuration } } + /// + /// Contains information defining a CVar for + /// + /// The type of the CVar, which can be any of: bool, int, long, float, string, any enum, and ushort. + /// + /// public sealed class CVarDef : CVarDef where T : notnull { public new T DefaultValue { get; } @@ -39,6 +71,28 @@ namespace Robust.Shared.Configuration } } + /// + /// Marks a static class as containing CVar definitions. + /// + /// + /// + /// There is no limit on the number of CVarDefs classes you can have, and all CVars will ultimately share the + /// same namespace regardless of which class they're in. + /// + /// + /// CVar definitions can be in any assembly, but should never be marked or + /// if not in a shared assembly. + /// + /// + /// + /// + /// public static class MyCVars + /// { + /// public static readonly CVarDef<bool> MyEnabled = + /// CVarDef.Create("mycvars.enabled", true, CVar.SERVER, "Enables the thing."); + /// } + /// + /// [AttributeUsage(AttributeTargets.Class)] [MeansImplicitUse] public sealed class CVarDefsAttribute : Attribute diff --git a/Robust.Shared/Configuration/ConfigurationCommands.cs b/Robust.Shared/Configuration/ConfigurationCommands.cs index 8fefec933..2ddb71aa7 100644 --- a/Robust.Shared/Configuration/ConfigurationCommands.cs +++ b/Robust.Shared/Configuration/ConfigurationCommands.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.Console; @@ -58,6 +59,25 @@ namespace Robust.Shared.Configuration throw new NotSupportedException(); } + + internal static IEnumerable GetCVarCompletionOptions(IConfigurationManager cfg) + { + return cfg.GetRegisteredCVars() + .Select(c => new CompletionOption(c, GetCVarValueHint(cfg, c))); + } + + private static string GetCVarValueHint(IConfigurationManager cfg, string cVar) + { + var flags = cfg.GetCVarFlags(cVar); + if ((flags & CVar.CONFIDENTIAL) != 0) + return Loc.GetString("cmd-cvar-value-hidden"); + + var value = cfg.GetCVar(cVar).ToString() ?? ""; + if (value.Length > 50) + value = $"{value[..51]}…"; + + return value; + } } [SuppressMessage("ReSharper", "StringLiteralTypo")] @@ -120,8 +140,7 @@ namespace Robust.Shared.Configuration var helpQuestion = Loc.GetString("cmd-cvar-compl-list"); return CompletionResult.FromHintOptions( - _cfg.GetRegisteredCVars() - .Select(c => new CompletionOption(c, GetCVarValueHint(_cfg, c))) + CVarCommandUtil.GetCVarCompletionOptions(_cfg) .Union(new[] { new CompletionOption("?", helpQuestion) }) .OrderBy(c => c.Value), Loc.GetString("cmd-cvar-arg-name")); @@ -134,19 +153,6 @@ namespace Robust.Shared.Configuration var type = _cfg.GetCVarType(cvar); return CompletionResult.FromHint($"<{type.Name}>"); } - - private string GetCVarValueHint(IConfigurationManager cfg, string cVar) - { - var flags = cfg.GetCVarFlags(cVar); - if ((flags & CVar.CONFIDENTIAL) != 0) - return Loc.GetString("cmd-cvar-value-hidden"); - - var value = cfg.GetCVar(cVar).ToString() ?? ""; - if (value.Length > 50) - value = $"{value[..51]}…"; - - return value; - } } internal sealed class CVarSubsCommand : LocalizedCommands @@ -191,4 +197,83 @@ namespace Robust.Shared.Configuration return CompletionResult.Empty; } } + + internal sealed class ConfigMarkRollbackCommand : IConsoleCommand + { + [Dependency] private readonly IConfigurationManager _cfg = null!; + + public string Command => "config_rollback_mark"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length is < 1 or > 2) + { + shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error")); + return; + } + + _cfg.MarkForRollback(args[0]); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromOptions( + CVarCommandUtil.GetCVarCompletionOptions(_cfg) + .OrderBy(c => c.Value)); + } + + return CompletionResult.Empty; + } + } + + internal sealed class ConfigUnmarkRollbackCommand : IConsoleCommand + { + [Dependency] private readonly IConfigurationManager _cfg = null!; + + public string Command => "config_rollback_unmark"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length is < 1 or > 2) + { + shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error")); + return; + } + + _cfg.UnmarkForRollback(args[0]); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromOptions( + CVarCommandUtil.GetCVarCompletionOptions(_cfg) + .OrderBy(c => c.Value)); + } + + return CompletionResult.Empty; + } + } + + + internal sealed class ConfigApplyRollbackCommand : IConsoleCommand + { + [Dependency] private readonly IConfigurationManager _cfg = null!; + + public string Command => "config_rollback_apply"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + _cfg.ApplyRollback(); + } + } } diff --git a/Robust.Shared/Configuration/ConfigurationManager.Rollback.cs b/Robust.Shared/Configuration/ConfigurationManager.Rollback.cs new file mode 100644 index 000000000..b95a433fe --- /dev/null +++ b/Robust.Shared/Configuration/ConfigurationManager.Rollback.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nett; + +namespace Robust.Shared.Configuration; + +internal partial class ConfigurationManager +{ + public void MarkForRollback(params CVarDef[] cVars) + { + MarkForRollback(cVars.Select(c => c.Name).ToArray()); + } + + public void MarkForRollback(params string[] cVars) + { + var alreadyPending = LoadPendingRollbackTable() ?? []; + + foreach (var cVar in cVars) + { + alreadyPending[cVar] = GetCVar(cVar); + } + + SavePendingRollbackTable(alreadyPending); + } + + public void UnmarkForRollback(params CVarDef[] cVars) + { + UnmarkForRollback(cVars.Select(c => c.Name).ToArray()); + } + + public void UnmarkForRollback(params string[] cVars) + { + var alreadyPending = LoadPendingRollbackTable() ?? []; + + foreach (var cVar in cVars) + { + alreadyPending.Remove(cVar); + } + + SavePendingRollbackTable(alreadyPending); + } + + private void SavePendingRollbackTable(Dictionary pending) + { + var tbl = SaveToTomlTable(pending.Keys, cVar => pending[cVar]); + var str = Toml.WriteString(tbl); + SetCVar(CVars.CfgRollbackData, str); + } + + public void ApplyRollback() + { + var rollbackValue = GetCVar(CVars.CfgRollbackData); + + if (string.IsNullOrWhiteSpace(rollbackValue)) + return; + + _sawmill.Debug("We have CVars to roll back!"); + + try + { + var tblRoot = Toml.ReadString(rollbackValue); + var loaded = LoadFromTomlTable(tblRoot); + _sawmill.Info($"Rolled back CVars: {string.Join(", ", loaded)}"); + } + catch (Exception e) + { + _sawmill.Error($"Failed to load rollback data:\n{e}"); + } + finally + { + SetCVar(CVars.CfgRollbackData, ""); + SaveToFile(); + } + } + + private Dictionary? LoadPendingRollbackTable() + { + var rollbackValue = GetCVar(CVars.CfgRollbackData); + + if (string.IsNullOrWhiteSpace(rollbackValue)) + return null; + + try + { + var tblRoot = Toml.ReadString(rollbackValue); + return ParseCVarValuesFromToml(tblRoot).ToDictionary(); + } + catch (Exception e) + { + _sawmill.Error($"Failed to load rollback data:\n{e}"); + return null; + } + } +} diff --git a/Robust.Shared/Configuration/ConfigurationManager.cs b/Robust.Shared/Configuration/ConfigurationManager.cs index b4324fd0b..376787f50 100644 --- a/Robust.Shared/Configuration/ConfigurationManager.cs +++ b/Robust.Shared/Configuration/ConfigurationManager.cs @@ -19,7 +19,7 @@ namespace Robust.Shared.Configuration /// Stores and manages global configuration variables. /// [Virtual] - internal class ConfigurationManager : IConfigurationManagerInternal + internal partial class ConfigurationManager : IConfigurationManagerInternal { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ILogManager _logManager = default!; @@ -60,36 +60,53 @@ namespace Robust.Shared.Configuration /// public HashSet LoadFromTomlStream(Stream file) { - var loaded = new HashSet(); + TomlTable tblRoot; try { - var callbackEvents = new ValueList(); - - // Ensure callbacks are raised OUTSIDE the write lock. - using (Lock.WriteGuard()) - { - foreach (var (cvar, value) in ParseCVarValuesFromToml(file)) - { - loaded.Add(cvar); - LoadTomlVar(cvar, value, ref callbackEvents); - } - } - - foreach (var callback in callbackEvents) - { - InvokeValueChanged(callback); - } + tblRoot = Toml.ReadStream(file); } catch (Exception e) { - loaded.Clear(); - _sawmill.Error("Unable to load configuration from stream:\n{0}", e); + _sawmill.Error("Unable to load configuration from table:\n{0}", e); + return []; + } + + return LoadFromTomlTable(tblRoot); + } + + private HashSet LoadFromTomlTable(TomlTable table) + { + var loaded = new HashSet(); + var callbackEvents = new ValueList(); + try + { + // Ensure callbacks are raised OUTSIDE the write lock. + using (Lock.WriteGuard()) + { + foreach (var (cvar, value) in ParseCVarValuesFromToml(table)) + { + loaded.Add(cvar); + LoadParsedVar(cvar, value, ref callbackEvents); + } + } + } + finally + { + RunDeferredInvokeCallbacks(in callbackEvents); } return loaded; } - private void LoadTomlVar( + private void RunDeferredInvokeCallbacks(in ValueList callbackEvents) + { + foreach (var callback in callbackEvents) + { + InvokeValueChanged(callback); + } + } + + private void LoadParsedVar( string cvar, object value, ref ValueList changedInvokes) @@ -108,7 +125,7 @@ namespace Robust.Shared.Configuration } catch { - _sawmill.Error($"TOML parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Toml type: {value.GetType()}"); + _sawmill.Error($"Parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Parsed type: {value.GetType()}"); return; } } @@ -119,16 +136,24 @@ namespace Robust.Shared.Configuration else { //or add another unregistered CVar - //Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered. - cfgVar = new ConfigVar(cvar, null!, CVar.NONE) { Value = value }; - _configVars.Add(cvar, cfgVar); + cfgVar = AddUnregisteredCVar(cvar, value); } cfgVar.ConfigModified = true; } + private ConfigVar AddUnregisteredCVar(string name, object value) + { + //Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered. + var cfgVar = new ConfigVar(name, null!, CVar.NONE) { Value = value }; + _configVars.Add(name, cfgVar); + return cfgVar; + } + public HashSet LoadDefaultsFromTomlStream(Stream stream) { + var tblRoot = Toml.ReadStream(stream); + var loaded = new HashSet(); var callbackEvents = new ValueList(); @@ -136,7 +161,7 @@ namespace Robust.Shared.Configuration // Ensure callbacks are raised OUTSIDE the write lock. using (Lock.WriteGuard()) { - foreach (var (cVarName, value) in ParseCVarValuesFromToml(stream)) + foreach (var (cVarName, value) in ParseCVarValuesFromToml(tblRoot)) { if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered) { @@ -181,9 +206,13 @@ namespace Robust.Shared.Configuration { try { - using var file = File.OpenRead(configFile); - var result = LoadFromTomlStream(file); + HashSet result; + using (var file = File.OpenRead(configFile)) + { + result = LoadFromTomlStream(file); + } SetSaveFile(configFile); + ApplyRollback(); _sawmill.Info($"Configuration loaded from file"); return result; } @@ -223,6 +252,13 @@ namespace Robust.Shared.Configuration /// public void SaveToTomlStream(Stream stream, IEnumerable cvars) + { + var table = SaveToTomlTable(cvars); + + Toml.WriteStream(table, stream); + } + + private TomlTable SaveToTomlTable(IEnumerable cvars, Func? overrideValue = null) { var tblRoot = Toml.Create(); @@ -233,10 +269,18 @@ namespace Robust.Shared.Configuration if (!_configVars.TryGetValue(name, out var cVar)) continue; - var value = cVar.Value; - if (value == null && cVar.Registered) + object? value; + if (overrideValue != null) { - value = cVar.DefaultValue; + value = overrideValue(name); + } + else + { + value = cVar.Value; + if (value == null && cVar.Registered) + { + value = cVar.DefaultValue; + } } if (value == null) @@ -263,6 +307,8 @@ namespace Robust.Shared.Configuration } //runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯ + // If you add a type here, add it to .Rollback.cs too!!! + // I can't share the code because of how the serialization layers work :( switch (value) { case Enum val: @@ -293,7 +339,7 @@ namespace Robust.Shared.Configuration } } - Toml.WriteStream(tblRoot, stream); + return tblRoot; } /// @@ -495,7 +541,7 @@ namespace Robust.Shared.Configuration public void LoadCVarsFromType(Type containingType) { - foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.Static)) + foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) { var fieldType = defField.FieldType; if (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(CVarDef<>)) @@ -754,10 +800,25 @@ namespace Robust.Shared.Configuration private void InvokeValueChanged(in ValueChangedInvoke invoke) { - OnCVarValueChanged?.Invoke(invoke.Info); + try + { + OnCVarValueChanged?.Invoke(invoke.Info); + } + catch (Exception e) + { + _sawmill.Error($"Error while running OnCVarValueChanged callback: {e}"); + } + foreach (var entry in invoke.Invoke.Entries) { - entry.Value!.Invoke(invoke.Value, in invoke.Info); + try + { + entry.Value!.Invoke(invoke.Value, in invoke.Info); + } + catch (Exception e) + { + _sawmill.Error($"Error while running OnValueChanged callback: {e}"); + } } } @@ -768,10 +829,8 @@ namespace Robust.Shared.Configuration return new ValueChangedInvoke(info, var.ValueChanged); } - private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(Stream stream) + private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(TomlTable tblRoot) { - var tblRoot = Toml.ReadStream(stream); - return ProcessTomlObject(tblRoot, ""); IEnumerable<(string cvar, object value)> ProcessTomlObject(TomlObject obj, string tablePath) diff --git a/Robust.Shared/Configuration/IConfigurationManager.cs b/Robust.Shared/Configuration/IConfigurationManager.cs index 3a6843bb3..13a130bf6 100644 --- a/Robust.Shared/Configuration/IConfigurationManager.cs +++ b/Robust.Shared/Configuration/IConfigurationManager.cs @@ -56,6 +56,7 @@ namespace Robust.Shared.Configuration /// so it is not recommended to modify CVars from other threads. /// /// + [NotContentImplementable] public interface IConfigurationManager { /// @@ -272,5 +273,113 @@ namespace Robust.Shared.Configuration where T : notnull; public event Action? OnCVarValueChanged; + + // + // Rollback + // + + /// + /// Snapshot a CVar to be rolled back later, even in the event of a client crash. + /// + /// + /// + /// This set of APIs is intended for settings menus that want to show the user a + /// "Do these settings look correct?" prompt with timeout, + /// so that settings can be rolled back even in the event of alt+F4 or client crash. + /// + /// + /// Rollback is applied on call or client restart, + /// unless CVars are unmarked again via + /// + /// + /// Rollback is tracked in the config file too, and this command does not save it automatically. Of course, + /// not saving the config file before client exit also effectively rolls back CVars. + /// + /// + /// Calling this method if a CVar is already marked for rollback will simply update the snapshot value. + /// + /// + /// The CVars to roll back. + /// + void MarkForRollback(params CVarDef[] cVars); + + /// + /// Snapshot a CVar to be rolled back later, even in the event of a client crash. + /// + /// + /// + /// This set of APIs is intended for settings menus that want to show the user a + /// "Do these settings look correct?" prompt with timeout, + /// so that settings can be rolled back even in the event of alt+F4 or client crash. + /// + /// + /// Rollback is applied on call or client restart, + /// unless CVars are unmarked again via + /// + /// + /// Rollback is tracked in the config file too, and this command does not save it automatically. Of course, + /// not saving the config file before client exit also effectively rolls back CVars. + /// + /// + /// Calling this method if a CVar is already marked for rollback will simply update the snapshot value. + /// + /// + /// The CVar names to snapshot and (possibly) roll back later. + /// + void MarkForRollback(params string[] cVars); + + /// + /// Unmark a CVar for rollback. + /// + /// + /// + /// This set of APIs is intended for settings menus that want to show the user a + /// "Do these settings look correct?" prompt with timeout, + /// so that settings can be rolled back even in the event of alt+F4 or client crash. + /// + /// + /// Rollback is tracked in the config file too, and this command does not save it automatically. + /// Users must still call manually to avoid rollback happening. + /// + /// + /// The CVars to unmark for rollback. + /// + /// + void UnmarkForRollback(params CVarDef[] cVars); + + /// + /// Unmark a CVar for rollback. + /// + /// + /// + /// This set of APIs is intended for settings menus that want to show the user a + /// "Do these settings look correct?" prompt with timeout, + /// so that settings can be rolled back even in the event of alt+F4 or client crash. + /// + /// + /// Rollback is tracked in the config file too, and this command does not save it automatically. + /// Users must still call manually to avoid rollback happening. + /// + /// + /// The CVars to unmark for rollback. + /// + /// + void UnmarkForRollback(params string[] cVars); + + /// + /// Apply all pending CVar rollbacks. + /// + /// + /// This set of APIs is intended for settings menus that want to show the user a + /// "Do these settings look correct?" prompt with timeout, + /// so that settings can be rolled back even in the event of alt+F4 or client crash. + /// + /// + /// This implicitly saves the config file to ensure the config file does not contain + /// rollback data for longer than necessary. + /// + /// + /// + void ApplyRollback(); } } diff --git a/Robust.Shared/Configuration/NetConfigurationManager.cs b/Robust.Shared/Configuration/NetConfigurationManager.cs index e4c73db62..f13c0e714 100644 --- a/Robust.Shared/Configuration/NetConfigurationManager.cs +++ b/Robust.Shared/Configuration/NetConfigurationManager.cs @@ -14,6 +14,7 @@ namespace Robust.Shared.Configuration /// A networked configuration manager that controls the replication of /// console variables between client and server. /// + [NotContentImplementable] public interface INetConfigurationManager : IConfigurationManager { /// diff --git a/Robust.Shared/Console/IConsoleCommand.cs b/Robust.Shared/Console/IConsoleCommand.cs index 1ab9785bd..b59d22072 100644 --- a/Robust.Shared/Console/IConsoleCommand.cs +++ b/Robust.Shared/Console/IConsoleCommand.cs @@ -1,13 +1,17 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Robust.Shared.Toolshed; namespace Robust.Shared.Console { /// - /// Basic interface to handle console commands. Any class implementing this will be - /// registered with the console system through reflection. + /// Basic interface to handle console commands. Any class implementing this will be + /// registered with the console system through reflection. /// + /// + /// For server commands, it is much preferred to use . + /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] public interface IConsoleCommand { diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index de28453aa..66b864192 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -31,6 +31,7 @@ namespace Robust.Shared.Console /// The console host exists as a singleton subsystem that provides all of the features of the console API. /// It will register console commands, spawn console shells and execute command strings. /// + [NotContentImplementable] public interface IConsoleHost { /// diff --git a/Robust.Shared/Console/IConsoleShell.cs b/Robust.Shared/Console/IConsoleShell.cs index d453fd1ef..3845ec3d8 100644 --- a/Robust.Shared/Console/IConsoleShell.cs +++ b/Robust.Shared/Console/IConsoleShell.cs @@ -7,6 +7,7 @@ namespace Robust.Shared.Console /// The console shell that executes commands. Each shell executes commands in the context of a player /// session, or without a session in a local context. /// + [NotContentImplementable] public interface IConsoleShell { /// diff --git a/Robust.Shared/Console/LocalizedCommands.cs b/Robust.Shared/Console/LocalizedCommands.cs index 8a74ec316..c2a981506 100644 --- a/Robust.Shared/Console/LocalizedCommands.cs +++ b/Robust.Shared/Console/LocalizedCommands.cs @@ -3,9 +3,16 @@ using System.Threading.Tasks; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Toolshed; namespace Robust.Shared.Console; +/// +/// A variant on that has some built-in default localization strings. +/// +/// +/// For server commands, it is much preferred to use . +/// public abstract class LocalizedCommands : IConsoleCommand { [Dependency] protected readonly ILocalizationManager LocalizationManager = default!; diff --git a/Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs b/Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs index 82510c9c1..8aadd7a01 100644 --- a/Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs +++ b/Robust.Shared/ContentPack/AssemblyTypeChecker.Dump.cs @@ -11,7 +11,7 @@ namespace Robust.Shared.ContentPack { internal sealed partial class AssemblyTypeChecker { - public static IEnumerable DumpMetaMembers(Type type) + public static IEnumerable<(string Value, bool IsField)> DumpMetaMembers(Type type) { var assemblyLoc = type.Assembly.Location; @@ -58,7 +58,7 @@ namespace Robust.Shared.ContentPack var fieldName = metaReader.GetString(fieldDef.Name); var fieldType = fieldDef.DecodeSignature(provider, 0); - yield return $"{fieldType.WhitelistToString()} {fieldName}"; + yield return ($"{fieldType.WhitelistToString()} {fieldName}", IsField: true); } foreach (var methodHandle in typeDef.GetMethods()) @@ -79,7 +79,7 @@ namespace Robust.Shared.ContentPack ? "" : $"<{new string(',', genericCount - 1)}>"; - yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})"; + yield return ($"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})", IsField: false); } } } diff --git a/Robust.Shared/ContentPack/IModLoader.cs b/Robust.Shared/ContentPack/IModLoader.cs index 69bd52f8c..1d4e64b84 100644 --- a/Robust.Shared/ContentPack/IModLoader.cs +++ b/Robust.Shared/ContentPack/IModLoader.cs @@ -10,6 +10,7 @@ namespace Robust.Shared.ContentPack /// /// The mod loader is in charge of loading content assemblies and managing them. /// + [NotContentImplementable] public interface IModLoader { /// diff --git a/Robust.Shared/ContentPack/IResourceManager.cs b/Robust.Shared/ContentPack/IResourceManager.cs index 92c3a97f8..7bcafc2ac 100644 --- a/Robust.Shared/ContentPack/IResourceManager.cs +++ b/Robust.Shared/ContentPack/IResourceManager.cs @@ -11,6 +11,7 @@ namespace Robust.Shared.ContentPack /// /// Virtual file system for all disk resources. /// + [NotContentImplementable] public interface IResourceManager { /// diff --git a/Robust.Shared/ContentPack/IWritableDirProvider.cs b/Robust.Shared/ContentPack/IWritableDirProvider.cs index bdde84d93..96c907556 100644 --- a/Robust.Shared/ContentPack/IWritableDirProvider.cs +++ b/Robust.Shared/ContentPack/IWritableDirProvider.cs @@ -9,6 +9,7 @@ namespace Robust.Shared.ContentPack /// Provides an API for file and directory manipulation inside of a rooted folder. /// [PublicAPI] + [NotContentImplementable] public interface IWritableDirProvider { /// diff --git a/Robust.Shared/ContentPack/ResourceManifestData.cs b/Robust.Shared/ContentPack/ResourceManifestData.cs index 123026dc2..a58a10f45 100644 --- a/Robust.Shared/ContentPack/ResourceManifestData.cs +++ b/Robust.Shared/ContentPack/ResourceManifestData.cs @@ -11,12 +11,13 @@ internal sealed record ResourceManifestData( string? DefaultWindowTitle, string? WindowIconSet, string? SplashLogo, + bool? ShowLoadingBar, bool AutoConnect, string[]? ClientAssemblies ) { public static readonly ResourceManifestData Default = - new ResourceManifestData(Array.Empty(), null, null, null, null, true, null); + new ResourceManifestData(Array.Empty(), null, null, null, null, null, true, null); public static ResourceManifestData LoadResourceManifest(IResourceManager res) { @@ -58,6 +59,10 @@ internal sealed record ResourceManifestData( if (mapping.TryGetNode("splashLogo", out var splashNode)) splashLogo = splashNode.AsString(); + bool? showBar = null; + if (mapping.TryGetNode("show_loading_bar", out var showBarNode)) + showBar = showBarNode.AsBool(); + bool autoConnect = true; if (mapping.TryGetNode("autoConnect", out var autoConnectNode)) autoConnect = autoConnectNode.AsBool(); @@ -70,6 +75,7 @@ internal sealed record ResourceManifestData( defaultWindowTitle, windowIconSet, splashLogo, + showBar, autoConnect, clientAssemblies ); diff --git a/Robust.Shared/ContentPack/Sandbox.yml b/Robust.Shared/ContentPack/Sandbox.yml index a8292a957..c969a9d03 100644 --- a/Robust.Shared/ContentPack/Sandbox.yml +++ b/Robust.Shared/ContentPack/Sandbox.yml @@ -309,6 +309,32 @@ Types: SixLabors.ImageSharp.Formats: IImageEncoder: { All: True } PixelTypeInfo: { All: True } + SixLabors.ImageSharp.Processing: + ResizeExtensions: + Methods: + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, SixLabors.ImageSharp.Rectangle, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, bool)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Processing.ResizeOptions)" + ProcessingExtensions: + Methods: + - "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, System.Action`1)" + - "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, SixLabors.ImageSharp.Configuration, System.Action`1)" + - "SixLabors.ImageSharp.Image`1 Clone<>(SixLabors.ImageSharp.Image`1, System.Action`1)" + - "SixLabors.ImageSharp.Image`1 Clone<>(SixLabors.ImageSharp.Image`1, SixLabors.ImageSharp.Configuration, System.Action`1)" + - "SixLabors.ImageSharp.Image`1 Clone<>(SixLabors.ImageSharp.Image`1, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])" + - "SixLabors.ImageSharp.Image`1 Clone<>(SixLabors.ImageSharp.Image`1, SixLabors.ImageSharp.Configuration, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])" + CropExtensions: + Methods: + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)" + - "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Rectangle)" + IImageProcessingContext: { } SixLabors.ImageSharp.PixelFormats: A8: { All: True } Argb32: { All: True } @@ -386,6 +412,7 @@ Types: - "void SaveAsPng(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Png.PngEncoder)" - "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream)" - "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Tga.TgaEncoder)" + Rectangle: {All: True} Size: { All: True } SizeF: { All: True } System.Buffers: @@ -652,6 +679,8 @@ Types: MethodInfo: { } TypeAttributes: { } # Enum TypeInfo: { } + System.Reflection.Metadata: + MetadataUpdateHandlerAttribute: { All: True } System.Runtime.CompilerServices: AsyncStateMachineAttribute: { All: True } AsyncTaskMethodBuilder: { All: True } @@ -665,6 +694,7 @@ Types: CompilerGeneratedAttribute: { All: True } DefaultInterpolatedStringHandler: { All: True } ExtensionAttribute: { All: True } + ExtensionMarkerAttribute: { All: True } IAsyncStateMachine: { All: True } InternalsVisibleToAttribute: { All: True } InterpolatedStringHandlerAttribute: { All: True } @@ -860,13 +890,19 @@ Types: - "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object)" - "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object, object)" - "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object[])" + - "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1)" + - "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1)" - "System.Text.StringBuilder AppendJoin(char, object[])" - "System.Text.StringBuilder AppendJoin(char, string[])" + - "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1)" + - "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1)" - "System.Text.StringBuilder AppendJoin(string, object[])" - "System.Text.StringBuilder AppendJoin(string, string[])" - "System.Text.StringBuilder AppendJoin<>(char, System.Collections.Generic.IEnumerable`1)" - "System.Text.StringBuilder AppendJoin<>(string, System.Collections.Generic.IEnumerable`1)" - "System.Text.StringBuilder AppendLine()" + - "System.Text.StringBuilder AppendLine(System.IFormatProvider, ref System.Text.StringBuilder/AppendInterpolatedStringHandler)" + - "System.Text.StringBuilder AppendLine(ref System.Text.StringBuilder/AppendInterpolatedStringHandler)" - "System.Text.StringBuilder AppendLine(string)" - "System.Text.StringBuilder Clear()" - "System.Text.StringBuilder Insert(int, bool)" @@ -889,6 +925,8 @@ Types: - "System.Text.StringBuilder Insert(int, ulong)" - "System.Text.StringBuilder Insert(int, ushort)" - "System.Text.StringBuilder Remove(int, int)" + - "System.Text.StringBuilder Replace(System.ReadOnlySpan`1, System.ReadOnlySpan`1)" + - "System.Text.StringBuilder Replace(System.ReadOnlySpan`1, System.ReadOnlySpan`1, int, int)" - "System.Text.StringBuilder Replace(char, char)" - "System.Text.StringBuilder Replace(char, char, int, int)" - "System.Text.StringBuilder Replace(string, string)" diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index 92a1febb2..cd4bae33d 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -1187,7 +1187,7 @@ public sealed class EntityDeserializer : msg = CurrentReadingEntity is not { } curr ? $"Encountered invalid EntityUid reference" - : $"Encountered invalid EntityUid reference wile reading entity {curr.YamlId}, component: {CurrentComponent}"; + : $"Encountered invalid EntityUid reference while reading entity {curr.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return EntityUid.Invalid; } @@ -1197,7 +1197,7 @@ public sealed class EntityDeserializer : msg = CurrentReadingEntity is not { } ent ? "Encountered unknown entity yaml uid" - : $"Encountered unknown entity yaml uid wile reading entity {ent.YamlId}, component: {CurrentComponent}"; + : $"Encountered unknown entity yaml uid while reading entity {ent.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return EntityUid.Invalid; } @@ -1274,7 +1274,7 @@ public sealed class EntityDeserializer : var msg = CurrentReadingEntity is not { } ent ? "Encountered unknown yaml map id" - : $"Encountered unknown yaml map id wile reading entity {ent.YamlId}, component: {CurrentComponent}"; + : $"Encountered unknown yaml map id while reading entity {ent.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return MapId.Nullspace; } diff --git a/Robust.Shared/EntitySerialization/EntitySerializer.cs b/Robust.Shared/EntitySerialization/EntitySerializer.cs index 36184f4f2..635e33e87 100644 --- a/Robust.Shared/EntitySerialization/EntitySerializer.cs +++ b/Robust.Shared/EntitySerialization/EntitySerializer.cs @@ -62,6 +62,7 @@ public sealed class EntitySerializer : ISerializationContext, private readonly ISawmill _log; public readonly Dictionary YamlUidMap = new(); public readonly HashSet YamlIds = new(); + public readonly ValueDataNode InvalidNode = new("invalid"); public string? CurrentComponent { get; private set; } public Entity? CurrentEntity { get; private set; } @@ -222,6 +223,7 @@ public sealed class EntitySerializer : ISerializationContext, /// setting of it may auto-include additional entities /// aside from the one provided. /// + /// The set of entities to serialize public void SerializeEntities(HashSet entities) { foreach (var uid in entities) @@ -329,7 +331,12 @@ public sealed class EntitySerializer : ISerializationContext, return true; } - // iterate over all of its children and grab the first grid with a mapping + map = null; + + // if this is a map, iterate over all of its children and grab the first grid with a mapping + if (!_mapQuery.HasComponent(root)) + return false; + var xform = _xformQuery.GetComponent(root); foreach (var child in xform._children) { @@ -339,7 +346,6 @@ public sealed class EntitySerializer : ISerializationContext, return true; } - map = null; return false; } @@ -979,7 +985,7 @@ public sealed class EntitySerializer : ISerializationContext, if (CurrentComponent == _xformName) { if (value == EntityUid.Invalid) - return new ValueDataNode("invalid"); + return InvalidNode; DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid)); Orphans.Add(CurrentEntityYamlUid); @@ -987,13 +993,13 @@ public sealed class EntitySerializer : ISerializationContext, if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate && !ErroringEntities.Contains(value)) _log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}"); - return new ValueDataNode("invalid"); + return InvalidNode; } if (ErroringEntities.Contains(value)) { // Referenced entity already logged an error, so we just silently fail. - return new ValueDataNode("invalid"); + return InvalidNode; } if (value == EntityUid.Invalid) @@ -1001,7 +1007,7 @@ public sealed class EntitySerializer : ISerializationContext, if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore) _log.Error($"Encountered an invalid entityUid reference."); - return new ValueDataNode("invalid"); + return InvalidNode; } if (value == Truncate) @@ -1016,9 +1022,9 @@ public sealed class EntitySerializer : ISerializationContext, _log.Error(EntMan.Deleted(value) ? $"Encountered a reference to a deleted entity {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}." : $"Encountered a reference to a missing entity: {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}."); - return new ValueDataNode("invalid"); + return InvalidNode; case MissingEntityBehaviour.Ignore: - return new ValueDataNode("invalid"); + return InvalidNode; case MissingEntityBehaviour.IncludeNullspace: if (!EntMan.TryGetComponent(value, out TransformComponent? xform) || xform.ParentUid != EntityUid.Invalid diff --git a/Robust.Shared/EntitySerialization/SerializationEnums.cs b/Robust.Shared/EntitySerialization/SerializationEnums.cs index 1e296feb1..bfc6ae1c7 100644 --- a/Robust.Shared/EntitySerialization/SerializationEnums.cs +++ b/Robust.Shared/EntitySerialization/SerializationEnums.cs @@ -87,7 +87,6 @@ public enum MissingEntityBehaviour AutoInclude, } - public enum EntityExceptionBehaviour { /// diff --git a/Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.Save.cs b/Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.Save.cs index 0afb8202c..c8ae5e7b7 100644 --- a/Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.Save.cs +++ b/Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.Save.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -18,6 +19,10 @@ public sealed partial class MapLoaderSystem /// /// Recursively serialize the given entities and all of their children. /// + /// + /// This method is not optimized for being given a large set of entities. I.e., this should be a small handful of + /// maps or grids, not something like . + /// public (MappingDataNode Node, FileCategory Category) SerializeEntitiesRecursive( HashSet entities, SerializationOptions? options = null) @@ -29,8 +34,6 @@ public sealed partial class MapLoaderSystem Log.Info($"Serializing entities: {string.Join(", ", entities.Select(x => ToPrettyString(x).ToString()))}"); var maps = entities.Select(x => Transform(x).MapID).ToHashSet(); - var ev = new BeforeSerializationEvent(entities, maps); - RaiseLocalEvent(ev); // In case no options were provided, we assume that if all of the starting entities are pre-init, we should // expect that **all** entities that get serialized should be pre-init. @@ -39,6 +42,9 @@ public sealed partial class MapLoaderSystem ExpectPreInit = (entities.All(x => LifeStage(x) < EntityLifeStage.MapInitialized)) }; + var ev = new BeforeSerializationEvent(entities, maps, opts.Category); + RaiseLocalEvent(ev); + var serializer = new EntitySerializer(_dependency, opts); serializer.OnIsSerializeable += OnIsSerializable; serializer.SerializeEntityRecursive(entities); @@ -230,4 +236,89 @@ public sealed partial class MapLoaderSystem Write(path, data); return true; } + + /// + public bool TrySaveAllEntities(ResPath path, SerializationOptions? options = null) + { + if (!TrySerializeAllEntities(out var data, options)) + return false; + + Write(path, data); + return true; + } + + /// + /// Attempt to serialize all entities. + /// + /// + /// Note that this alone is not sufficient for a proper full-game save, as the game may contain things like chat + /// logs or resources and prototypes that were uploaded mid-game. + /// + public bool TrySerializeAllEntities([NotNullWhen(true)] out MappingDataNode? data, SerializationOptions? options = null) + { + data = null; + var opts = options ?? SerializationOptions.Default with + { + MissingEntityBehaviour = MissingEntityBehaviour.Error + }; + + opts.Category = FileCategory.Save; + _stopwatch.Restart(); + Log.Info($"Serializing all entities"); + + var entities = EntityManager.GetEntities().ToHashSet(); + var maps = _mapSystem.Maps.Keys.ToHashSet(); + var ev = new BeforeSerializationEvent(entities, maps, FileCategory.Save); + var serializer = new EntitySerializer(_dependency, opts); + + // Remove any non-serializable entities and their children (prevent error spam) + var toRemove = new Queue(); + foreach (var entity in entities) + { + // TODO SERIALIZATION Perf + // IsSerializable gets called again by serializer.SerializeEntities() + if (!serializer.IsSerializable(entity)) + toRemove.Enqueue(entity); + } + + if (toRemove.Count > 0) + { + if (opts.MissingEntityBehaviour == MissingEntityBehaviour.Error) + { + // The save will probably contain references to the non-serializable entities, and we avoid spamming errors. + opts.MissingEntityBehaviour = MissingEntityBehaviour.Ignore; + Log.Error($"Attempted to serialize one or more non-serializable entities"); + } + + while (toRemove.TryDequeue(out var next)) + { + entities.Remove(next); + foreach (var uid in Transform(next)._children) + { + toRemove.Enqueue(uid); + } + } + } + + try + { + RaiseLocalEvent(ev); + serializer.OnIsSerializeable += OnIsSerializable; + serializer.SerializeEntities(entities); + data = serializer.Write(); + var cat = serializer.GetCategory(); + DebugTools.AssertEqual(cat, FileCategory.Save); + var ev2 = new AfterSerializationEvent(entities, data, cat); + RaiseLocalEvent(ev2); + + Log.Debug($"Serialized {serializer.EntityData.Count} entities in {_stopwatch.Elapsed}"); + } + catch (Exception e) + { + Log.Error($"Caught exception while trying to serialize all entities:\n{e}"); + return false; + } + + return true; + } } diff --git a/Robust.Shared/Exceptions/RuntimeLog.cs b/Robust.Shared/Exceptions/RuntimeLog.cs index efc885792..9213e68d1 100644 --- a/Robust.Shared/Exceptions/RuntimeLog.cs +++ b/Robust.Shared/Exceptions/RuntimeLog.cs @@ -95,6 +95,7 @@ namespace Robust.Shared.Exceptions /// The term "runtime" dates back to BYOND, in which an exception is called a "runtime error". /// As such, what we call exceptions is called a "runtime" in BYOND. /// + [NotContentImplementable] public interface IRuntimeLog { int ExceptionCount { get; } diff --git a/Robust.Shared/GameObjects/CompIdx.cs b/Robust.Shared/GameObjects/CompIdx.cs index 136f45bb2..ab7d537c9 100644 --- a/Robust.Shared/GameObjects/CompIdx.cs +++ b/Robust.Shared/GameObjects/CompIdx.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading; using Robust.Shared.Maths; @@ -33,7 +34,7 @@ public readonly struct CompIdx : IEquatable var curLength = array.Length; if (curLength <= idx.Value) { - var newLength = MathHelper.NextPowerOfTwo(Math.Max(8, idx.Value)); + var newLength = MathHelper.NextPowerOfTwo(Math.Max(8, idx.Value + 1)); Array.Resize(ref array, newLength); } diff --git a/Robust.Shared/GameObjects/Component.cs b/Robust.Shared/GameObjects/Component.cs index cddbb8cea..e2679c779 100644 --- a/Robust.Shared/GameObjects/Component.cs +++ b/Robust.Shared/GameObjects/Component.cs @@ -1,4 +1,5 @@ using System; +using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager.Attributes; @@ -7,7 +8,24 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects { - /// + /// + /// The base class for all ECS components. A component is a piece of data, ideally without methods, that is + /// attached to or will be attached to some . Entities do not have any data without + /// components to specify it. + /// + /// + /// + /// Components must be registered, usually with , for the ECS to + /// recognize them. + /// + /// + /// Components implicitly have , and are always valid for YAML ser/de. + /// + /// + /// + /// + /// + /// [Reflect(false)] [ImplicitDataDefinitionForInheritors] public abstract partial class Component : IComponent diff --git a/Robust.Shared/GameObjects/ComponentAttributes.cs b/Robust.Shared/GameObjects/ComponentAttributes.cs index 7a538ec0b..e9a92e56e 100644 --- a/Robust.Shared/GameObjects/ComponentAttributes.cs +++ b/Robust.Shared/GameObjects/ComponentAttributes.cs @@ -6,14 +6,16 @@ namespace Robust.Shared.GameObjects; /// /// Marks a component as being automatically registered by /// +/// [AttributeUsage(AttributeTargets.Class, Inherited = false)] [BaseTypeRequired(typeof(IComponent))] [MeansImplicitUse] public sealed class RegisterComponentAttribute : Attribute; /// -/// Defines Name that this component is represented with in prototypes. +/// Defines Name that this component is represented with in prototypes. /// +/// [AttributeUsage(AttributeTargets.Class)] public sealed class ComponentProtoNameAttribute(string prototypeName) : Attribute { @@ -21,8 +23,9 @@ public sealed class ComponentProtoNameAttribute(string prototypeName) : Attribut } /// -/// Marks a component as not being saved when saving maps/grids. +/// Marks a component as not being saved when saving maps/grids. This is useful for purely runtime information. /// /// +/// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class UnsavedComponentAttribute : Attribute; diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index 71ddcbacd..20bedc1d2 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -582,6 +582,10 @@ namespace Robust.Shared.GameObjects } } + /// + /// Exception fired whenever a component not recognized by the engine is encountered. + /// This is usually caused by forgetting . + /// [Serializable] public sealed class UnknownComponentException : Exception { @@ -596,10 +600,17 @@ namespace Robust.Shared.GameObjects } } + /// + /// Exception fired if you try to register a new component after all registrations have been locked. + /// This is, typically, after network IDs have been assigned. + /// public sealed class ComponentRegistrationLockException : Exception { } + /// + /// Exception fired when a component's name is entirely invalid. All component type names must end with Component. + /// public sealed class InvalidComponentNameException : Exception { public InvalidComponentNameException(string message) : base(message) diff --git a/Robust.Shared/GameObjects/ComponentState.cs b/Robust.Shared/GameObjects/ComponentState.cs index ee9e57403..4f9dd051c 100644 --- a/Robust.Shared/GameObjects/ComponentState.cs +++ b/Robust.Shared/GameObjects/ComponentState.cs @@ -1,18 +1,37 @@ using System; +using Robust.Shared.GameStates; using Robust.Shared.Serialization; namespace Robust.Shared.GameObjects; +/// +/// An abstract base class for a component's network state. For simple cases, you can automatically generate this using +/// and . +/// +/// +/// +/// If your component's state is particularly complex, or you otherwise want manual control, you can implement this +/// directly and register necessary event handlers for and +/// . +/// +/// +/// How state is actually applied for a component, and what it looks like, is user defined. For an example, look at +/// . +/// +/// [RequiresSerializable] [Serializable, NetSerializable] [Virtual] public abstract class ComponentState : IComponentState; /// -/// Represents the state of a component for networking purposes. +/// Represents the state of a component for networking purposes. /// public interface IComponentState; +/// +/// Internal for RT, you probably want . +/// public interface IComponentDeltaState : IComponentState { public void ApplyToFullState(IComponentState fullState); @@ -21,20 +40,20 @@ public interface IComponentDeltaState : IComponentState } /// -/// Interface for component states that only contain partial state data. The actual delta state class should be a -/// separate class from the full component states. +/// Interface for component states that only contain partial state data. The actual delta state class should be a +/// separate class from the full component states. /// /// The full-state class associated with this partial state public interface IComponentDeltaState : IComponentDeltaState where TState: IComponentState { /// - /// This function will apply the current delta state to the provided full state, modifying it in the process. + /// This function will apply the current delta state to the provided full state, modifying it in the process. /// public void ApplyToFullState(TState fullState); /// - /// This function should take in a full state and return a new full state with the current delta applied, WITHOUT - /// modifying the original input state. + /// This function should take in a full state and return a new full state with the current delta applied, + /// WITHOUT modifying the original input state. /// public TState CreateNewFullState(TState fullState); diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index 2dc1b6bb8..e73ab638b 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -17,8 +17,11 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects { /// - /// Stores the position and orientation of the entity. + /// Stores the relative and global position and orientation of the entity.
+ /// This also tracks the overall transform hierarchy, which allows entities to be children of other entities + /// and move when their parent moves cheaply. ///
+ /// [RegisterComponent, NetworkedComponent] public sealed partial class TransformComponent : Component, IComponentDebug { diff --git a/Robust.Shared/GameObjects/Docs.xml b/Robust.Shared/GameObjects/Docs.xml new file mode 100644 index 000000000..6eaf60eb8 --- /dev/null +++ b/Robust.Shared/GameObjects/Docs.xml @@ -0,0 +1,14 @@ + + + + Attempts to look up on the given entity, writing it into the given space + if it finds it and the space was not already empty. + + + This is preferable to + if it + is erroneous for the component to not be present, but you don't want to crash the game.
+ This is also preferable if you may have already looked up the component, saving on lookup time. +
+
+
diff --git a/Robust.Shared/GameObjects/Entity.cs b/Robust.Shared/GameObjects/Entity.cs index 619e3f0f9..aeefa47db 100644 --- a/Robust.Shared/GameObjects/Entity.cs +++ b/Robust.Shared/GameObjects/Entity.cs @@ -5,6 +5,16 @@ using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; +/// +/// An with an associated component (or components) looked up in advance. +/// This is used by APIs to strongly type them over a required component, and can easily be obtained for an +/// EntityUid using , +/// , +/// and other methods. +/// +/// +/// This type exists for up to eight (i.e. Entity<T1, T2, T3, T4, T5, T6, T7, T8>) parameters. +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T : IComponent? @@ -53,6 +63,7 @@ public record struct Entity : IFluentEntityUid, IAsType public readonly EntityUid AsType() => Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? @@ -124,6 +135,7 @@ public record struct Entity : IFluentEntityUid, IAsType public readonly EntityUid AsType() => Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? @@ -231,6 +243,7 @@ public record struct Entity : IFluentEntityUid, IAsType public readonly EntityUid AsType() => Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? @@ -362,6 +375,7 @@ public record struct Entity : IFluentEntityUid, IAsType Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? @@ -517,6 +531,7 @@ public record struct Entity : IFluentEntityUid, IAsType Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? @@ -696,6 +711,7 @@ public record struct Entity : IFluentEntityUid, IAsType< public readonly EntityUid AsType() => Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? @@ -899,6 +915,7 @@ public record struct Entity : IFluentEntityUid, IAsT public readonly EntityUid AsType() => Owner; } +/// [NotYamlSerializable] public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? where T8 : IComponent? diff --git a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs index 60bff3afc..53661d3f5 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs @@ -11,6 +11,7 @@ namespace Robust.Shared.GameObjects /// EntitySystems communicate with each other. ///
[PublicAPI] + [NotContentImplementable] public interface IBroadcastEventBus { /// diff --git a/Robust.Shared/GameObjects/EntityEventBus.Common.cs b/Robust.Shared/GameObjects/EntityEventBus.Common.cs index 1d8c22859..b076d269b 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Common.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Common.cs @@ -12,7 +12,7 @@ namespace Robust.Shared.GameObjects; internal sealed partial class EntityEventBus : IEventBus { - private IEntityManager _entMan; + private EntityManager _entMan; private IComponentFactory _comFac; private IReflectionManager _reflection; @@ -34,18 +34,18 @@ internal sealed partial class EntityEventBus : IEventBus /// /// Array of component events and their handlers. The array is indexed by a component's /// , while the dictionary is indexed by the event type. This does not include events - /// with the + /// with the , unless is false. /// - internal FrozenDictionary[] _eventSubs = default!; + private FrozenDictionary[] _eventSubs = default!; /// - /// Variant of that also includes events with the + /// Variant of that only includes events with the /// - internal FrozenDictionary[] _compEventSubs = default!; + private FrozenDictionary[] _compEventSubs = default!; // pre-freeze event subscription data - internal Dictionary?[] _eventSubsUnfrozen = - Array.Empty>(); + private Dictionary?[] _eventSubsUnfrozen = []; + private Dictionary?[] _compEventSubsUnfrozen = []; /// /// Inverse of , mapping event types to sets of components. @@ -94,10 +94,6 @@ internal sealed partial class EntityEventBus : IEventBus /// private sealed class EventData { - /// - /// set? - /// - public bool ComponentEvent; public bool IsOrdered; public bool OrderingUpToDate; public ValueList BroadcastRegistrations; diff --git a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs index e20e72603..f921e6c91 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Robust.Shared.Collections; @@ -11,10 +11,12 @@ using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { + [NotContentImplementable] public interface IEventBus : IDirectedEventBus, IBroadcastEventBus { } + [NotContentImplementable] public interface IDirectedEventBus { void RaiseLocalEvent(EntityUid uid, TEvent args, bool broadcast = false) @@ -67,7 +69,8 @@ namespace Robust.Shared.GameObjects /// /// This has a very specific purpose, and has massive potential to be abused. /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal - /// is because of the component network source generator. + /// is because of the component network source generator.
+ /// This may be removed, modified, or pulled back internal at ANY TIME. ///
public void RaiseComponentEvent(EntityUid uid, TComponent component, TEvent args) where TEvent : notnull @@ -101,9 +104,6 @@ namespace Robust.Shared.GameObjects { internal delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref Unit args); - private delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref TEvent args) - where TEvent : notnull; - /// /// Max size of a components event subscription linked list. /// Used to limit the stackalloc in @@ -118,7 +118,7 @@ namespace Robust.Shared.GameObjects /// /// The entity manager to watch for entity/component events. /// The reflection manager to use when finding derived types. - public EntityEventBus(IEntityManager entMan, IReflectionManager reflection) + public EntityEventBus(EntityManager entMan, IReflectionManager reflection) { _entMan = entMan; _comFac = entMan.ComponentFactory; @@ -176,13 +176,8 @@ namespace Robust.Shared.GameObjects public void RaiseComponentEvent(EntityUid uid, IComponent component, CompIdx type, ref TEvent args) where TEvent : notnull { - ref var unitRef = ref Unsafe.As(ref args); - - DispatchComponent( - uid, - component, - type, - ref unitRef); + if (_compEventSubs[type.Value].TryGetValue(typeof(TEvent), out var handler)) + handler(uid, component, ref Unsafe.As(ref args)); } public void OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare() @@ -249,15 +244,13 @@ namespace Robust.Shared.GameObjects where TComp : IComponent where TEvent : notnull { - void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) - => handler(uid, (TComp)comp, args); + void EventHandler(EntityUid uid, IComponent comp, ref Unit ev) + { + ref var tev = ref Unsafe.As(ref ev); + handler(uid, (TComp) comp, tev); + } - EntSubscribe( - CompIdx.Index(), - typeof(TComp), - typeof(TEvent), - EventHandler, - null); + EntAddSubscription(CompIdx.Index(), typeof(TComp), typeof(TEvent), EventHandler); } public void SubscribeLocalEvent( @@ -268,71 +261,51 @@ namespace Robust.Shared.GameObjects where TComp : IComponent where TEvent : notnull { - void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) - => handler(uid, (TComp)comp, args); + void EventHandler(EntityUid uid, IComponent comp, ref Unit ev) + { + ref var tev = ref Unsafe.As(ref ev); + handler(uid, (TComp) comp, tev); + } - var orderData = CreateOrderingData(orderType, before, after); - - EntSubscribe( - CompIdx.Index(), - typeof(TComp), - typeof(TEvent), - EventHandler, - orderData); - - RegisterCommon(typeof(TEvent), orderData, out _); + EntAddSubscription(CompIdx.Index(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after); } public void SubscribeLocalEvent(ComponentEventRefHandler handler) where TComp : IComponent where TEvent : notnull { - void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) - => handler(uid, (TComp)comp, ref args); + void EventHandler(EntityUid uid, IComponent comp, ref Unit ev) + { + ref var tev = ref Unsafe.As(ref ev); + handler(uid, (TComp) comp, ref tev); + } - EntSubscribe( - CompIdx.Index(), - typeof(TComp), - typeof(TEvent), - EventHandler, - null); + EntAddSubscription(CompIdx.Index(), typeof(TComp), typeof(TEvent), EventHandler); } public void SubscribeLocalEvent(ComponentEventRefHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull { - void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) - => handler(uid, (TComp)comp, ref args); + void EventHandler(EntityUid uid, IComponent comp, ref Unit ev) + { + ref var tev = ref Unsafe.As(ref ev); + handler(uid, (TComp) comp, ref tev); + } - var orderData = CreateOrderingData(orderType, before, after); - - EntSubscribe( - CompIdx.Index(), - typeof(TComp), - typeof(TEvent), - EventHandler, - orderData); - - RegisterCommon(typeof(TEvent), orderData, out _); + EntAddSubscription(CompIdx.Index(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after); } public void SubscribeLocalEvent(EntityEventRefHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull { - void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) - => handler(new Entity(uid, (TComp) comp), ref args); + void EventHandler(EntityUid uid, IComponent comp, ref Unit ev) + { + ref var tev = ref Unsafe.As(ref ev); + handler(new Entity(uid, (TComp) comp), ref tev); + } - var orderData = CreateOrderingData(orderType, before, after); - - EntSubscribe( - CompIdx.Index(), - typeof(TComp), - typeof(TEvent), - EventHandler, - orderData); - - RegisterCommon(typeof(TEvent), orderData, out _); + EntAddSubscription(CompIdx.Index(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after); } /// @@ -340,7 +313,24 @@ namespace Robust.Shared.GameObjects where TComp : IComponent where TEvent : notnull { - EntUnsubscribe(CompIdx.Index(), typeof(TEvent)); + if (!_comFac.TryGetRegistration(typeof(TComp), out _)) + { + if (!IgnoreUnregisteredComponents) + throw new InvalidOperationException($"Component is not a valid reference type: {typeof(TComp).Name}"); + + return; + } + + if (_subscriptionLock) + throw new InvalidOperationException("Subscription locked."); + + var i = CompIdx.ArrayIndex(); + + _eventSubsUnfrozen[i]!.Remove(typeof(TEvent)); + _compEventSubsUnfrozen[i]!.Remove(typeof(TEvent)); + + if (_eventSubsInv.TryGetValue(typeof(TEvent), out var t)) + t.Remove(CompIdx.Index()); } private void ComFacOnComponentsAdded(ComponentRegistration[] regs) @@ -351,6 +341,7 @@ namespace Robust.Shared.GameObjects foreach (var reg in regs) { CompIdx.RefArray(ref _eventSubsUnfrozen, reg.Idx) ??= new(); + CompIdx.RefArray(ref _compEventSubsUnfrozen, reg.Idx) ??= new(); } } @@ -374,40 +365,17 @@ namespace Robust.Shared.GameObjects _subscriptionLock = true; _eventData = _eventDataUnfrozen.ToFrozenDictionary(); - // Find last non-null entry. - var last = 0; - for (var i = 0; i < _eventSubsUnfrozen.Length; i++) - { - var entry = _eventSubsUnfrozen[i]; - if (entry != null) - last = i; - } - - // TODO PERFORMANCE - // make this only contain events that actually use comp-events - // Assuming it makes the frozen dictionaries more specialized and thus faster. - // AFAIK currently only MapInit is both a comp-event and a general event. - // It should probably be changed to just be a comp event. - _compEventSubs = _eventSubsUnfrozen - .Take(last+1) + _eventSubs = TrimNull(_eventSubsUnfrozen) .Select(dict => dict?.ToFrozenDictionary()!) .ToArray(); - _eventSubs = _eventSubsUnfrozen - .Take(last+1) - .Select(dict => dict?.Where(x => !IsComponentEvent(x.Key)).ToFrozenDictionary()!) + _compEventSubs = TrimNull(_compEventSubsUnfrozen) + .Select(dict => dict?.ToFrozenDictionary()!) .ToArray(); CalcOrdering(); } - private bool IsComponentEvent(Type t) - { - var isCompEv = _eventData[t].ComponentEvent; - DebugTools.Assert(isCompEv == t.HasCustomAttribute()); - return isCompEv; - } - public void OnComponentRemoved(in RemovedComponentEventArgs e) { EntRemoveComponent(e.BaseArgs.Owner, e.Idx); @@ -417,13 +385,15 @@ namespace Robust.Shared.GameObjects CompIdx compType, Type compTypeObj, Type eventType, - DirectedRegistration registration) + DirectedEventHandler handler, + Type? orderType = null, + Type[]? before = null, + Type[]? after = null) { if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); - if (compType.Value >= _eventSubsUnfrozen.Length - || _eventSubsUnfrozen[compType.Value] is not { } compSubs) + if (!_comFac.TryGetRegistration(compTypeObj, out _)) { if (IgnoreUnregisteredComponents) return; @@ -431,53 +401,25 @@ namespace Robust.Shared.GameObjects throw new InvalidOperationException($"Component is not a valid reference type: {compTypeObj.Name}"); } - if (compSubs.ContainsKey(eventType)) + if (eventType.GetCustomAttribute() is { } attr) { - throw new InvalidOperationException( - $"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}"); - } + if (!_compEventSubsUnfrozen[compType.Value]!.TryAdd(eventType, handler)) + throw new InvalidOperationException($"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}"); - compSubs.Add(eventType, registration); - - RegisterCommon(eventType, registration.Ordering, out var data); - data.ComponentEvent = eventType.HasCustomAttribute(); - if (!data.ComponentEvent) - _eventSubsInv.GetOrNew(eventType).Add(compType); - } - - private void EntSubscribe( - CompIdx compType, - Type compTypeObj, - Type eventType, - DirectedEventHandler handler, - OrderingData? order) - where TEvent : notnull - { - EntAddSubscription(compType, compTypeObj, eventType, new DirectedRegistration(handler, order, - (EntityUid uid, IComponent comp, ref Unit ev) => - { - ref var tev = ref Unsafe.As(ref ev); - handler(uid, comp, ref tev); - })); - } - - private void EntUnsubscribe(CompIdx compType, Type eventType) - { - if (_subscriptionLock) - throw new InvalidOperationException("Subscription locked."); - - if (compType.Value >= _eventSubsUnfrozen.Length - || _eventSubsUnfrozen[compType.Value] is not { } compSubs) - { - if (IgnoreUnregisteredComponents) + // An exclusive component-event is only raised via RaiseComponentEvent, hence it don't need a normal + // directed event subscription + if (attr.Exclusive) return; - - throw new InvalidOperationException("Trying to unsubscribe from unregistered component!"); } - var removed = compSubs.Remove(eventType); - if (removed) - _eventSubsInv[eventType].Remove(compType); + var orderData = orderType == null ? null : CreateOrderingData(orderType, before, after); + var reg = new DirectedRegistration(orderData, handler); + + if (!_eventSubsUnfrozen[compType.Value]!.TryAdd(eventType, reg)) + throw new InvalidOperationException($"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}"); + + RegisterCommon(eventType, reg.Ordering, out _); + _eventSubsInv.GetOrNew(eventType).Add(compType); } private void EntAddEntity(EntityUid euid) @@ -501,8 +443,6 @@ namespace Robust.Shared.GameObjects foreach (var evType in compSubs.Keys) { - DebugTools.Assert(!_eventData[evType].ComponentEvent); - if (eventTable.Free < 0) GrowEventTable(eventTable); @@ -563,7 +503,6 @@ namespace Robust.Shared.GameObjects foreach (var evType in compSubs.Keys) { - DebugTools.Assert(!_eventData[evType].ComponentEvent); ref var indices = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType); if (Unsafe.IsNullRef(ref indices)) { @@ -667,17 +606,6 @@ namespace Robust.Shared.GameObjects } } - private void DispatchComponent( - EntityUid euid, - IComponent component, - CompIdx baseType, - ref Unit args) - where TEvent : notnull - { - if (_compEventSubs[baseType.Value].TryGetValue(typeof(TEvent), out var reg)) - reg.Handler(euid, component, ref args); - } - public void ClearSubscriptions() { _subscriptionLock = false; @@ -691,6 +619,10 @@ namespace Robust.Shared.GameObjects { sub?.Clear(); } + foreach (var sub in _compEventSubsUnfrozen) + { + sub?.Clear(); + } } public void Dispose() @@ -705,22 +637,14 @@ namespace Robust.Shared.GameObjects _compEventSubs = null!; _eventSubs = null!; _eventSubsUnfrozen = null!; + _compEventSubsUnfrozen = null!; _eventSubsInv = null!; } - internal sealed class DirectedRegistration : OrderedRegistration + internal sealed class DirectedRegistration(OrderingData? ordering, DirectedEventHandler handler) + : OrderedRegistration(ordering) { - public readonly Delegate Original; - public readonly DirectedEventHandler Handler; - - public DirectedRegistration( - Delegate original, - OrderingData? ordering, - DirectedEventHandler handler) : base(ordering) - { - Original = original; - Handler = handler; - } + public readonly DirectedEventHandler Handler = handler; public void SetOrder(int order) { @@ -753,6 +677,48 @@ namespace Robust.Shared.GameObjects public int Next; public CompIdx Component; } + + /// + /// Return a new array with any trailing null entries removed. + /// + public static T[] TrimNull(T[] input) + { + // Find last non-null entry. + var last = 0; + for (var i = 0; i < input.Length; i++) + { + var entry = input[i]; + if (entry != null) + last = i; + } + + return input[..(last + 1)]; + } + + /// + /// Get an array of event handlers for a given component event, indexed by the component's net-id. + /// + /// + /// For most events, this will generally be a pretty sparse array, with most entries being null. However, for + /// the get and handle state events, this array will be relatively dense and helps save PVS a lot of save a + /// FrozenDictionary lookups. + /// + internal DirectedEventHandler?[] GetNetCompEventHandlers() + { + DebugTools.Assert(_subscriptionLock); + DebugTools.Assert(typeof(TEvent).HasCustomAttribute()); + + var netComps = _comFac.NetworkedComponents!; + var result = new DirectedEventHandler?[netComps.Count]; + + for (var i = 0; i < netComps.Count; i++) + { + var reg = netComps[i]; + result[i] = _compEventSubs[reg.Idx.Value].GetValueOrDefault(typeof(TEvent)); + } + + return result; + } } /// diff --git a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs index b45c581e1..ae7c772bd 100644 --- a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs +++ b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs @@ -39,7 +39,7 @@ public abstract partial class EntityManager Dirty(uid, comp, metadata); } - public virtual void DirtyField(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null) + public virtual void DirtyField(EntityUid uid, T comp, [ValidateMember] string fieldName, MetaDataComponent? metadata = null) where T : IComponentDelta { var compReg = ComponentFactory.GetRegistration(CompIdx.Index()); diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index bb0dfe520..09dd5cdf5 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -21,7 +21,6 @@ using Robust.Shared.Exceptions; namespace Robust.Shared.GameObjects { - /// public partial class EntityManager { [IoC.Dependency] private readonly IComponentFactory _componentFactory = default!; @@ -406,7 +405,7 @@ namespace Robust.Shared.GameObjects var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg); ComponentAdded?.Invoke(eventArgs); - _eventBus.OnComponentAdded(eventArgs); + EventBusInternal.OnComponentAdded(eventArgs); LifeAddToEntity(uid, component, reg.Idx); @@ -427,7 +426,7 @@ namespace Robust.Shared.GameObjects LifeStartup(uid, component, reg.Idx); if (metadata.EntityLifeStage >= EntityLifeStage.MapInitialized) - EventBus.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance); + EventBusInternal.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance); } /// @@ -717,7 +716,7 @@ namespace Robust.Shared.GameObjects var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata, idx); ComponentRemoved?.Invoke(eventArgs); - _eventBus.OnComponentRemoved(eventArgs); + EventBusInternal.OnComponentRemoved(eventArgs); if (!terminating) { @@ -1181,14 +1180,14 @@ namespace Robust.Shared.GameObjects { var comps = _entTraitArray[CompIdx.ArrayIndex()]; DebugTools.Assert(comps != null, $"Unknown component: {typeof(TComp1).Name}"); - return new EntityQuery(comps, _resolveSawmill); + return new EntityQuery(this, comps); } public EntityQuery GetEntityQuery(Type type) { var comps = _entTraitDict[type]; DebugTools.Assert(comps != null, $"Unknown component: {type.Name}"); - return new EntityQuery(comps, _resolveSawmill); + return new EntityQuery(this, comps); } /// @@ -1224,7 +1223,7 @@ namespace Robust.Shared.GameObjects var set = _entCompIndex[uid]; if (set.Count > comps.Length) { - comps = new IComponent[set.Count]; + comps = new IComponent?[set.Count]; } var i = 0; @@ -1708,9 +1707,12 @@ namespace Robust.Shared.GameObjects } public bool CanGetComponentState(IEventBus eventBus, IComponent component, ICommonSession player) + => CanGetComponentState(component, player); + + public bool CanGetComponentState(IComponent component, ICommonSession player) { var attempt = new ComponentGetStateAttemptEvent(player); - eventBus.RaiseComponentEvent(component.Owner, component, ref attempt); + EventBusInternal.RaiseComponentEvent(component.Owner, component, ref attempt); return !attempt.Cancelled; } @@ -1753,17 +1755,61 @@ namespace Robust.Shared.GameObjects } } + /// + /// An index of all entities with a given component, avoiding looking up the component's storage every time. + /// Using these saves on dictionary lookups, making your code slightly more efficient, and ties in nicely with + /// . + /// + /// Any component type. + /// + /// + /// public sealed class MySystem : EntitySystem + /// { + /// private EntityQuery<TransformComponent> _transforms = default!; + ///
+ /// public override void Initialize() + /// { + /// _transforms = GetEntityQuery<TransformComponent>(); + /// } + ///
+ /// public void DoThings(EntityUid myEnt) + /// { + /// var ent = _transforms.Get(myEnt); + /// // ... + /// } + /// } + ///
+ ///
+ /// + /// Queries hold references to internals, and are always up to date with the world. + /// They can not however perform mutation, if you need to add or remove components you must use + /// or methods. + /// + /// EntitySystem.GetEntityQuery() + /// EntityManager.GetEntityQuery() public readonly struct EntityQuery where TComp1 : IComponent { + private readonly EntityManager _entMan; private readonly Dictionary _traitDict; - private readonly ISawmill _sawmill; - public EntityQuery(Dictionary traitDict, ISawmill sawmill) + internal EntityQuery(EntityManager entMan, Dictionary traitDict) { + _entMan = entMan; _traitDict = traitDict; - _sawmill = sawmill; } + /// + /// Gets for an entity, throwing if it can't find it. + /// + /// The entity to do a lookup for. + /// The located component. + /// Thrown if the entity does not have a component of type . + /// + /// IEntityManager.GetComponent<T>(EntityUid) + /// + /// + /// EntitySystem.Comp<T>(EntityUid) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1 GetComponent(EntityUid uid) @@ -1774,6 +1820,7 @@ namespace Robust.Shared.GameObjects throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); } + /// [MethodImpl(MethodImplOptions.AggressiveInlining), Pure] public Entity Get(EntityUid uid) { @@ -1783,6 +1830,22 @@ namespace Robust.Shared.GameObjects throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); } + /// + /// Gets for an entity, if it's present. + /// + /// + /// If it is strictly errorenous for a component to not be present, you may want to use + /// instead. + /// + /// The entity to do a lookup for. + /// The located component, if any. + /// Whether the component was found. + /// + /// IEntityManager.TryGetComponent<T>(EntityUid, out T?) + /// + /// + /// EntitySystem.TryComp<T>(EntityUid, out T?) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) @@ -1796,6 +1859,7 @@ namespace Robust.Shared.GameObjects return TryGetComponent(uid.Value, out component); } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? component) @@ -1810,24 +1874,40 @@ namespace Robust.Shared.GameObjects return false; } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryComp(EntityUid uid, [NotNullWhen(true)] out TComp1? component) => TryGetComponent(uid, out component); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) => TryGetComponent(uid, out component); + /// + /// Tests if the given entity has . + /// + /// The entity to do a lookup for. + /// Whether the component exists for that entity. + /// If you immediately need to then look up that component, it's more efficient to use . + /// + /// IEntityManager.HasComponent<T>(EntityUid) + /// + /// + /// EntitySystem.HasComp<T>(EntityUid) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool HasComp(EntityUid uid) => HasComponent(uid); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool HasComponent(EntityUid uid) @@ -1835,6 +1915,7 @@ namespace Robust.Shared.GameObjects return _traitDict.TryGetValue(uid, out var comp) && !comp.Deleted; } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool HasComponent([NotNullWhen(true)] EntityUid? uid) @@ -1842,6 +1923,14 @@ namespace Robust.Shared.GameObjects return uid != null && HasComponent(uid.Value); } + /// + /// The entity to do a lookup for. + /// The space to write the component into if found. + /// Whether to log if the component is missing, for diagnostics. + /// Whether the component was found. + /// + /// EntitySystem.Resolve<T>(EntityUid, out T?) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true) { @@ -1858,19 +1947,29 @@ namespace Robust.Shared.GameObjects } if (logMissing) - { - _sawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {uid}!\n{Environment.StackTrace}"); - } + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{Environment.StackTrace}"); return false; } + /// + /// The space to write the component into if found. + /// Whether to log if the component is missing, for diagnostics. + /// Whether the component was found. + /// + /// EntitySystem.Resolve<T>(EntityUid, out T?) + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Resolve(ref Entity entity, bool logMissing = true) { return Resolve(entity.Owner, ref entity.Comp, logMissing); } + /// + /// Gets for an entity if it's present, or null if it's not. + /// + /// The entity to do the lookup on. + /// The component, if it exists. [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1? CompOrNull(EntityUid uid) @@ -1881,6 +1980,7 @@ namespace Robust.Shared.GameObjects return default; } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1 Comp(EntityUid uid) @@ -1966,7 +2066,7 @@ namespace Robust.Shared.GameObjects } if (logMissing) - _sawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {uid}!\n{new StackTrace(1, true)}"); + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{new StackTrace(1, true)}"); return false; } @@ -2110,8 +2210,35 @@ namespace Robust.Shared.GameObjects #region Query /// - /// Returns all matching unpaused components. + /// Iterates all entities that have the given components, including the components themselves, but only if + /// the entity they're on is not Paused. /// + /// + /// + /// Internally, checks for what components exist are done in the order of the generics, so the least frequently + /// occurring component should always be the first argument, and so on. + /// + /// + /// This type exists for up to four components, TComp1 through TComp4, as generic arguments. + /// + /// + /// + /// + /// // within a system. + /// var enumerator = EntityQueryEnumerator<TransformComponent>(); + ///
+ /// while (enumerator.MoveNext(out var ent, out var xform)) + /// { + /// // ... do work with the entity we just found. + /// } + ///
+ ///
+ /// + /// EntitySystem.EntityQueryEnumerator<TComp1, ...>() + /// + /// + /// IEntityManager.EntityQueryEnumerator<TComp1, ...>() + /// public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent { @@ -2126,6 +2253,12 @@ namespace Robust.Shared.GameObjects _metaQuery = metaQuery; } + /// + /// Provides the next entity and component in the enumerator, if there are still more to iterate through. + /// + /// The found entity, if any. + /// A component on the found entity. + /// Whether the enumerator was empty (and as such no entity nor component were returned) public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1) { while (true) @@ -2167,9 +2300,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching unpaused components. - /// + /// public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2188,6 +2319,8 @@ namespace Robust.Shared.GameObjects _metaQuery = metaQuery; } + /// + /// A component on the found entity. public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2) { while (true) @@ -2236,9 +2369,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching unpaused components. - /// + /// public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2261,6 +2392,9 @@ namespace Robust.Shared.GameObjects _metaQuery = metaQuery; } + /// + /// A component on the found entity. + /// A component on the found entity. public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3) { while (true) @@ -2319,9 +2453,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching unpaused components. - /// + /// public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2348,6 +2480,10 @@ namespace Robust.Shared.GameObjects _metaQuery = metaQuery; } + /// + /// A component on the found entity. + /// A component on the found entity. + /// A component on the found entity. public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3, [NotNullWhen(true)] out TComp4? comp4) { while (true) @@ -2419,8 +2555,35 @@ namespace Robust.Shared.GameObjects #region All query /// - /// Returns all matching components, paused or not. + /// Iterates all entities that have the given components, including the components themselves, regardless + /// of if the entity is Paused. /// + /// + /// + /// Internally, checks for what components exist are done in the order of the generics, so the least frequently + /// occurring component should always be the first argument, and so on. + /// + /// + /// This type exists for up to four components, TComp1 through TComp4, as generic arguments. + /// + /// + /// + /// + /// // within a system. + /// var enumerator = AllEntityQueryEnumerator<TransformComponent>(); + ///
+ /// while (enumerator.MoveNext(out var ent, out var xform)) + /// { + /// // ... do work with the entity we just found. + /// } + ///
+ ///
+ /// + /// EntitySystem.AllEntityQuery<TComp1, ...>() + /// + /// + /// IEntityManager.AllEntityQueryEnumerator<TComp1, ...>() + /// public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent { @@ -2432,6 +2595,7 @@ namespace Robust.Shared.GameObjects _traitDict = traitDict.GetEnumerator(); } + /// public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1) { while (true) @@ -2468,9 +2632,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching components, paused or not. - /// + /// public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2486,6 +2648,7 @@ namespace Robust.Shared.GameObjects _traitDict2 = traitDict2; } + /// public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2) { while (true) @@ -2529,9 +2692,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching components, paused or not. - /// + /// public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2551,6 +2712,7 @@ namespace Robust.Shared.GameObjects _traitDict3 = traitDict3; } + /// public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3) { while (true) @@ -2604,9 +2766,7 @@ namespace Robust.Shared.GameObjects } } - /// - /// Returns all matching components, paused or not. - /// + /// public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2630,6 +2790,7 @@ namespace Robust.Shared.GameObjects _traitDict4 = traitDict4; } + /// public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3, [NotNullWhen(true)] out TComp4? comp4) { while (true) diff --git a/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs b/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs index 84ed16f03..f569636f0 100644 --- a/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs +++ b/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs @@ -24,7 +24,7 @@ public partial class EntityManager component.CreationTick = CurrentTick; // networked components are assumed to be dirty when added to entities. See also: ClearTicks() component.LastModifiedTick = CurrentTick; - EventBus.RaiseComponentEvent(uid, component, idx, CompAddInstance); + EventBusInternal.RaiseComponentEvent(uid, component, idx, CompAddInstance); component.LifeStage = ComponentLifeStage.Added; #pragma warning restore CS0618 // Type or member is obsolete } @@ -39,7 +39,7 @@ public partial class EntityManager DebugTools.Assert(component.LifeStage == ComponentLifeStage.Added); component.LifeStage = ComponentLifeStage.Initializing; - EventBus.RaiseComponentEvent(uid, component, idx, CompInitInstance); + EventBusInternal.RaiseComponentEvent(uid, component, idx, CompInitInstance); component.LifeStage = ComponentLifeStage.Initialized; } @@ -53,7 +53,7 @@ public partial class EntityManager DebugTools.Assert(component.LifeStage == ComponentLifeStage.Initialized); component.LifeStage = ComponentLifeStage.Starting; - EventBus.RaiseComponentEvent(uid, component, idx, CompStartupInstance); + EventBusInternal.RaiseComponentEvent(uid, component, idx, CompStartupInstance); component.LifeStage = ComponentLifeStage.Running; } @@ -76,7 +76,7 @@ public partial class EntityManager } component.LifeStage = ComponentLifeStage.Stopping; - EventBus.RaiseComponentEvent(uid, component, idx, CompShutdownInstance); + EventBusInternal.RaiseComponentEvent(uid, component, idx, CompShutdownInstance); component.LifeStage = ComponentLifeStage.Stopped; } @@ -90,7 +90,7 @@ public partial class EntityManager DebugTools.Assert(component.LifeStage != ComponentLifeStage.PreAdd); component.LifeStage = ComponentLifeStage.Removing; - EventBus.RaiseComponentEvent(uid, component, idx, CompRemoveInstance); + EventBusInternal.RaiseComponentEvent(uid, component, idx, CompRemoveInstance); component.LifeStage = ComponentLifeStage.Deleted; } diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 29f1cc9dc..349e648fc 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -81,14 +81,14 @@ namespace Robust.Shared.GameObjects ///
protected readonly HashSet Entities = new(); - private EntityEventBus _eventBus = null!; + internal EntityEventBus EventBusInternal = null!; protected int NextEntityUid = (int) EntityUid.FirstUid; protected int NextNetworkId = (int) NetEntity.First; /// - public IEventBus EventBus => _eventBus; + public IEventBus EventBus => EventBusInternal; public event Action>? EntityAdded; public event Action>? EntityInitialized; @@ -118,7 +118,7 @@ namespace Robust.Shared.GameObjects private SharedMapSystem _mapSystem = default!; private ISawmill _sawmill = default!; - private ISawmill _resolveSawmill = default!; + internal ISawmill ResolveSawmill = default!; public bool Started { get; protected set; } @@ -142,14 +142,14 @@ namespace Robust.Shared.GameObjects if (Initialized) throw new InvalidOperationException("Initialize() called multiple times"); - _eventBus = new EntityEventBus(this, _reflection); + EventBusInternal = new EntityEventBus(this, _reflection); InitializeComponents(); _metaReg = _componentFactory.GetRegistration(typeof(MetaDataComponent)); _xformReg = _componentFactory.GetRegistration(typeof(TransformComponent)); _xformName = _xformReg.Name; _sawmill = LogManager.GetSawmill("entity"); - _resolveSawmill = LogManager.GetSawmill("resolve"); + ResolveSawmill = LogManager.GetSawmill("resolve"); #if DEBUG _mainThreadId = Environment.CurrentManagedThreadId; @@ -210,7 +210,11 @@ namespace Robust.Shared.GameObjects catch (Exception e) { _sawmill.Error($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}"); +#if !EXCEPTION_TOLERANCE + throw; +#else return false; +#endif } if (compMapping.AnyExcept(protoMapping)) @@ -230,7 +234,7 @@ namespace Robust.Shared.GameObjects // TODO: Probably better to call this on its own given it's so infrequent. _entitySystemManager.Initialize(); Started = true; - _eventBus.LockSubscriptions(); + EventBusInternal.LockSubscriptions(); _mapSystem = System(); _xforms = System(); _containers = System(); @@ -245,7 +249,7 @@ namespace Robust.Shared.GameObjects { ShuttingDown = true; FlushEntities(); - _eventBus.ClearSubscriptions(); + EventBusInternal.ClearSubscriptions(); _entitySystemManager.Shutdown(); ClearComponents(); ShuttingDown = false; @@ -259,8 +263,8 @@ namespace Robust.Shared.GameObjects ShuttingDown = true; FlushEntities(); _entitySystemManager.Clear(); - _eventBus.Dispose(); - _eventBus = null!; + EventBusInternal.Dispose(); + EventBusInternal = null!; ClearComponents(); ShuttingDown = false; @@ -279,18 +283,13 @@ namespace Robust.Shared.GameObjects using (histogram?.WithLabels("EntityEventBus").NewTimer()) using (_prof.Group("Events")) { - _eventBus.ProcessEventQueue(); + EventBusInternal.ProcessEventQueue(); } using (histogram?.WithLabels("QueuedDeletion").NewTimer()) using (_prof.Group("QueueDel")) { - while (QueuedDeletions.TryDequeue(out var uid)) - { - DeleteEntity(uid); - } - - QueuedDeletionsSet.Clear(); + ProcessQueueudDeletions(); } using (histogram?.WithLabels("ComponentCull").NewTimer()) @@ -300,6 +299,16 @@ namespace Robust.Shared.GameObjects } } + internal virtual void ProcessQueueudDeletions() + { + while (QueuedDeletions.TryDequeue(out var uid)) + { + DeleteEntity(uid); + } + + QueuedDeletionsSet.Clear(); + } + public virtual void FrameUpdate(float frameTime) { _entitySystemManager.FrameUpdate(frameTime); @@ -596,11 +605,14 @@ namespace Robust.Shared.GameObjects { var ev = new EntityTerminatingEvent((uid, metadata)); BeforeEntityTerminating?.Invoke(ref ev); - EventBus.RaiseLocalEvent(uid, ref ev, true); + EventBusInternal.RaiseLocalEvent(uid, ref ev, true); } catch (Exception e) { _sawmill.Error($"Caught exception while raising event {nameof(EntityTerminatingEvent)} on entity {ToPrettyString(uid, metadata)}\n{e}"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } foreach (var child in xform._children) @@ -643,6 +655,9 @@ namespace Robust.Shared.GameObjects catch(Exception e) { _sawmill.Error($"Caught exception while trying to recursively delete child entity '{ToPrettyString(child)}' of '{ToPrettyString(uid, metadata)}'\n{e}"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } } @@ -661,6 +676,9 @@ namespace Robust.Shared.GameObjects catch (Exception e) { _sawmill.Error($"Caught exception while trying to call shutdown on component of entity '{ToPrettyString(uid, metadata)}'\n{e}"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } } } @@ -676,9 +694,12 @@ namespace Robust.Shared.GameObjects catch (Exception e) { _sawmill.Error($"Caught exception while invoking event {nameof(EntityDeleted)} on '{ToPrettyString(uid, metadata)}'\n{e}"); +#if !EXCEPTION_TOLERANCE + throw; +#endif } - _eventBus.OnEntityDeleted(uid); + EventBusInternal.OnEntityDeleted(uid); Entities.Remove(uid); // Need to get the ID above before MetadataComponent shutdown but only remove it after everything else is done. NetEntityLookup.Remove(metadata.NetEntity); @@ -912,7 +933,7 @@ namespace Robust.Shared.GameObjects // we want this called before adding components EntityAdded?.Invoke((uid, metadata)); - _eventBus.OnEntityAdded(uid); + EventBusInternal.OnEntityAdded(uid); Entities.Add(uid); // add the required MetaDataComponent directly. @@ -1029,7 +1050,7 @@ namespace Robust.Shared.GameObjects DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized, $"Expected entity {ToPrettyString(entity)} to be initialized, was {meta.EntityLifeStage}"); SetLifeStage(meta, EntityLifeStage.MapInitialized); - EventBus.RaiseLocalEvent(entity, MapInitEventInstance); + EventBusInternal.RaiseLocalEvent(entity, MapInitEventInstance); } /// diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 72e026668..275e732e7 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -155,7 +155,7 @@ public partial class EntitySystem } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected void DirtyField(Entity entity, string fieldName, MetaDataComponent? meta = null) + protected void DirtyField(Entity entity, [ValidateMember]string fieldName, MetaDataComponent? meta = null) where T : IComponentDelta { if (!Resolve(entity.Owner, ref entity.Comp)) @@ -165,7 +165,7 @@ public partial class EntitySystem } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected void DirtyField(EntityUid uid, T component, string fieldName, MetaDataComponent? meta = null) + protected void DirtyField(EntityUid uid, T component, [ValidateMember]string fieldName, MetaDataComponent? meta = null) where T : IComponentDelta { EntityManager.DirtyField(uid, component, fieldName, meta); diff --git a/Robust.Shared/GameObjects/EntitySystem.Subscriptions.cs b/Robust.Shared/GameObjects/EntitySystem.Subscriptions.cs index 35764cb80..a38259d92 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Subscriptions.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Subscriptions.cs @@ -148,7 +148,7 @@ namespace Robust.Shared.GameObjects { foreach (var sub in _subscriptions) { - sub.Unsubscribe(this, EntityManager.EventBus); + sub.Unsubscribe(this, EntityManager.EventBusInternal); } _subscriptions = default; diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index c923cddd9..7a6bdee9e 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -31,8 +31,14 @@ namespace Robust.Shared.GameObjects protected IComponentFactory Factory => EntityManager.ComponentFactory; + /// + /// A logger sawmill for logging debug/informational messages into. + /// public ISawmill Log { get; private set; } = default!; + /// + /// The name for the preprovided log sawmill . + /// protected virtual string SawmillName { get @@ -59,9 +65,19 @@ namespace Robust.Shared.GameObjects } } + /// + /// A list of systems this system MUST update after. This determines the overall update order for systems. + /// protected internal List UpdatesAfter { get; } = new(); + /// + /// A list of systems this system MUST update before. This determines the overall update order for systems. + /// protected internal List UpdatesBefore { get; } = new(); + /// + /// Whether this system will also tick outside of predicted ticks on the client. + /// + /// public bool UpdatesOutsidePrediction { get; protected internal set; } IEnumerable IEntitySystem.UpdatesAfter => UpdatesAfter; @@ -97,36 +113,69 @@ namespace Robust.Shared.GameObjects #region Event Proxy + /// + /// Raise an event on the event bus, broadcasted locally to all listeners by value. + /// + /// The message to send, consuming it. + /// The type of the message to send. protected void RaiseLocalEvent(T message) where T : notnull { - EntityManager.EventBus.RaiseEvent(EventSource.Local, message); + EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, message); } + /// + /// Raise an event on the event bus, broadcasted locally to all listeners by ref. + /// + /// The location of a message, to be sent by reference and modified in place. + /// The type of the message to send. protected void RaiseLocalEvent(ref T message) where T : notnull { - EntityManager.EventBus.RaiseEvent(EventSource.Local, ref message); + EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, ref message); } + /// + /// Raise an event of unknown type on the event bus, broadcasted locally to all listeners of its underlying + /// type. + /// + /// The message to send. protected void RaiseLocalEvent(object message) { - EntityManager.EventBus.RaiseEvent(EventSource.Local, message); + EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, message); } + /// + /// Queue an event to broadcast locally at the end of the tick. + /// + /// The entity event to raise. protected void QueueLocalEvent(EntityEventArgs message) { - EntityManager.EventBus.QueueEvent(EventSource.Local, message); + EntityManager.EventBusInternal.QueueEvent(EventSource.Local, message); } + /// + /// Queue a networked event to be broadcast to all clients at the end of the tick. + /// + /// The entity event to raise. protected void RaiseNetworkEvent(EntityEventArgs message) { EntityManager.EntityNetManager?.SendSystemNetworkMessage(message); } + /// + /// Queue a networked event to be sent to a specific connection at the end of the tick. + /// + /// The entity event to raise. + /// The session to send the event to. protected void RaiseNetworkEvent(EntityEventArgs message, INetChannel channel) { EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, channel); } + /// + /// Queue a networked event to be sent to a specific session at the end of the tick. + /// + /// The entity event to raise. + /// The session to send the event to. protected void RaiseNetworkEvent(EntityEventArgs message, ICommonSession session) { EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, session.Channel); @@ -135,7 +184,7 @@ namespace Robust.Shared.GameObjects /// /// Raises a networked event with some filter. /// - /// The event to send + /// The entity event to raise. /// The filter that specifies recipients /// Optional bool specifying whether or not to save this event to replays. protected void RaiseNetworkEvent(EntityEventArgs message, Filter filter, bool recordReplay = true) @@ -149,32 +198,80 @@ namespace Robust.Shared.GameObjects } } + /// + /// Raise a networked event for a given recipient entity, if there's a client attached. + /// + /// The entity event to raise. + /// The entity to look up a session to send to on. protected void RaiseNetworkEvent(EntityEventArgs message, EntityUid recipient) { if (_playerMan.TryGetSessionByEntity(recipient, out var session)) EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, session.Channel); } + /// + /// Raise a local event, optionally broadcasted, on a specific entity by value. + /// + /// The entity to raise the event on. + /// The event to raise, sent by value. + /// Whether to broadcast the event alongside raising it directed. + /// The type of the event to raise. protected void RaiseLocalEvent(EntityUid uid, TEvent args, bool broadcast = false) where TEvent : notnull { - EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast); + EntityManager.EventBusInternal.RaiseLocalEvent(uid, args, broadcast); } + /// + /// Raise a local event, optionally broadcasted, on a specific entity by value. + /// This raise the event for ' underlying concrete type. + /// + /// The entity to raise the event on. + /// The event to raise, sent by value. + /// Whether to broadcast the event alongside raising it directed. protected void RaiseLocalEvent(EntityUid uid, object args, bool broadcast = false) { - EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast); + EntityManager.EventBusInternal.RaiseLocalEvent(uid, args, broadcast); } + /// + /// Raise a local event, optionally broadcasted, on a specific entity by ref. + /// + /// The entity to raise the event on. + /// The event to raise, sent by ref. + /// Whether to broadcast the event alongside raising it directed. + /// The type of the event to raise. protected void RaiseLocalEvent(EntityUid uid, ref TEvent args, bool broadcast = false) where TEvent : notnull { - EntityManager.EventBus.RaiseLocalEvent(uid, ref args, broadcast); + EntityManager.EventBusInternal.RaiseLocalEvent(uid, ref args, broadcast); } + /// + /// Raise a local event, optionally broadcasted, on a specific entity by ref. + /// This raise the event for ' underlying concrete type. + /// + /// The entity to raise the event on. + /// The event to raise, sent by ref. + /// Whether to broadcast the event alongside raising it directed. protected void RaiseLocalEvent(EntityUid uid, ref object args, bool broadcast = false) { - EntityManager.EventBus.RaiseLocalEvent(uid, ref args, broadcast); + EntityManager.EventBusInternal.RaiseLocalEvent(uid, ref args, broadcast); + } + + /// + protected void RaiseComponentEvent(EntityUid uid, TComp comp, ref TEvent args) + where TEvent : notnull + where TComp : IComponent + { + EntityManager.EventBusInternal.RaiseComponentEvent(uid, comp, ref args); + } + + /// + public void RaiseComponentEvent(EntityUid uid, IComponent component, ref TEvent args) + where TEvent : notnull + { + EntityManager.EventBusInternal.RaiseComponentEvent(uid, component, ref args); } #endregion diff --git a/Robust.Shared/GameObjects/EntityUid.cs b/Robust.Shared/GameObjects/EntityUid.cs index 989c6a0da..243252d6e 100644 --- a/Robust.Shared/GameObjects/EntityUid.cs +++ b/Robust.Shared/GameObjects/EntityUid.cs @@ -9,9 +9,27 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects { /// - /// This type contains a network identification number of an entity. - /// This can be used by the EntityManager to access an entity + /// This type contains the unique identifier for a given entity. + /// This can be used with and obtained from to manipulate, query, and otherwise + /// work with entities and their components. /// + /// + /// + /// An entity is a unique identifier (this type) and a collection of assorted s that are + /// attached to it. Components provide data to describe the entity, and entities+components are operated on by + /// s. + /// + /// + /// EntityUids are not guaranteed to be unique across individual instances of the game, or individual instances + /// of . For network identification, see , and for global + /// uniqueness across time you'll need to make something yourself. + /// + /// + /// Sharing EntityUids between s, or otherwise summoning IDs from thin air, is + /// effectively undefined behavior and most likely will refer to some random other entity that may or may not + /// still exist. + /// + /// [CopyByRef] public readonly struct EntityUid : IEquatable, IComparable, ISpanFormattable { @@ -28,7 +46,7 @@ namespace Robust.Shared.GameObjects public static readonly EntityUid FirstUid = new(1); /// - /// Creates an instance of this structure, with the given network ID. + /// Creates an instance of this structure, with the given unique id. /// public EntityUid(int id) { @@ -58,8 +76,7 @@ namespace Robust.Shared.GameObjects } /// - /// Checks if the ID value is valid. Does not check if it identifies - /// a valid Entity. + /// Checks if the ID value is valid at all, but does not check if it identifies a currently living entity. /// [Pure] public bool IsValid() diff --git a/Robust.Shared/GameObjects/EventBusAttributes.cs b/Robust.Shared/GameObjects/EventBusAttributes.cs index 54848a892..13aa55d30 100644 --- a/Robust.Shared/GameObjects/EventBusAttributes.cs +++ b/Robust.Shared/GameObjects/EventBusAttributes.cs @@ -3,15 +3,17 @@ using System; namespace Robust.Shared.GameObjects; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public sealed class ByRefEventAttribute : Attribute -{ -} +public sealed class ByRefEventAttribute : Attribute; /// -/// Indicates that an eventbus event should only ever be raised through . -/// This allows extra optimizations. +/// This attribute enables an event to be raised as a "component event" via . /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] internal sealed class ComponentEventAttribute : Attribute { + /// + /// If true, this event may **only** be raised via as a "component event", as any subscriptions will not be + /// included in the normal event tables. + /// + public bool Exclusive = true; } diff --git a/Robust.Shared/GameObjects/IComponent.cs b/Robust.Shared/GameObjects/IComponent.cs index c3999910f..695312e25 100644 --- a/Robust.Shared/GameObjects/IComponent.cs +++ b/Robust.Shared/GameObjects/IComponent.cs @@ -5,11 +5,12 @@ using Robust.Shared.Timing; namespace Robust.Shared.GameObjects { - /// - /// Base component for the ECS system. - /// Instances are dynamically instantiated by a ComponentFactory, and will have their IoC Dependencies resolved. - /// + /// + /// The base interface for an ECS component. You probably want .
+ ///
+ /// [ImplicitDataDefinitionForInheritors] + [NotContentImplementable] public partial interface IComponent { /// @@ -50,18 +51,21 @@ namespace Robust.Shared.GameObjects EntityUid Owner { get; set; } /// - /// Component has been (or is currently being) initialized. + /// Whether the component has been or is being initialized. /// + /// bool Initialized { get; } /// /// This is true when the component is active. /// + /// bool Running { get; } /// /// True if the component has been removed from its owner, AKA deleted. /// + /// bool Deleted { get; } /// diff --git a/Robust.Shared/GameObjects/IComponentDebug.cs b/Robust.Shared/GameObjects/IComponentDebug.cs index d930f78cc..2d468eef4 100644 --- a/Robust.Shared/GameObjects/IComponentDebug.cs +++ b/Robust.Shared/GameObjects/IComponentDebug.cs @@ -1,7 +1,14 @@ namespace Robust.Shared.GameObjects { + /// + /// An ages old interface to add extra debug data to the "entfo" server-side command. + /// public partial interface IComponentDebug : IComponent { + /// + /// Returns a debug string to print to the console or otherwise display from debug tools. + /// This can contain newlines. + /// string GetDebugString(); } } diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index 4490a5494..219826c6d 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -49,6 +49,7 @@ namespace Robust.Shared.GameObjects /// /// /// + [NotContentImplementable] public interface IComponentFactory { event Action ComponentsAdded; diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index aae524d1a..3b71a269d 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -7,6 +7,7 @@ using Robust.Shared.Timing; namespace Robust.Shared.GameObjects { + [NotContentImplementable] public partial interface IEntityManager { /// diff --git a/Robust.Shared/GameObjects/IEntityNetworkManager.cs b/Robust.Shared/GameObjects/IEntityNetworkManager.cs index e0daa06f6..b04e17aa3 100644 --- a/Robust.Shared/GameObjects/IEntityNetworkManager.cs +++ b/Robust.Shared/GameObjects/IEntityNetworkManager.cs @@ -6,6 +6,7 @@ namespace Robust.Shared.GameObjects /// /// Manages the sending and receiving of network messages between the server and client(s). /// + [NotContentImplementable] public interface IEntityNetworkManager { /// diff --git a/Robust.Shared/GameObjects/IEntitySystem.cs b/Robust.Shared/GameObjects/IEntitySystem.cs index 383744f42..4536a8513 100644 --- a/Robust.Shared/GameObjects/IEntitySystem.cs +++ b/Robust.Shared/GameObjects/IEntitySystem.cs @@ -11,6 +11,7 @@ namespace Robust.Shared.GameObjects /// They get managed by an . /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] + [NotContentImplementable] public interface IEntitySystem : IEntityEventSubscriber { IEnumerable UpdatesAfter { get; } diff --git a/Robust.Shared/GameObjects/IMapInit.cs b/Robust.Shared/GameObjects/IMapInit.cs deleted file mode 100644 index 194535788..000000000 --- a/Robust.Shared/GameObjects/IMapInit.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Robust.Shared.GameObjects -{ - /// - /// Raised directed on an entity when the map is initialized. - /// - public sealed class MapInitEvent : EntityEventArgs - { - } -} diff --git a/Robust.Shared/GameObjects/MapInitEvent.cs b/Robust.Shared/GameObjects/MapInitEvent.cs new file mode 100644 index 000000000..dcb3ac249 --- /dev/null +++ b/Robust.Shared/GameObjects/MapInitEvent.cs @@ -0,0 +1,9 @@ +namespace Robust.Shared.GameObjects; + +/// +/// Raised directed on an entity when the map is initialized. +/// +[ComponentEvent(Exclusive = false)] +public sealed class MapInitEvent : EntityEventArgs +{ +} diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs index 56a4e586e..7d7d2ead2 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs @@ -405,9 +405,8 @@ public sealed partial class EntityLookupSystem EntityUid? ignored = null) { var broadphaseInv = _transform.GetInvWorldMatrix(lookupUid); + var polygon = new SlimPolygon(worldBounds, broadphaseInv, out var localAABB); - var localAABB = broadphaseInv.TransformBox(worldBounds); - var polygon = new SlimPolygon(localAABB); var result = AnyEntitiesIntersecting(lookupUid, polygon, localAABB, @@ -768,9 +767,7 @@ public sealed partial class EntityLookupSystem if (!_broadQuery.TryGetComponent(gridId, out var lookup)) return; - var localAABB = _transform.GetInvWorldMatrix(gridId).TransformBox(worldBounds); - var polygon = new SlimPolygon(localAABB); - + var polygon = new SlimPolygon(worldBounds, _transform.GetInvWorldMatrix(gridId), out var localAABB); AddEntitiesIntersecting(gridId, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, lookup); AddContained(intersecting, flags); } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index bbcceea7f..a2a76458e 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -1692,48 +1692,49 @@ public abstract partial class SharedMapSystem public bool MoveNext(out TileRef tile) { - if (_x >= _upperX) + while (true) { - tile = TileRef.Zero; - return false; - } - - var gridTile = new Vector2i(_x, _y); - - _y++; - - if (_y >= _upperY) - { - _x++; - _y = _lowerY; - } - - var gridChunk = _mapSystem.GridTileToChunkIndices(_uid, _grid, gridTile); - - if (_grid.Chunks.TryGetValue(gridChunk, out var chunk)) - { - var chunkTile = chunk.GridTileToChunkTile(gridTile); - tile = _mapSystem.GetTileRef(_uid, _grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); - - if (_ignoreEmpty && tile.Tile.IsEmpty) - return MoveNext(out tile); - - if (_predicate == null || _predicate(tile)) + if (_x >= _upperX) { - return true; + tile = TileRef.Zero; + return false; + } + + var gridTile = new Vector2i(_x, _y); + + _y++; + + if (_y >= _upperY) + { + _x++; + _y = _lowerY; + } + + var gridChunk = _mapSystem.GridTileToChunkIndices(_uid, _grid, gridTile); + + if (_grid.Chunks.TryGetValue(gridChunk, out var chunk)) + { + var chunkTile = chunk.GridTileToChunkTile(gridTile); + tile = _mapSystem.GetTileRef(_uid, _grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y); + + if (_ignoreEmpty && tile.Tile.IsEmpty) + continue; + + if (_predicate == null || _predicate(tile)) + { + return true; + } + } + else if (!_ignoreEmpty) + { + tile = new TileRef(_uid, gridTile.X, gridTile.Y, Tile.Empty); + + if (_predicate == null || _predicate(tile)) + { + return true; + } } } - else if (!_ignoreEmpty) - { - tile = new TileRef(_uid, gridTile.X, gridTile.Y, Tile.Empty); - - if (_predicate == null || _predicate(tile)) - { - return true; - } - } - - return MoveNext(out tile); } } } diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 60dfd05ab..773140f49 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -14,6 +14,9 @@ using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { + /// + /// Manages all the grids and maps in the ECS, providing methods to create and modify them. + /// public abstract partial class SharedMapSystem : EntitySystem { [Dependency] private readonly ITileDefinitionManager _tileMan = default!; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs index 48f835e5f..7d2532735 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Map; @@ -132,6 +133,7 @@ public abstract partial class SharedTransformSystem /// /// Creates map-relative given some . /// + [Pure] public EntityCoordinates ToCoordinates(MapCoordinates coordinates) { if (_map.TryGetMap(coordinates.MapId, out var uid)) @@ -145,11 +147,13 @@ public abstract partial class SharedTransformSystem /// /// Returns the grid that the entity whose position the coordinates are relative to is on. /// + [Pure] public EntityUid? GetGrid(EntityCoordinates coordinates) { return GetGrid(coordinates.EntityId); } + [Pure] public EntityUid? GetGrid(Entity entity) { return !Resolve(entity, ref entity.Comp, logMissing:false) ? null : entity.Comp.GridUid; @@ -158,6 +162,7 @@ public abstract partial class SharedTransformSystem /// /// Returns the Map Id these coordinates are on. /// + [Pure] public MapId GetMapId(EntityCoordinates coordinates) { return GetMapId(coordinates.EntityId); diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 92093acf1..557bc1375 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -801,6 +801,41 @@ public abstract class SharedUserInterfaceSystem : EntitySystem return actors.Contains(actor); } + /// + /// Returns true if any of the specified UI keys are open for this entity by anyone. + /// + /// The entity to check. + /// The UI keys to check. + /// True if any UI is open, false otherwise. + [PublicAPI] + public bool IsUiOpen(Entity entity, IEnumerable uiKeys) + { + if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) + return false; + + foreach (var key in uiKeys) + { + if (entity.Comp.Actors.ContainsKey(key)) + return true; + } + + return false; + } + + /// + /// Returns true if any UI is open for this entity by anyone. + /// + /// The entity to check. + /// True if any UI is open, false otherwise. + [PublicAPI] + public bool IsAnyUiOpen(Entity entity) + { + if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false)) + return false; + + return entity.Comp.Actors.Count > 0; + } + /// /// Raises a BUI message locally (on client or server) without networking it. /// diff --git a/Robust.Shared/Graphics/IClydeHandle.cs b/Robust.Shared/Graphics/IClydeHandle.cs index bdff0d4c0..9d92205c0 100644 --- a/Robust.Shared/Graphics/IClydeHandle.cs +++ b/Robust.Shared/Graphics/IClydeHandle.cs @@ -1,6 +1,6 @@ namespace Robust.Shared.Graphics; -public interface IClydeHandle +internal interface IClydeHandle { long Value { get; } } diff --git a/Robust.Shared/Graphics/IEye.cs b/Robust.Shared/Graphics/IEye.cs index cf3c9d15e..3845df497 100644 --- a/Robust.Shared/Graphics/IEye.cs +++ b/Robust.Shared/Graphics/IEye.cs @@ -10,6 +10,7 @@ namespace Robust.Shared.Graphics /// It's a 2D camera in other game dev lingo basically. /// [PublicAPI] + [NotContentImplementable] public interface IEye { /// diff --git a/Robust.Shared/Input/Binding/ICommandBindRegistry.cs b/Robust.Shared/Input/Binding/ICommandBindRegistry.cs index 768348664..8f7e94165 100644 --- a/Robust.Shared/Input/Binding/ICommandBindRegistry.cs +++ b/Robust.Shared/Input/Binding/ICommandBindRegistry.cs @@ -12,6 +12,7 @@ namespace Robust.Shared.Input.Binding /// fire after another system's handlers. This also allows easy unregistering of all bindings /// for a given system / manager. /// + [NotContentImplementable] public interface ICommandBindRegistry { /// CO diff --git a/Robust.Shared/Input/InputCmdContext.cs b/Robust.Shared/Input/InputCmdContext.cs index d164698dd..fb186914c 100644 --- a/Robust.Shared/Input/InputCmdContext.cs +++ b/Robust.Shared/Input/InputCmdContext.cs @@ -6,6 +6,7 @@ namespace Robust.Shared.Input /// /// An Input Context to determine which key binds are currently available to the player. /// + [NotContentImplementable] public interface IInputCmdContext : IEnumerable { /// diff --git a/Robust.Shared/Input/InputCmdMessage.cs b/Robust.Shared/Input/InputCmdMessage.cs index ab5eb84a0..cc7a9ae29 100644 --- a/Robust.Shared/Input/InputCmdMessage.cs +++ b/Robust.Shared/Input/InputCmdMessage.cs @@ -192,6 +192,7 @@ namespace Robust.Shared.Input } } + [NotContentImplementable] public interface IFullInputCmdMessage { GameTick Tick { get; } diff --git a/Robust.Shared/Input/InputContextContainer.cs b/Robust.Shared/Input/InputContextContainer.cs index 892d2e7c0..b0aca8ce6 100644 --- a/Robust.Shared/Input/InputContextContainer.cs +++ b/Robust.Shared/Input/InputContextContainer.cs @@ -7,6 +7,7 @@ namespace Robust.Shared.Input /// /// Contains a set of created s. /// + [NotContentImplementable] public interface IInputContextContainer { /// diff --git a/Robust.Shared/Input/PlayerCommandStates.cs b/Robust.Shared/Input/PlayerCommandStates.cs index eb849a3d3..6d5db3eed 100644 --- a/Robust.Shared/Input/PlayerCommandStates.cs +++ b/Robust.Shared/Input/PlayerCommandStates.cs @@ -5,6 +5,7 @@ namespace Robust.Shared.Input /// /// Contains a mapping of to their current . /// + [NotContentImplementable] public interface IPlayerCommandStates { /// diff --git a/Robust.Shared/IoC/DynamicTypeFactory.cs b/Robust.Shared/IoC/DynamicTypeFactory.cs index 77dfc1763..6d759596a 100644 --- a/Robust.Shared/IoC/DynamicTypeFactory.cs +++ b/Robust.Shared/IoC/DynamicTypeFactory.cs @@ -12,6 +12,7 @@ namespace Robust.Shared.IoC /// /// [PublicAPI] + [NotContentImplementable] public interface IDynamicTypeFactory { /// diff --git a/Robust.Shared/IoC/IDependencyCollection.cs b/Robust.Shared/IoC/IDependencyCollection.cs index 31d859013..866a78c7c 100644 --- a/Robust.Shared/IoC/IDependencyCollection.cs +++ b/Robust.Shared/IoC/IDependencyCollection.cs @@ -31,6 +31,7 @@ namespace Robust.Shared.IoC /// /// /// + [NotContentImplementable] public interface IDependencyCollection { IDependencyCollection FromParent(IDependencyCollection parentCollection); diff --git a/Robust.Shared/Localization/ILocalizationManager.cs b/Robust.Shared/Localization/ILocalizationManager.cs index 59bab25c1..7d04c5016 100644 --- a/Robust.Shared/Localization/ILocalizationManager.cs +++ b/Robust.Shared/Localization/ILocalizationManager.cs @@ -20,6 +20,7 @@ namespace Robust.Shared.Localization /// /// [PublicAPI] + [NotContentImplementable] public interface ILocalizationManager { /// diff --git a/Robust.Shared/Log/ILogManager.cs b/Robust.Shared/Log/ILogManager.cs index d3bc8c0b5..26e414365 100644 --- a/Robust.Shared/Log/ILogManager.cs +++ b/Robust.Shared/Log/ILogManager.cs @@ -6,6 +6,7 @@ namespace Robust.Shared.Log /// Manages logging sawmills. /// /// + [NotContentImplementable] public interface ILogManager { /// diff --git a/Robust.Shared/Log/Logger.cs b/Robust.Shared/Log/Logger.cs index f0ee62ddc..1bd629f59 100644 --- a/Robust.Shared/Log/Logger.cs +++ b/Robust.Shared/Log/Logger.cs @@ -19,7 +19,7 @@ namespace Robust.Shared.Log /// The instance we're using. /// As it's a direct proxy to IoC this will not work if IoC is not functional. /// - // TODO: Maybe cache this to improve performance. + // TODO: Kill private static ILogManager LogManagerSingleton => IoCManager.Resolve(); /// diff --git a/Robust.Shared/Map/EntityCoordinates.cs b/Robust.Shared/Map/EntityCoordinates.cs index 10b1130d0..323fe187c 100644 --- a/Robust.Shared/Map/EntityCoordinates.cs +++ b/Robust.Shared/Map/EntityCoordinates.cs @@ -14,7 +14,7 @@ namespace Robust.Shared.Map /// A set of coordinates relative to another entity. /// [PublicAPI, DataRecord] - public readonly record struct EntityCoordinates : ISpanFormattable + public readonly partial record struct EntityCoordinates : ISpanFormattable { public static readonly EntityCoordinates Invalid = new(EntityUid.Invalid, Vector2.Zero); diff --git a/Robust.Shared/Map/Events/MapSerializationEvents.cs b/Robust.Shared/Map/Events/MapSerializationEvents.cs index c2721c1bc..477161ed5 100644 --- a/Robust.Shared/Map/Events/MapSerializationEvents.cs +++ b/Robust.Shared/Map/Events/MapSerializationEvents.cs @@ -34,7 +34,10 @@ public sealed class BeforeEntityReadEvent /// For convenience, the event also contains a set with all the maps that the entities are on. This does not /// necessarily mean that the maps are themselves getting serialized. /// -public readonly record struct BeforeSerializationEvent(HashSet Entities, HashSet MapIds); +public readonly record struct BeforeSerializationEvent( + HashSet Entities, + HashSet MapIds, + FileCategory Category = FileCategory.Unknown); /// /// This event is broadcast just after entities (and their children) have been serialized, but before it gets written to a yaml file. diff --git a/Robust.Shared/Map/IMapManager.cs b/Robust.Shared/Map/IMapManager.cs index 5d7f8a393..d0aaea4a5 100644 --- a/Robust.Shared/Map/IMapManager.cs +++ b/Robust.Shared/Map/IMapManager.cs @@ -16,8 +16,9 @@ namespace Robust.Shared.Map public delegate bool GridCallback(EntityUid uid, MapGridComponent grid, ref TState state); /// - /// This manages all of the grids in the world. + /// This manages all the grids and maps in the world. Largely superseded by . /// + [NotContentImplementable] public interface IMapManager { public const bool Approximate = false; diff --git a/Robust.Shared/Map/ITileDefinition.cs b/Robust.Shared/Map/ITileDefinition.cs index 0c17213ba..357ed7734 100644 --- a/Robust.Shared/Map/ITileDefinition.cs +++ b/Robust.Shared/Map/ITileDefinition.cs @@ -13,6 +13,9 @@ namespace Robust.Shared.Map /// /// The numeric tile ID used to refer to this tile inside the map datastructure. /// + /// + /// The engine does not automatically generate these IDs, games are responsible for assigning them. + /// ushort TileId { get; } /// diff --git a/Robust.Shared/Map/ITileDefinitionManager.cs b/Robust.Shared/Map/ITileDefinitionManager.cs index c40920256..02c087408 100644 --- a/Robust.Shared/Map/ITileDefinitionManager.cs +++ b/Robust.Shared/Map/ITileDefinitionManager.cs @@ -7,6 +7,7 @@ namespace Robust.Shared.Map /// /// This manages tile definitions for grid tiles. /// + [NotContentImplementable] public interface ITileDefinitionManager : IEnumerable { Tile GetVariantTile(string name, IRobustRandom random); diff --git a/Robust.Shared/Map/MapCoordinates.cs b/Robust.Shared/Map/MapCoordinates.cs index 1e8319036..361598b94 100644 --- a/Robust.Shared/Map/MapCoordinates.cs +++ b/Robust.Shared/Map/MapCoordinates.cs @@ -13,7 +13,7 @@ namespace Robust.Shared.Map /// [PublicAPI, DataRecord] [Serializable, NetSerializable] - public readonly record struct MapCoordinates : ISpanFormattable + public readonly partial record struct MapCoordinates : ISpanFormattable { public static readonly MapCoordinates Nullspace = new(Vector2.Zero, MapId.Nullspace); diff --git a/Robust.Shared/Map/MapId.cs b/Robust.Shared/Map/MapId.cs index b76b89987..4045791de 100644 --- a/Robust.Shared/Map/MapId.cs +++ b/Robust.Shared/Map/MapId.cs @@ -1,11 +1,26 @@ using System; +using Robust.Shared.GameObjects; using Robust.Shared.Serialization; namespace Robust.Shared.Map { + /// + /// Uniquely identifies a map. + /// + /// + /// All maps, aside from , are also entities. When writing generic code it's usually + /// preferable to use or instead. + /// + /// + /// [Serializable, NetSerializable] public readonly struct MapId : IEquatable { + /// + /// The equivalent of null for maps. There is no map entity assigned to this and anything here is + /// a root (has no parent) for the transform hierarchy.
+ /// All map entities live in nullspace and function as roots, for example. + ///
public static readonly MapId Nullspace = new(0); internal readonly int Value; diff --git a/Robust.Shared/Map/Tile.cs b/Robust.Shared/Map/Tile.cs index 139047288..2ec851e36 100644 --- a/Robust.Shared/Map/Tile.cs +++ b/Robust.Shared/Map/Tile.cs @@ -23,12 +23,12 @@ public readonly struct Tile : IEquatable, ISpanFormattable public readonly byte Flags; /// - /// Variant of this tile to render. + /// Variant of this tile to render. /// public readonly byte Variant; /// - /// Rotation and mirroring of this tile to render. 0-3 is normal, 4-7 is mirrored. + /// Rotation and mirroring of this tile to render. 0-3 is normal, 4-7 is mirrored. /// public readonly byte RotationMirroring; diff --git a/Robust.Shared/Network/HttpClientHolder.cs b/Robust.Shared/Network/HttpClientHolder.cs index 9372cf1a8..42625812d 100644 --- a/Robust.Shared/Network/HttpClientHolder.cs +++ b/Robust.Shared/Network/HttpClientHolder.cs @@ -17,6 +17,7 @@ namespace Robust.Shared.Network; /// content code can't send arbitrary HTTP requests. /// /// +[NotContentImplementable] public interface IHttpClientHolder { HttpClient Client { get; } diff --git a/Robust.Shared/Network/IClientNetManager.cs b/Robust.Shared/Network/IClientNetManager.cs index fc31c1ba5..5e14b6e25 100644 --- a/Robust.Shared/Network/IClientNetManager.cs +++ b/Robust.Shared/Network/IClientNetManager.cs @@ -5,6 +5,7 @@ namespace Robust.Shared.Network /// /// The Client version of the INetManager. /// + [NotContentImplementable] public interface IClientNetManager : INetManager { /// diff --git a/Robust.Shared/Network/INetChannel.cs b/Robust.Shared/Network/INetChannel.cs index 90fd25044..387a26c4c 100644 --- a/Robust.Shared/Network/INetChannel.cs +++ b/Robust.Shared/Network/INetChannel.cs @@ -7,6 +7,7 @@ namespace Robust.Shared.Network /// /// A network channel between this peer and a remote peer. /// + [NotContentImplementable] public interface INetChannel { /// diff --git a/Robust.Shared/Network/INetManager.cs b/Robust.Shared/Network/INetManager.cs index 1c04d7a9c..89b407e95 100644 --- a/Robust.Shared/Network/INetManager.cs +++ b/Robust.Shared/Network/INetManager.cs @@ -7,6 +7,7 @@ namespace Robust.Shared.Network /// /// A network server that listens for connections, relays packets, and manages channels. /// + [NotContentImplementable] public interface INetManager { /// diff --git a/Robust.Shared/Network/IServerNetManager.cs b/Robust.Shared/Network/IServerNetManager.cs index 6da688fe6..1b029fa24 100644 --- a/Robust.Shared/Network/IServerNetManager.cs +++ b/Robust.Shared/Network/IServerNetManager.cs @@ -6,6 +6,7 @@ namespace Robust.Shared.Network /// /// The server version of the INetManager. /// + [NotContentImplementable] public interface IServerNetManager : INetManager { public delegate Task NetApprovalDelegate(NetApprovalEventArgs eventArgs); diff --git a/Robust.Shared/Network/LoginType.cs b/Robust.Shared/Network/LoginType.cs index c1d633fc9..79bfa0ee3 100644 --- a/Robust.Shared/Network/LoginType.cs +++ b/Robust.Shared/Network/LoginType.cs @@ -1,5 +1,8 @@ namespace Robust.Shared.Network { + /// + /// The possible kinds of login a user can engage in. + /// public enum LoginType : byte { /// diff --git a/Robust.Shared/Network/NetChannelArgs.cs b/Robust.Shared/Network/NetChannelArgs.cs index 873d9dcc6..136206d54 100644 --- a/Robust.Shared/Network/NetChannelArgs.cs +++ b/Robust.Shared/Network/NetChannelArgs.cs @@ -90,6 +90,7 @@ namespace Robust.Shared.Network /// /// Structured reason common interface. /// + [NotContentImplementable] public interface INetStructuredReason { NetDisconnectMessage Message { get; } diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index a8eda20dc..12b40a806 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -173,7 +173,8 @@ namespace Robust.Shared.Network PatronTier = joinedRespJson.UserData.PatronTier, HWId = legacyHwid, ModernHWIds = modernHWIds, - Trust = joinedRespJson.ConnectionData!.Trust + Trust = joinedRespJson.ConnectionData!.Trust, + CreatedTime = joinedRespJson.UserData.CreatedTime }; padSuccessMessage = false; type = LoginType.LoggedIn; @@ -378,7 +379,7 @@ namespace Robust.Shared.Network // ReSharper disable ClassNeverInstantiated.Local private sealed record HasJoinedResponse(bool IsValid, HasJoinedUserData? UserData, HasJoinedConnectionData? ConnectionData); - private sealed record HasJoinedUserData(string UserName, Guid UserId, string? PatronTier); + private sealed record HasJoinedUserData(string UserName, Guid UserId, string? PatronTier, DateTime CreatedTime); private sealed record HasJoinedConnectionData(string[] Hwids, float Trust); // ReSharper restore ClassNeverInstantiated.Local } diff --git a/Robust.Shared/Network/NetUserData.cs b/Robust.Shared/Network/NetUserData.cs index 428ea3d96..7ee61388e 100644 --- a/Robust.Shared/Network/NetUserData.cs +++ b/Robust.Shared/Network/NetUserData.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using System.Text; using Robust.Shared.ViewVariables; @@ -18,6 +19,9 @@ namespace Robust.Shared.Network [ViewVariables] public string? PatronTier { get; init; } + [ViewVariables] + public DateTime? CreatedTime { get; init; } + public ImmutableArray HWId { get; init; } /// diff --git a/Robust.Shared/Network/NetUserId.cs b/Robust.Shared/Network/NetUserId.cs index 3ab3c734c..7b46a3f16 100644 --- a/Robust.Shared/Network/NetUserId.cs +++ b/Robust.Shared/Network/NetUserId.cs @@ -3,6 +3,23 @@ using Robust.Shared.Serialization; namespace Robust.Shared.Network { + /// + /// A unique identifier for a given user's account. + /// + /// + /// + /// If connected to auth and auth is mandatory, this is guaranteed to be + /// globally unique within that auth service, duplicate players will not exist. + /// + /// + /// Similarly, the engine assumes that if the user is a known guest with an + /// assigned ID, their ID is also globally unique. + /// + /// + /// This is independent of username, and in the current auth implementation users can freely change username. + /// Think of this as a way to refer to a given account regardless of what it's named. + /// + /// [Serializable, NetSerializable] public struct NetUserId : IEquatable, ISelfSerialize { diff --git a/Robust.Shared/Physics/Collision/Shapes/IPhysShape.cs b/Robust.Shared/Physics/Collision/Shapes/IPhysShape.cs index 1137ac06e..078ce4071 100644 --- a/Robust.Shared/Physics/Collision/Shapes/IPhysShape.cs +++ b/Robust.Shared/Physics/Collision/Shapes/IPhysShape.cs @@ -17,6 +17,7 @@ namespace Robust.Shared.Physics.Collision.Shapes /// /// A primitive physical shape that is used by a . /// + [NotContentImplementable] public interface IPhysShape : IEquatable { /// diff --git a/Robust.Shared/Physics/Dynamics/Contacts/Contact.cs b/Robust.Shared/Physics/Dynamics/Contacts/Contact.cs index e65ece7fe..ba21487e8 100644 --- a/Robust.Shared/Physics/Dynamics/Contacts/Contact.cs +++ b/Robust.Shared/Physics/Dynamics/Contacts/Contact.cs @@ -241,27 +241,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts } IsTouching = touching; - var status = ContactStatus.NoContact; - - if (!wasTouching) - { - if (touching) - { - status = ContactStatus.StartTouching; - } - } - else - { - if (!touching) - { - status = ContactStatus.EndTouching; - } - // Still touching - else - { - status = ContactStatus.Touching; - } - } + var status = GetContactStatus(this, wasTouching); #if DEBUG if (!sensor) @@ -273,6 +253,32 @@ namespace Robust.Shared.Physics.Dynamics.Contacts return status; } + [Pure] + internal static ContactStatus GetContactStatus(Contact contact, bool wasTouching) + { + if (!wasTouching) + { + if (contact.IsTouching) + { + return ContactStatus.StartTouching; + } + } + else + { + if (!contact.IsTouching) + { + return ContactStatus.EndTouching; + } + // Still touching + else + { + return ContactStatus.Touching; + } + } + + return ContactStatus.NoContact; + } + /// /// Trimmed down version of that only updates whether or not the contact's shapes are /// touching. diff --git a/Robust.Shared/Physics/IBroadPhase.cs b/Robust.Shared/Physics/IBroadPhase.cs index a1b22eddf..c9bc2e3eb 100644 --- a/Robust.Shared/Physics/IBroadPhase.cs +++ b/Robust.Shared/Physics/IBroadPhase.cs @@ -6,6 +6,7 @@ using Robust.Shared.Physics.Dynamics; namespace Robust.Shared.Physics; +[NotContentImplementable] public interface IBroadPhase { int Count { get; } @@ -61,6 +62,7 @@ public interface IBroadPhase void RebuildBottomUp(); } +[NotContentImplementable] public interface IBroadPhase : ICollection where T : notnull { int Capacity { get; } diff --git a/Robust.Shared/Physics/Shapes/Polygon.cs b/Robust.Shared/Physics/Shapes/Polygon.cs index df5b51b51..5961d482d 100644 --- a/Robust.Shared/Physics/Shapes/Polygon.cs +++ b/Robust.Shared/Physics/Shapes/Polygon.cs @@ -41,6 +41,8 @@ internal record struct Polygon : IPhysShape } + public Polygon(PhysShapeAabb aabb) : this(aabb.LocalBounds) {} + public Polygon(PolygonShape polyShape) { Unsafe.SkipInit(out this); @@ -52,6 +54,24 @@ internal record struct Polygon : IPhysShape polyShape.Normals.AsSpan()[..VertexCount].CopyTo(_normals.AsSpan); } + internal Polygon(SlimPolygon slim) + { + Unsafe.SkipInit(out this); + Radius = slim.Radius; + VertexCount = slim.VertexCount; + + _vertices._00 = slim._vertices._00; + _vertices._01 = slim._vertices._01; + _vertices._02 = slim._vertices._02; + _vertices._03 = slim._vertices._03; + + _normals._00 = slim._normals._00; + _normals._01 = slim._normals._01; + _normals._02 = slim._normals._02; + _normals._03 = slim._normals._03; + Centroid = slim.Centroid; + } + public Polygon(Box2 box) { Unsafe.SkipInit(out this); diff --git a/Robust.Shared/Physics/Shapes/SlimPolygon.cs b/Robust.Shared/Physics/Shapes/SlimPolygon.cs index 7c341f6e3..53e69a161 100644 --- a/Robust.Shared/Physics/Shapes/SlimPolygon.cs +++ b/Robust.Shared/Physics/Shapes/SlimPolygon.cs @@ -1,6 +1,9 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; using Robust.Shared.Maths; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Serialization.Manager.Attributes; @@ -33,7 +36,6 @@ internal record struct SlimPolygon : IPhysShape public SlimPolygon(Box2 box) { - Unsafe.SkipInit(out this); Radius = 0f; _vertices._00 = box.BottomLeft; @@ -45,27 +47,74 @@ internal record struct SlimPolygon : IPhysShape _normals._01 = new Vector2(1.0f, 0.0f); _normals._02 = new Vector2(0.0f, 1.0f); _normals._03 = new Vector2(-1.0f, 0.0f); + Centroid = box.Center; } - public SlimPolygon(Box2Rotated bounds) + public SlimPolygon(in Box2 box, in Matrix3x2 transform, out Box2 aabb) { Unsafe.SkipInit(out this); + transform.TransformBox(box, out var x, out var y); + var tmp = SimdHelpers.GetAABB(x, y); + aabb = Unsafe.As, Box2>(ref tmp); + + if (Sse.IsSupported) + { + var span = MemoryMarshal.Cast>(_vertices.AsSpan); + span[0] = Sse.UnpackLow(x, y); + span[1] = Sse.UnpackHigh(x, y); + } + else + { + _vertices._00 = new Vector2(x[0], y[0]); + _vertices._01 = new Vector2(x[1], y[1]); + _vertices._02 = new Vector2(x[2], y[2]); + _vertices._03 = new Vector2(x[3], y[3]); + } + Radius = 0f; + Centroid = (_vertices._00 + _vertices._02) / 2; - _vertices._00 = bounds.BottomLeft; - _vertices._01 = bounds.BottomRight; - _vertices._02 = bounds.TopRight; - _vertices._03 = bounds.TopLeft; - + // TODO SIMD + // Probably use a special case for SlimPolygon Polygon.CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4); - - Centroid = bounds.Center; } - public Box2 ComputeAABB(Transform transform, int childIndex) + public SlimPolygon(in Box2Rotated box, in Matrix3x2 transform, out Box2 aabb) + : this(in box.Box, box.Transform * transform, out aabb) { + } + + public SlimPolygon(in Box2Rotated box) + { + Unsafe.SkipInit(out this); + box.GetVertices(out var x, out var y); + + if (Sse.IsSupported) + { + var span = MemoryMarshal.Cast>(_vertices.AsSpan); + span[0] = Sse.UnpackLow(x, y); + span[1] = Sse.UnpackHigh(x, y); + } + else + { + _vertices._00 = new Vector2(x[0], y[0]); + _vertices._01 = new Vector2(x[1], y[1]); + _vertices._02 = new Vector2(x[2], y[2]); + _vertices._03 = new Vector2(x[3], y[3]); + } + + Radius = 0f; + Centroid = (_vertices._00 + _vertices._02) / 2; + + // TODO SIMD + // Probably use a special case for SlimPolygon + Polygon.CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4); + } + + public Box2 ComputeAABBSlow(Transform transform) + { + // This is just Polygon.ComputeAABB DebugTools.Assert(VertexCount > 0); - DebugTools.Assert(childIndex == 0); var verts = _vertices.AsSpan; var lower = Transform.Mul(transform, verts[0]); var upper = lower; @@ -81,6 +130,36 @@ internal record struct SlimPolygon : IPhysShape return new Box2(lower - r, upper + r); } + public Box2 ComputeAABBSse(Transform transform) + { + var span = MemoryMarshal.Cast>(_vertices.AsSpan); + // Span = [x0, y0, x1, y1], [x2, y2, x3, y3] + + var polyX = Sse.Shuffle(span[0], span[1], 0b_10_00_10_00); + var polyY = Sse.Shuffle(span[0], span[1], 0b_11_01_11_01); + // polyX = [x0, x1, x2, x3], polyY = [y0, y1, y2, y3] + + Transform.MulSimd(transform, polyX, polyY, out var x, out var y); + var lbrt = SimdHelpers.GetAABB(x, y); + + // Next we enlarge the bounds by th radius. i.e, box.Enlarged(R); + // TODO is this even needed for SlimPoly? Is the radius ever set to non-zero? + var zero = Vector128.Zero; + var r = Vector128.Create(Radius); + lbrt = lbrt - Sse.MoveLowToHigh(r, zero) + Sse.MoveHighToLow(r, zero); + // lbrt = lbrt - [R, R, 0, 0] + [0, 0, R, R] + + return Unsafe.As, Box2>(ref lbrt); + } + + public Box2 ComputeAABB(Transform transform, int childIndex) + { + DebugTools.Assert(childIndex == 0); + return Sse.IsSupported + ? ComputeAABBSse(transform) + : ComputeAABBSlow(transform); + } + public bool Equals(SlimPolygon other) { return Radius.Equals(other.Radius) && _vertices.AsSpan[..VertexCount].SequenceEqual(other._vertices.AsSpan[..VertexCount]); diff --git a/Robust.Shared/Physics/Systems/FixtureSystem.Shapes.cs b/Robust.Shared/Physics/Systems/FixtureSystem.Shapes.cs index 277cfaf5b..1a02f76be 100644 --- a/Robust.Shared/Physics/Systems/FixtureSystem.Shapes.cs +++ b/Robust.Shared/Physics/Systems/FixtureSystem.Shapes.cs @@ -73,7 +73,7 @@ namespace Robust.Shared.Physics.Systems } } - public static MassData GetMassData(IPhysShape shape, float density) + public static MassData GetMassData(T shape, float density) where T : IPhysShape { var data = new MassData(); @@ -82,7 +82,7 @@ namespace Robust.Shared.Physics.Systems return data; } - public static void GetMassData(IPhysShape shape, ref MassData data, float density) + public static void GetMassData(T shape, ref MassData data, float density) where T : IPhysShape { // Box2D just calls fixture.GetMassData which just calls the shape method anyway soooo // we can just cut out the middle-man @@ -106,16 +106,17 @@ namespace Robust.Shared.Physics.Systems data.I = data.Mass * (0.5f * circle.Radius * circle.Radius + Vector2.Dot(circle.Position, circle.Position)); break; case PhysShapeAabb aabb: - var polygon = (PolygonShape) aabb; + var polygon = new Polygon(aabb); GetMassData(polygon, ref data, density); break; - case Polygon fastPoly: - GetMassData(new PolygonShape(fastPoly), ref data, density); + case PolygonShape fatPoly: + GetMassData(new Polygon(fatPoly), ref data, density); break; case SlimPolygon slim: - GetMassData(new PolygonShape(slim), ref data, density); + var slimPoly = new Polygon(slim); + GetMassData(slimPoly, ref data, density); break; - case PolygonShape poly: + case Polygon poly: // Polygon mass, centroid, and inertia. // Let rho be the polygon density in mass per unit area. // Then: @@ -142,6 +143,7 @@ namespace Robust.Shared.Physics.Systems var count = poly.VertexCount; DebugTools.Assert(count >= 3); + DebugTools.Assert(poly._normals._00 != Vector2.Zero); Vector2 center = new(0.0f, 0.0f); float area = 0.0f; @@ -149,15 +151,16 @@ namespace Robust.Shared.Physics.Systems // Get a reference point for forming triangles. // Use the first vertex to reduce round-off errors. - var s = poly.Vertices[0]; + var s = poly._vertices._00; + var polySpan = poly._vertices.AsSpan; const float k_inv3 = 1.0f / 3.0f; for (var i = 0; i < count; ++i) { // Triangle vertices. - var e1 = poly.Vertices[i] - s; - var e2 = i + 1 < count ? poly.Vertices[i+1] - s : poly.Vertices[0] - s; + var e1 = polySpan[i] - s; + var e2 = i + 1 < count ? polySpan[i+1] - s : polySpan[0] - s; float D = Vector2Helpers.Cross(e1, e2); diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index cce6cd4f3..dbd1eff56 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.Numerics; -using System.Threading.Tasks; -using Microsoft.Extensions.ObjectPool; -using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -35,10 +32,9 @@ namespace Robust.Shared.Physics.Systems private EntityQuery _physicsQuery; private EntityQuery _xformQuery; - private float _broadphaseExpand; + private readonly HashSet _gridMoveBuffer = new(); - private readonly Dictionary _broadMatrices = new(); - private HashSet _gridMoveBuffer = new(); + private float _frameTime; /* * Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required. @@ -55,9 +51,9 @@ namespace Robust.Shared.Physics.Systems _contactJob = new() { - _mapManager = _mapManager, + MapManager = _mapManager, System = this, - BroadphaseExpand = _broadphaseExpand, + TransformSys = EntityManager.System(), // TODO: EntityManager one isn't ready yet? XformQuery = GetEntityQuery(), }; @@ -71,13 +67,13 @@ namespace Robust.Shared.Physics.Systems UpdatesOutsidePrediction = true; UpdatesAfter.Add(typeof(SharedTransformSystem)); - Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true); - } - - private void SetBroadphaseExpand(float value) - { - _contactJob.BroadphaseExpand = value; - _broadphaseExpand = value; + Subs.CVar(_cfg, + CVars.TargetMinimumTickrate, + val => + { + _frameTime = 1f / val; + }, + true); } public void Rebuild(BroadphaseComponent component, bool fullBuild) @@ -109,6 +105,7 @@ namespace Robust.Shared.Physics.Systems // This is so that if we're on a broadphase that's moving (e.g. a grid) we need to make sure anything // we move over is getting checked for collisions, and putting it on the movebuffer is the easiest way to do so. var moveBuffer = _physicsSystem.MoveBuffer; + _gridMoveBuffer.Clear(); foreach (var gridUid in movedGrids) { @@ -120,7 +117,7 @@ namespace Robust.Shared.Physics.Systems continue; var worldAABB = _transform.GetWorldMatrix(xform).TransformBox(grid.LocalAABB); - var enlargedAABB = worldAABB.Enlarged(_broadphaseExpand); + var enlargedAABB = worldAABB.Enlarged(GetBroadphaseExpand(_physicsQuery.GetComponent(gridUid), _frameTime)); var state = (moveBuffer, _gridMoveBuffer); QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB); @@ -135,6 +132,11 @@ namespace Robust.Shared.Physics.Systems } } + private float GetBroadphaseExpand(PhysicsComponent body, float frameTime) + { + return body.LinearVelocity.Length() * 1.2f * frameTime; + } + private void QueryMapBroadphase(IBroadPhase broadPhase, ref (HashSet, HashSet) state, Box2 enlargedAABB) @@ -163,11 +165,12 @@ namespace Robust.Shared.Physics.Systems /// internal void FindNewContacts() { + _contactJob.FrameTime = _frameTime; + _contactJob.Pairs.Clear(); + var moveBuffer = _physicsSystem.MoveBuffer; var movedGrids = _physicsSystem.MovedGrids; - _gridMoveBuffer.Clear(); - // Find any entities being driven over that might need to be considered FindGridContacts(movedGrids); @@ -195,55 +198,32 @@ namespace Robust.Shared.Physics.Systems _contactJob.MoveBuffer.Add(proxy); } - _broadMatrices.Clear(); - var broadQuery = AllEntityQuery(); - - // Cache broadphase matrices up front. - // We'll defer the proxy world AABBs until we get contacts rather than doing it on every single move. - // This is because contacts are run in parallel so we can spread the work a bit more and also don't duplicate it per tick. - while (broadQuery.MoveNext(out var bUid, out _)) - { - _broadMatrices[bUid] = _transform.GetWorldMatrix(bUid); - } - - for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++) - { - _contactJob.ContactBuffer.Add(new List()); - } - var count = moveBuffer.Count; _parallel.ProcessNow(_contactJob, count); - for (var i = 0; i < count; i++) + foreach (var (proxyA, proxyB, flags) in _contactJob.Pairs) { - var proxies = _contactJob.ContactBuffer[i]; + var otherBody = proxyB.Body; + var contactFlags = ContactFlags.None; - if (proxies.Count == 0) - continue; - - var proxyA = _contactJob.MoveBuffer[i]; - var proxyABody = proxyA.Body; - - _fixturesQuery.TryGetComponent(proxyA.Entity, out var manager); - - foreach (var other in proxies) + // Because we may be colliding with something asleep (due to the way grid movement works) need + // to make sure the contact doesn't fail. + // This is because we generate a contact across 2 different broadphases where both bodies aren't + // moving locally but are moving in world-terms. + if ((flags & PairFlag.Wake) == PairFlag.Wake) { - var otherBody = other.Body; - - // Because we may be colliding with something asleep (due to the way grid movement works) need - // to make sure the contact doesn't fail. - // This is because we generate a contact across 2 different broadphases where both bodies aren't - // moving locally but are moving in world-terms. - if (proxyA.Fixture.Hard && other.Fixture.Hard && - (_gridMoveBuffer.Contains(proxyA) || _gridMoveBuffer.Contains(other))) - { - _physicsSystem.WakeBody(proxyA.Entity, force: true, manager: manager, body: proxyABody); - _physicsSystem.WakeBody(other.Entity, force: true, body: otherBody); - } - - _physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other); + _physicsSystem.WakeBody(proxyA.Entity, force: true, body: proxyA.Body); + _physicsSystem.WakeBody(proxyB.Entity, force: true, body: otherBody); } + + // TODO: Actually implement this for grids, atm they have their own skrungly fixture handling which prevents this. + if ((PairFlag.Grid & flags) == PairFlag.Grid) + { + contactFlags |= ContactFlags.Grid; + } + + _physicsSystem.AddPair(proxyA.FixtureId, proxyB.FixtureId, proxyA, proxyB, flags: contactFlags); } moveBuffer.Clear(); @@ -252,6 +232,8 @@ namespace Robust.Shared.Physics.Systems private void HandleGridCollisions(HashSet movedGrids) { + // TODO: Could move this into its own job. + // Ideally we'd just have some way to flag an entity as "AABB moves not proxy" into its own movebuffer. foreach (var gridUid in movedGrids) { var grid = _gridQuery.GetComponent(gridUid); @@ -301,6 +283,12 @@ namespace Robust.Shared.Physics.Systems return true; } + // If the other entity is lower ID and also moved then let that handle the collision. + if (tuple.grid.Owner.Id > uid.Id && tuple._physicsSystem.MovedGrids.Contains(uid)) + { + return true; + } + var (_, _, otherGridMatrix, otherGridInvMatrix) = tuple.xformSystem.GetWorldPositionRotationMatrixWithInv(collidingXform); var otherGridBounds = otherGridMatrix.TransformBox(component.LocalAABB); var otherTransform = tuple._physicsSystem.GetPhysicsTransform(uid); @@ -337,6 +325,10 @@ namespace Robust.Shared.Physics.Systems { var otherFixture = fixturesB.Fixtures[otherId]; + // There's already a contact so ignore it. + if (fixture.Contacts.ContainsKey(otherFixture)) + break; + for (var j = 0; j < otherFixture.Shape.ChildCount; j++) { var otherAABB = otherFixture.Shape.ComputeAABB(otherTransform, j); @@ -370,7 +362,7 @@ namespace Robust.Shared.Physics.Systems FixtureProxy proxy, Box2 worldAABB, EntityUid broadphase, - List pairBuffer) + List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer) { DebugTools.Assert(proxy.Body.CanCollide); @@ -401,7 +393,7 @@ namespace Robust.Shared.Physics.Systems } var broadphaseComp = _broadphaseQuery.GetComponent(broadphase); - var state = (pairBuffer, proxy); + var state = (pairBuffer, _physicsSystem.MoveBuffer, this, _physicsSystem, proxy); QueryBroadphase(broadphaseComp.DynamicTree, state, aabb); @@ -411,23 +403,57 @@ namespace Robust.Shared.Physics.Systems QueryBroadphase(broadphaseComp.StaticTree, state, aabb); } - private void QueryBroadphase(IBroadPhase broadPhase, (List, FixtureProxy) state, Box2 aabb) + private void QueryBroadphase(IBroadPhase broadPhase, (List<(FixtureProxy, FixtureProxy, PairFlag)>, HashSet MoveBuffer, SharedBroadphaseSystem Broadphase, SharedPhysicsSystem PhysicsSystem, FixtureProxy) state, Box2 aabb) { broadPhase.QueryAabb(ref state, static ( - ref (List pairBuffer, FixtureProxy proxy) tuple, + ref (List<(FixtureProxy, FixtureProxy, PairFlag)> pairs, HashSet moveBuffer, SharedBroadphaseSystem broadphase, SharedPhysicsSystem physicsSystem, FixtureProxy proxy) tuple, in FixtureProxy other) => { DebugTools.Assert(other.Body.CanCollide); // Logger.DebugS("physics", $"Checking {proxy.Entity} against {other.Fixture.Body.Owner} at {aabb}"); - if (tuple.proxy == other || - !SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture) || - tuple.proxy.Entity == other.Entity) + if (tuple.proxy.Entity == other.Entity || + !SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture)) { return true; } - tuple.pairBuffer.Add(other); + // Avoid creating duplicate pairs. + // We give priority to whoever has the lower entity ID. + if (tuple.proxy.Entity.Id > other.Entity.Id) + { + // Let the other fixture handle it. + if (tuple.moveBuffer.Contains(other)) + return true; + } + + // Check if contact already exists. + if (tuple.proxy.Fixture.Contacts.ContainsKey(other.Fixture)) + return true; + + // TODO: Add in the slow path check here but turnstiles currently explodes this on content so. + if (!tuple.physicsSystem.ShouldCollideJoints(tuple.proxy.Entity, other.Entity)) + return true; + + // TODO: Sensors handled elsewhere when we do v3 port. + //if (!tuple.proxy.Fixture.Hard || !other.Fixture.Hard) + // return true; + + // TODO: Check if interlocked + array is better here which is what box2d does + // It then just heap allocates anything over the array size. + var flags = PairFlag.None; + if (tuple.proxy.Fixture.Hard && + other.Fixture.Hard && + (tuple.broadphase._gridMoveBuffer.Contains(tuple.proxy) || tuple.broadphase._gridMoveBuffer.Contains(other))) + { + flags |= PairFlag.Wake; + } + + lock (tuple.pairs) + { + tuple.pairs.Add((tuple.proxy, other, flags)); + } + return true; }, aabb, true); } @@ -560,39 +586,42 @@ namespace Robust.Shared.Physics.Systems { public SharedBroadphaseSystem System = default!; public SharedTransformSystem TransformSys = default!; - public IMapManager _mapManager = default!; - - public float BroadphaseExpand; + public IMapManager MapManager = default!; public EntityQuery XformQuery; - public List> ContactBuffer = new(); - public List MoveBuffer = new(); + public readonly List MoveBuffer = new(); - public int BatchSize => 8; + public List<(FixtureProxy, FixtureProxy, PairFlag)> Pairs = new(64); + + public float FrameTime; + + // Box2D uses 64 but we have to do grid queries for each fixtureproxy which will add a fair bit of overhead. + // Plus we also run events + trycomp for joints on top. + public int BatchSize => 16; public void Execute(int index) { var proxy = MoveBuffer[index]; var broadphaseUid = XformQuery.GetComponent(proxy.Entity).Broadphase?.Uid; - var worldAABB = System._broadMatrices[broadphaseUid!.Value].TransformBox(proxy.AABB); - var buffer = ContactBuffer[index]; - buffer.Clear(); + var worldAABB = TransformSys.GetWorldMatrix(broadphaseUid!.Value).TransformBox(proxy.AABB); var mapUid = XformQuery.GetComponent(proxy.Entity).MapUid ?? EntityUid.Invalid; + var broadphaseExpand = System.GetBroadphaseExpand(proxy.Body, FrameTime); + var proxyBody = proxy.Body; DebugTools.Assert(!proxyBody.Deleted); - var state = (System, proxy, worldAABB, buffer); + var state = (System, proxy, worldAABB, Pairs); // Get every broadphase we may be intersecting. - _mapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(BroadphaseExpand), ref state, + MapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state, static (EntityUid uid, MapGridComponent _, ref ( SharedBroadphaseSystem system, FixtureProxy proxy, Box2 worldAABB, - List pairBuffer) tuple) => + List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer) tuple) => { ref var buffer = ref tuple.pairBuffer; tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer); @@ -602,9 +631,24 @@ namespace Robust.Shared.Physics.Systems includeMap: false); // Struct ref moment, I have no idea what's fastest. - buffer = state.buffer; - System.FindPairs(proxy, worldAABB, mapUid, buffer); + System.FindPairs(proxy, worldAABB, mapUid, Pairs); } } + + [Flags] + private enum PairFlag : byte + { + None = 0, + + /// + /// Should we wake the contacting entities. + /// + Wake = 1 << 0, + + /// + /// Is it a grid collision. + /// + Grid = 1 << 1, + } } } diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs index 29646dc2f..5dfdcedea 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Contacts.cs @@ -263,16 +263,13 @@ public abstract partial class SharedPhysicsSystem // Broadphase has already done the faster check for collision mask / layers // so no point duplicating - // Does a contact already exist? - if (fixtureA.Contacts.ContainsKey(fixtureB)) - return; - + DebugTools.Assert(!fixtureA.Contacts.ContainsKey(fixtureB)); DebugTools.Assert(!fixtureB.Contacts.ContainsKey(fixtureA)); var xformA = entA.Comp2; var xformB = entB.Comp2; // Does a joint override collision? Is at least one body dynamic? - if (!ShouldCollide(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB)) + if (!ShouldCollideSlow(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB)) return; // Call the factory. @@ -310,14 +307,14 @@ public abstract partial class SharedPhysicsSystem /// /// Go through the cached broadphase movement and update contacts. /// - internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB) + internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB, ContactFlags flags = ContactFlags.None) { AddPair((proxyA.Entity, proxyA.Body, proxyA.Xform), (proxyB.Entity, proxyB.Body, proxyB.Xform), fixtureAId, fixtureBId, proxyA.Fixture, proxyA.ChildIndex, proxyB.Fixture, proxyB.ChildIndex, - proxyA.Body, proxyB.Body); + proxyA.Body, proxyB.Body, flags: flags); } internal static bool ShouldCollide(Fixture fixtureA, Fixture fixtureB) @@ -447,7 +444,8 @@ public abstract partial class SharedPhysicsSystem { // Check default filtering if (!ShouldCollide(fixtureA, fixtureB) || - !ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB)) + !ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) || + !ShouldCollideJoints(uidA, uidB)) { DestroyContact(contact); continue; @@ -577,57 +575,7 @@ public abstract partial class SharedPhysicsSystem continue; } - switch (status[i]) - { - case ContactStatus.StartTouching: - { - if (!contact.IsTouching) continue; - - var fixtureA = contact.FixtureA!; - var fixtureB = contact.FixtureB!; - var bodyA = contact.BodyA!; - var bodyB = contact.BodyB!; - var uidA = contact.EntityA; - var uidB = contact.EntityB; - var worldPoint = worldPoints[i]; - var points = new FixedArray2(worldPoint._00, worldPoint._01); - var worldNormal = worldPoint._02; - - var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal); - var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal); - - RaiseLocalEvent(uidA, ref ev1, true); - RaiseLocalEvent(uidB, ref ev2, true); - break; - } - case ContactStatus.Touching: - break; - case ContactStatus.EndTouching: - { - var fixtureA = contact.FixtureA; - var fixtureB = contact.FixtureB; - - // If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted) - // then we'll just skip the EndCollide. - if (fixtureA == null || fixtureB == null) continue; - - var bodyA = contact.BodyA!; - var bodyB = contact.BodyB!; - var uidA = contact.EntityA; - var uidB = contact.EntityB; - - var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB); - var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA); - - RaiseLocalEvent(uidA, ref ev1); - RaiseLocalEvent(uidB, ref ev2); - break; - } - case ContactStatus.NoContact: - break; - default: - throw new ArgumentOutOfRangeException(); - } + RunContactEvents(status[i], contact, worldPoints[i]); } ArrayPool.Shared.Return(contacts); @@ -635,6 +583,60 @@ public abstract partial class SharedPhysicsSystem ArrayPool>.Shared.Return(worldPoints); } + internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray4 worldPoint) + { + switch (status) + { + case ContactStatus.StartTouching: + { + if (!contact.IsTouching) return; + + var fixtureA = contact.FixtureA!; + var fixtureB = contact.FixtureB!; + var bodyA = contact.BodyA!; + var bodyB = contact.BodyB!; + var uidA = contact.EntityA; + var uidB = contact.EntityB; + var points = new FixedArray2(worldPoint._00, worldPoint._01); + var worldNormal = worldPoint._02; + + var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal); + var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal); + + RaiseLocalEvent(uidA, ref ev1, true); + RaiseLocalEvent(uidB, ref ev2, true); + break; + } + case ContactStatus.Touching: + break; + case ContactStatus.EndTouching: + { + var fixtureA = contact.FixtureA; + var fixtureB = contact.FixtureB; + + // If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted) + // then we'll just skip the EndCollide. + if (fixtureA == null || fixtureB == null) return; + + var bodyA = contact.BodyA!; + var bodyB = contact.BodyB!; + var uidA = contact.EntityA; + var uidB = contact.EntityB; + + var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB); + var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA); + + RaiseLocalEvent(uidA, ref ev1); + RaiseLocalEvent(uidB, ref ev2); + break; + } + case ContactStatus.NoContact: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4[] worldPoints) { if (count == 0) @@ -720,10 +722,31 @@ public abstract partial class SharedPhysicsSystem } } + /// + /// Is there a joint blocking collision between these bodies. + /// + internal bool ShouldCollideJoints(Entity entA, Entity entB) + { + // Does a joint prevent collision? + // if one of them doesn't have jointcomp then they can't share a common joint. + // otherwise, only need to iterate over the joints of one component as they both store the same joint. + if (JointQuery.Resolve(entA.Owner, ref entA.Comp, false) && JointQuery.HasComp(entB)) + { + foreach (var joint in entA.Comp.Joints.Values) + { + // Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking. + if (!joint.CollideConnected && (entB.Owner == joint.BodyAUid || entB.Owner == joint.BodyBUid)) + return false; + } + } + + return true; + } + /// /// Used to prevent bodies from colliding; may lie depending on joints. /// - protected bool ShouldCollide( + internal bool ShouldCollideSlow( EntityUid uid, EntityUid other, PhysicsComponent body, @@ -757,18 +780,7 @@ public abstract partial class SharedPhysicsSystem return false; } - // Does a joint prevent collision? - // if one of them doesn't have jointcomp then they can't share a common joint. - // otherwise, only need to iterate over the joints of one component as they both store the same joint. - if (TryComp(uid, out JointComponent? jointComponentA) && HasComp(other)) - { - foreach (var joint in jointComponentA.Joints.Values) - { - // Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking. - if (!joint.CollideConnected && (other == joint.BodyAUid || other == joint.BodyBUid)) - return false; - } - } + // Joints already handled before the contact pair is made. var preventCollideMessage = new PreventCollideEvent(uid, other, body, otherBody, fixture, otherFixture); RaiseLocalEvent(uid, ref preventCollideMessage); diff --git a/Robust.Shared/Physics/Transform.cs b/Robust.Shared/Physics/Transform.cs index 0efa327d5..92b1d6554 100644 --- a/Robust.Shared/Physics/Transform.cs +++ b/Robust.Shared/Physics/Transform.cs @@ -24,6 +24,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using JetBrains.Annotations; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -80,6 +81,23 @@ namespace Robust.Shared.Physics return new Vector2(x, y); } + // Simd version of Mul + // I wanted to put this in SimdHelpers, but ran into some Robust.Math project reference issues. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal static void MulSimd( + in Transform transform, + Vector128 x, + Vector128 y, + out Vector128 xOut, + out Vector128 yOut) + { + var cos = Vector128.Create(transform.Quaternion2D.C); + var sin = Vector128.Create(transform.Quaternion2D.S); + xOut = cos * x - sin * y + Vector128.Create(transform.Position.X); + yOut = sin * x + cos * y + Vector128.Create(transform.Position.Y); + } + [Pure] public static Vector2 MulT(in Vector2[] A, in Vector2 v) { diff --git a/Robust.Shared/Physics/VerticesSimplifier.cs b/Robust.Shared/Physics/VerticesSimplifier.cs index 230ac3452..2de49de87 100644 --- a/Robust.Shared/Physics/VerticesSimplifier.cs +++ b/Robust.Shared/Physics/VerticesSimplifier.cs @@ -14,6 +14,7 @@ namespace Robust.Shared.Physics /// /// Takes in a list of vertices and removes any that are redundant (within tolerance). /// + [NotContentImplementable] public interface IVerticesSimplifier { List Simplify(List vertices, float tolerance); diff --git a/Robust.Shared/Player/ActorComponent.cs b/Robust.Shared/Player/ActorComponent.cs index 234defbbf..19cb95a58 100644 --- a/Robust.Shared/Player/ActorComponent.cs +++ b/Robust.Shared/Player/ActorComponent.cs @@ -3,9 +3,16 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.Player; -[RegisterComponent] +/// +/// This component is added to entities that are currently controlled by a player and is removed when the player is detached. +/// +/// +[RegisterComponent, UnsavedComponent] public sealed partial class ActorComponent : Component { + /// + /// The player session currently attached to the entity. + /// [ViewVariables] public ICommonSession PlayerSession { get; internal set; } = default!; } diff --git a/Robust.Shared/Player/ActorSystem.cs b/Robust.Shared/Player/ActorSystem.cs index c34c5d8e8..56ebde567 100644 --- a/Robust.Shared/Player/ActorSystem.cs +++ b/Robust.Shared/Player/ActorSystem.cs @@ -22,6 +22,9 @@ public sealed class ActorSystem : EntitySystem _playerManager.SetAttachedEntity(component.PlayerSession, null); } + /// + /// Retrieves the session on a given entity, if one exists. + /// [PublicAPI] public bool TryGetSession(EntityUid? uid, out ICommonSession? session) { @@ -35,6 +38,9 @@ public sealed class ActorSystem : EntitySystem return false; } + /// + /// Retrieves the session on a given entity, if one exists. + /// [PublicAPI] [Pure] public ICommonSession? GetSession(EntityUid? uid) diff --git a/Robust.Shared/Player/ICommonSession.cs b/Robust.Shared/Player/ICommonSession.cs index 94177574e..40aa5763f 100644 --- a/Robust.Shared/Player/ICommonSession.cs +++ b/Robust.Shared/Player/ICommonSession.cs @@ -8,69 +8,86 @@ using Robust.Shared.Network; namespace Robust.Shared.Player; /// -/// Common info between client and server sessions. +/// Common info between client and server sessions. /// +/// +[NotContentImplementable] public interface ICommonSession { /// - /// Status of the session. + /// Status of the session, dictating the connection state. /// SessionStatus Status { get; } /// - /// Entity UID that this session is represented by in the world, if any. + /// Entity UID that this session is represented by in the world, if any. Any attached entity will have + /// . /// + /// EntityUid? AttachedEntity { get; } /// - /// The UID of this session. + /// The unique user ID of this session. /// + /// + /// If this user's is or + /// (), + /// their user id is globally unique as described on + /// . + /// NetUserId UserId { get; } /// - /// Current name of this player. + /// Current name of this player. /// string Name { get; } /// - /// Current connection latency of this session. If is not null this simply returns - /// . This is not currently usable by client-side code that wants to try access ping - /// information of other players. + /// Current connection latency of this session. If is not null this simply returns + /// . This is not currently usable by client-side code that wants to try access + /// ping information of other players. /// short Ping { get; } // TODO PlayerManager ping networking. /// - /// The current network channel for this session. + /// The current network channel for this session, if one exists. /// /// - /// On the Server every player has a network channel, - /// on the Client only the LocalPlayer has a network channel, and that channel points to the server. + /// On the server every player has a network channel, + /// and on the client only the LocalPlayer has a network channel, and that channel points to the server. + /// Sessions without channels will have null here. /// INetChannel Channel { get; set; } + /// + /// How this session logged in, which dictates the uniqueness of . + /// LoginType AuthType { get; } /// - /// List of "eyes" to use for PVS range checks. + /// List of "eyes" to use for PVS range checks. /// HashSet ViewSubscriptions { get; } + /// + /// The last time this session connected. + /// DateTime ConnectedTime { get; set; } /// - /// Session state, for sending player lists to clients. + /// Session state, for sending player lists to clients. /// SessionState State { get; } /// - /// Class for storing arbitrary session-specific data that is not lost upon reconnect. + /// Class for storing arbitrary session-specific data that is not lost upon reconnect. /// SessionData Data { get; } /// - /// If true, this indicates that this is a client-side session, and should be ignored when applying a server's - /// game state. + /// If true, this indicates that this is a client-side session, and should be ignored when applying a server's + /// game state. /// bool ClientSide { get; set; } } diff --git a/Robust.Shared/Player/ISharedPlayerManager.cs b/Robust.Shared/Player/ISharedPlayerManager.cs index 7631067ea..ece64cdab 100644 --- a/Robust.Shared/Player/ISharedPlayerManager.cs +++ b/Robust.Shared/Player/ISharedPlayerManager.cs @@ -10,21 +10,28 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.Player; +[NotContentImplementable] public interface ISharedPlayerManager { /// - /// list of connected sessions. + /// List of all connected sessions. /// + /// + /// You should not modify the contents of this list. + /// ICommonSession[] Sessions { get; } /// - /// Sessions with a remote endpoint. On the server, this is equivalent to . On the client, - /// this will only ever contain + /// Sessions with a remote endpoint. On the server, this is equivalent to . On the client, + /// this will only ever contain /// + /// + /// You should not modify the contents of this list. + /// ICommonSession[] NetworkedSessions { get; } /// - /// Dictionary mapping connected users to their sessions. + /// Dictionary mapping connected users to their sessions. /// IReadOnlyDictionary SessionsDict { get; } @@ -39,7 +46,7 @@ public interface ISharedPlayerManager int MaxPlayers { get; } /// - /// Initializes the manager. + /// Initializes the manager. /// /// Maximum number of players that can connect to this server at one time. Does nothing /// on the client. @@ -49,79 +56,123 @@ public interface ISharedPlayerManager void Shutdown(); /// - /// Indicates that some session's networked data has changed. This will cause an updated player list to be sent to - /// all players. + /// Indicates that some session's networked data has changed. This will cause an updated player list to be sent + /// to all players. /// void Dirty(); /// - /// The session of the local player. This will be null on the server. + /// The session of the local player. This will be null on the server. /// [ViewVariables] ICommonSession? LocalSession { get; } /// - /// The user Id of the local player. This will be null on the server. + /// The user id of the local player. This will be null on the server. /// [ViewVariables] NetUserId? LocalUser { get; } /// - /// The entity currently controlled by the local player. This will be null on the server. + /// The entity currently controlled by the local player. This will be null on the server. /// [ViewVariables] EntityUid? LocalEntity { get; } /// - /// This gets invoked when a session's changes. + /// This gets invoked when a session's changes. /// event EventHandler? PlayerStatusChanged; /// - /// Attempts to resolve a username into a . + /// Attempts to resolve a username into a . /// bool TryGetUserId(string userName, out NetUserId userId); /// - /// Attempts to get the session that is currently attached to a given entity. + /// Attempts to get the session that is currently attached to a given entity. /// bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session); /// - /// Attempts to get the session with the given . + /// Attempts to get the session with the given . /// bool TryGetSessionById([NotNullWhen(true)] NetUserId? user, [NotNullWhen(true)] out ICommonSession? session); /// - /// Attempts to get the session with the given . + /// Attempts to get the session with the given . /// bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out ICommonSession? session); /// - /// Attempts to get the session that corresponds to the given channel. + /// Attempts to get the session that corresponds to the given channel. /// bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out ICommonSession? session); + /// + /// Gets the session that corresponds to the given channel, throwing if it doesn't exist. + /// + /// Thrown if no such session exists. ICommonSession GetSessionByChannel(INetChannel channel) => GetSessionById(channel.UserId); + /// + /// Gets the session that corresponds to the given user id, throwing if it doesn't exist. + /// + /// Thrown if no such session exists. ICommonSession GetSessionById(NetUserId user); /// - /// Check if the given user id has an active session. + /// Check if the given user id has an active session. /// bool ValidSessionId(NetUserId user) => TryGetSessionById(user, out _); + /// + /// Alternate method to get + /// SessionData GetPlayerData(NetUserId userId); + /// + /// Grabs a session's if it can be found. + /// + /// The user ID to get data for. + /// The session data if found. + /// Success or failure. bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out SessionData? data); + /// + /// Grabs a session's if it can be found. + /// + /// The username to get data for. + /// The session data if found. + /// Success or failure. bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out SessionData? data); + /// + /// Checks if a given user has any . + /// bool HasPlayerData(NetUserId userId); + /// + /// Returns all session data. + /// + /// IEnumerable GetAllPlayerData(); void GetPlayerStates(GameTick fromTick, List states); void UpdateState(ICommonSession commonSession); + /// void RemoveSession(ICommonSession session, bool removeData = false); + /// + /// Completely destroys a session, optionally also removing its data. + /// void RemoveSession(NetUserId user, bool removeData = false); + /// + /// Creates a session from a network channel. + /// ICommonSession CreateAndAddSession(INetChannel channel); + /// + /// Creates a new session, without a network channel attached. + /// + /// + /// This should be used carefully, games tend to expect a network channel to be present unless they're client + /// side. This is for example used to create a session for singleplayer clients. + /// ICommonSession CreateAndAddSession(NetUserId user, string name); /// diff --git a/Robust.Shared/Prototypes/Attributes.cs b/Robust.Shared/Prototypes/Attributes.cs index cc26fbfc3..e8a90f68a 100644 --- a/Robust.Shared/Prototypes/Attributes.cs +++ b/Robust.Shared/Prototypes/Attributes.cs @@ -7,9 +7,14 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.Shared.Prototypes; /// -/// Quick attribute to give the prototype its type string. -/// To prevent needing to instantiate it because interfaces can't declare statics. +/// Defines the unique type id ("kind") and load priority for a prototype, and registers it so the game knows about +/// the type in question during init and can deserialize it. +///
///
+/// +/// +/// +/// [AttributeUsage(AttributeTargets.Class, Inherited = false)] #if !ROBUST_ANALYZERS_TEST [BaseTypeRequired(typeof(IPrototype))] @@ -20,11 +25,18 @@ namespace Robust.Shared.Prototypes; public class PrototypeAttribute : Attribute { /// - /// Override for the name of this kind of prototype. If not specified, this is automatically inferred via + /// Override for the name of this kind of prototype. + /// If not specified, this is automatically inferred via /// public string? Type { get; internal set; } - public readonly int LoadPriority = 1; + /// + /// Defines the load order for prototype kinds. Higher priorities are loaded earlier. + /// + public readonly int LoadPriority; + + /// See . + /// See . public PrototypeAttribute(string? type = null, int loadPriority = 1) { Type = type; @@ -38,6 +50,13 @@ public class PrototypeAttribute : Attribute } } +/// +/// Defines the unique type id ("kind") and load priority for a prototype, and registers it so the game knows about +/// the type in question during init and can deserialize it. +///
+///
+/// +/// [AttributeUsage(AttributeTargets.Class, Inherited = false)] #if !ROBUST_ANALYZERS_TEST [BaseTypeRequired(typeof(IPrototype))] @@ -47,6 +66,8 @@ public class PrototypeAttribute : Attribute #endif public sealed class PrototypeRecordAttribute : PrototypeAttribute { + /// See . + /// See . public PrototypeRecordAttribute(string type, int loadPriority = 1) : base(type, loadPriority) { } diff --git a/Robust.Shared/Prototypes/Docs.xml b/Robust.Shared/Prototypes/Docs.xml new file mode 100644 index 000000000..aaff03ed4 --- /dev/null +++ b/Robust.Shared/Prototypes/Docs.xml @@ -0,0 +1,18 @@ + + + + + + It is strongly discouraged to ever construct inheritors of this interface manually, if you need a prototype that + is also runtime-constructable data, consider moving all the data into its own class and using + to flatten it into + the prototype class. + + + Prototypes should never be mutated at runtime, and should be treated as a read only source of truth. Keep + in mind that for example reading a container like a list from the prototype does not make a copy, and you must do + so yourself if you intend to mutate. + + + + diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index eb6f227b4..b5be7c370 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -141,19 +141,19 @@ namespace Robust.Shared.Prototypes ///
[ViewVariables] [ParentDataFieldAttribute(typeof(AbstractPrototypeIdArraySerializer))] - public string[]? Parents { get; } + public string[]? Parents { get; private set; } [ViewVariables] [NeverPushInheritance] [AbstractDataField] - public bool Abstract { get; } + public bool Abstract { get; private set; } /// /// A dictionary mapping the component type list to the YAML mapping containing their settings. /// [DataField("components")] [AlwaysPushInheritance] - public ComponentRegistry Components { get; } = new(); + public ComponentRegistry Components = new(); public EntityPrototype() { @@ -286,7 +286,7 @@ namespace Robust.Shared.Prototypes } [DataRecord] - public record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping); + public partial record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping); [DataDefinition] public sealed partial class EntityPlacementProperties diff --git a/Robust.Shared/Prototypes/IPrototype.cs b/Robust.Shared/Prototypes/IPrototype.cs index 9cb4b03a8..7dff43f27 100644 --- a/Robust.Shared/Prototypes/IPrototype.cs +++ b/Robust.Shared/Prototypes/IPrototype.cs @@ -7,17 +7,20 @@ using Robust.Shared.ViewVariables; namespace Robust.Shared.Prototypes { /// - /// An IPrototype is a prototype that can be loaded from the global YAML prototypes. + /// IPrototype, when combined with , defines a type that the game can load from + /// the global YAML prototypes folder during init or runtime. It's a way of defining data for the game to read + /// and act on. /// - /// - /// To use this, the prototype must be accessible through IoC with - /// and it must have a to give it a type string. - /// + /// + /// + /// + /// public interface IPrototype { /// - /// An ID for this prototype instance. - /// If this is a duplicate, an error will be thrown. + /// A unique ID for this prototype instance. + /// This will never be a duplicate, and the game will error during loading if there are multiple prototypes + /// with the same unique ID. /// #if !ROBUST_ANALYZERS_TEST [ViewVariables(VVAccess.ReadOnly)] @@ -25,13 +28,40 @@ namespace Robust.Shared.Prototypes string ID { get; } } + /// + /// An extension of that allows for a prototype to have parents that it inherits data + /// from. This, alongside and + /// , allow data-based multiple inheritance. + /// + /// + /// An example of this in practice is . + /// + /// + /// + /// + /// public interface IInheritingPrototype { + /// + /// The collection of parents for this prototype. Parents' data is applied to the child in order of + /// specification in the array. + /// string[]? Parents { get; } + /// + /// Whether this prototype is "abstract". This behaves ike an abstract class, abstract prototypes are never + /// indexable and do not show up when enumerating prototypes, as they're just a source of data to inherit + /// from. + /// bool Abstract { get; } } + /// + /// Marks a field as a prototype's unique identifier. This field must always be a string?. + ///
+ /// This field is always required. + ///
+ /// public sealed class IdDataFieldAttribute : DataFieldAttribute { public const string Name = "id"; @@ -41,6 +71,13 @@ namespace Robust.Shared.Prototypes } } + /// + /// Marks a field as the parent/parents field for this prototype, as required by + /// . This must either be a string?, or string[]?. + ///
+ /// This field is never required. + ///
+ /// public sealed class ParentDataFieldAttribute : DataFieldAttribute { public const string Name = "parent"; @@ -50,6 +87,13 @@ namespace Robust.Shared.Prototypes } } + /// + /// Marks a field as the abstract field for this prototype, as required by + /// . This must be a bool. + ///
+ /// This field is never required. + ///
+ /// public sealed class AbstractDataFieldAttribute : DataFieldAttribute { public const string Name = "abstract"; diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index 6e4978fdf..fcb5cedb1 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -16,13 +16,19 @@ using Robust.Shared.Utility; namespace Robust.Shared.Prototypes; /// -/// Handle storage and loading of YAML prototypes. +/// Handle storage and loading of YAML prototypes. These are defined in code using +/// and on classes that implement or +/// . /// /// -/// Terminology: -/// "Kinds" are the types of prototypes there are, like . -/// "Prototypes" are simply filled-in prototypes from YAML. +/// Terminology:
+/// "Kinds" are the types of prototypes there are, like .
+/// "Prototypes" are simply filled-in prototypes from YAML.
///
+/// +/// +/// +[NotContentImplementable] public interface IPrototypeManager { void Initialize(); diff --git a/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs b/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs index 21ebe2a46..e92ab3a92 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs @@ -53,7 +53,7 @@ public partial class PrototypeManager var extractedList = new List(); var i = 0; - foreach (var document in DataNodeParser.ParseYamlStream(reader)) + foreach (var document in DataNodeParser.ParseYamlStream(reader, internStrings: true)) { i += 1; LoadedData?.Invoke(document); @@ -152,7 +152,7 @@ public partial class PrototypeManager return; var i = 0; - foreach (var document in DataNodeParser.ParseYamlStream(reader)) + foreach (var document in DataNodeParser.ParseYamlStream(reader, internStrings: true)) { LoadedData?.Invoke(document); @@ -254,7 +254,7 @@ public partial class PrototypeManager _hasEverBeenReloaded = true; var i = 0; - foreach (var document in DataNodeParser.ParseYamlStream(stream)) + foreach (var document in DataNodeParser.ParseYamlStream(stream, internStrings: true)) { LoadedData?.Invoke(document); diff --git a/Robust.Shared/Prototypes/PrototypeManager.YamlValidate.cs b/Robust.Shared/Prototypes/PrototypeManager.YamlValidate.cs index 9107f671c..65035b428 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.YamlValidate.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.YamlValidate.cs @@ -39,7 +39,14 @@ public partial class PrototypeManager } var yamlStream = new YamlStream(); - yamlStream.Load(reader); + try + { + yamlStream.Load(reader); + } + catch (Exception e) + { + throw new PrototypeLoadException($"Error loading file: '{resourcePath}'\n{e}"); + } foreach (var doc in yamlStream.Documents) { diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index dfd9a0c9f..c727ff6ce 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -1005,12 +1005,7 @@ namespace Robust.Shared.Prototypes static string CalculatePrototypeName(Type type) { - const string prototype = "Prototype"; - if (!type.Name.EndsWith(prototype)) - throw new InvalidPrototypeNameException($"Prototype {type} must end with the word Prototype"); - - var name = type.Name.AsSpan(); - return $"{char.ToLowerInvariant(name[0])}{name[1..^prototype.Length]}"; + return PrototypeUtility.CalculatePrototypeName(type.Name); } /// @@ -1274,11 +1269,4 @@ namespace Robust.Shared.Prototypes throw new ArgumentOutOfRangeException($"Unable to pick valid prototype for {typeof(T)}?"); } } - - public sealed class InvalidPrototypeNameException : Exception - { - public InvalidPrototypeNameException(string message) : base(message) - { - } - } } diff --git a/Robust.Shared/Prototypes/PrototypeUtility.cs b/Robust.Shared/Prototypes/PrototypeUtility.cs new file mode 100644 index 000000000..db3f898b5 --- /dev/null +++ b/Robust.Shared/Prototypes/PrototypeUtility.cs @@ -0,0 +1,23 @@ +using System; + +namespace Robust.Shared.Prototypes; + +public static class PrototypeUtility +{ + /// + /// Prototypes using autogenerated names are required to end in this string. + /// + public const string PrototypeNameEnding = "Prototype"; + + /// + /// Given the type name of a Prototype, returns an autogenerated name for it. + /// + public static string CalculatePrototypeName(string type) + { + var name = type.AsSpan(); + if (!type.EndsWith(PrototypeNameEnding)) + return $"{char.ToLowerInvariant(name[0])}{name.Slice(1).ToString()}"; + + return $"{char.ToLowerInvariant(name[0])}{name.Slice(1, name.Length - PrototypeNameEnding.Length - 1).ToString()}"; + } +} diff --git a/Robust.Shared/Random/IRobustRandom.cs b/Robust.Shared/Random/IRobustRandom.cs index 4e368c748..488903592 100644 --- a/Robust.Shared/Random/IRobustRandom.cs +++ b/Robust.Shared/Random/IRobustRandom.cs @@ -13,6 +13,7 @@ namespace Robust.Shared.Random; public interface IRobustRandom { /// Get the underlying . + [Obsolete("Do not access the underlying implementation")] System.Random GetRandom(); /// Set seed for underlying . @@ -138,6 +139,15 @@ public interface IRobustRandom /// Randomly switches positions in collection. void Shuffle(IList list) { + if (list is T[] arr) + { + // Done to avoid significant performance dip from Moq workaround in RandomExtensions.cs, + // doubt it matters much. + // https://github.com/space-wizards/RobustToolbox/issues/6329 + Shuffle(arr); + return; + } + var n = list.Count; while (n > 1) { @@ -162,13 +172,7 @@ public interface IRobustRandom /// Randomly switches positions in collection. void Shuffle(ValueList list) { - var n = list.Count; - while (n > 1) - { - n -= 1; - var k = Next(n + 1); - (list[k], list[n]) = (list[n], list[k]); - } + Shuffle(list.Span); } } diff --git a/Robust.Shared/Random/RandomExtensions.cs b/Robust.Shared/Random/RandomExtensions.cs index a499b4a7a..36d60a4eb 100644 --- a/Robust.Shared/Random/RandomExtensions.cs +++ b/Robust.Shared/Random/RandomExtensions.cs @@ -178,7 +178,10 @@ public static class RandomExtensions if (allowDuplicates == false && count >= source.Count) { var arr = source.ToArray(); - random.Shuffle(arr); + // Explicit type cast to IList to avoid calling the Span overload. + // We have some tests that rely on mocking of this call, and Moq doesn't support Span atm. + // https://github.com/space-wizards/RobustToolbox/issues/6329 + random.Shuffle((IList)arr); return arr; } @@ -232,7 +235,10 @@ public static class RandomExtensions if (allowDuplicates == false && count >= source.Length) { var arr = source.ToArray(); - random.Shuffle(arr); + // Explicit type cast to IList to avoid calling the Span overload. + // We have some tests that rely on mocking of this call, and Moq doesn't support Span atm. + // https://github.com/space-wizards/RobustToolbox/issues/6329 + random.Shuffle((IList)arr); return arr; } diff --git a/Robust.Shared/Reflection/IReflectionManager.cs b/Robust.Shared/Reflection/IReflectionManager.cs index e8d13152f..d2519b1b7 100644 --- a/Robust.Shared/Reflection/IReflectionManager.cs +++ b/Robust.Shared/Reflection/IReflectionManager.cs @@ -23,6 +23,7 @@ namespace Robust.Shared.Reflection /// /// /// +[NotContentImplementable] public interface IReflectionManager { /// diff --git a/Robust.Shared/Reflection/ReflectionManager.cs b/Robust.Shared/Reflection/ReflectionManager.cs index a6cff5d6b..112ca726b 100644 --- a/Robust.Shared/Reflection/ReflectionManager.cs +++ b/Robust.Shared/Reflection/ReflectionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -107,7 +108,11 @@ namespace Robust.Shared.Reflection public void LoadAssemblies(IEnumerable assemblies) { - this.assemblies.AddRange(assemblies); + var assembliesArray = assemblies.Distinct().ToArray(); + if (this.assemblies.Intersect(assembliesArray).Any()) + throw new InvalidOperationException("Attempted to load the same assembly multiple times!"); + + this.assemblies.AddRange(assembliesArray); _getAllTypesCache.Clear(); OnAssemblyAdded?.Invoke(this, new ReflectionUpdateEventArgs(this)); } diff --git a/Robust.Shared/Replays/IReplayRecordingManager.cs b/Robust.Shared/Replays/IReplayRecordingManager.cs index 0a6b0adb7..98de3dbd6 100644 --- a/Robust.Shared/Replays/IReplayRecordingManager.cs +++ b/Robust.Shared/Replays/IReplayRecordingManager.cs @@ -2,14 +2,19 @@ using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Markdown.Mapping; using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; using System.Threading.Tasks; using Robust.Shared.ContentPack; using Robust.Shared.GameStates; +using Robust.Shared.Serialization; using Robust.Shared.Utility; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; namespace Robust.Shared.Replays; +[NotContentImplementable] public interface IReplayRecordingManager { /// @@ -184,6 +189,7 @@ public record struct ReplayRecordingStats(TimeSpan Time, uint Ticks, long Size, /// /// /// +[NotContentImplementable] public interface IReplayFileWriter { /// @@ -202,6 +208,25 @@ public interface IReplayFileWriter ResPath path, ReadOnlyMemory bytes, CompressionLevel compressionLevel = CompressionLevel.Optimal); + + /// + /// Writes a yaml document into a file in the replay. + /// + /// The file path to write to. + /// The yaml document to write to the file. + /// How much to compress the file. + void WriteYaml( + ResPath path, + YamlDocument yaml, + CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + var memStream = new MemoryStream(); + using var writer = new StreamWriter(memStream); + var yamlStream = new YamlStream {yaml}; + yamlStream.Save(new YamlMappingFix(new Emitter(writer)), false); + writer.Flush(); + WriteBytes(path, memStream.AsMemory(), compressionLevel); + } } /// diff --git a/Robust.Shared/Replays/SharedReplayRecordingManager.Write.cs b/Robust.Shared/Replays/SharedReplayRecordingManager.Write.cs index cc3a12b0c..00dd9b50e 100644 --- a/Robust.Shared/Replays/SharedReplayRecordingManager.Write.cs +++ b/Robust.Shared/Replays/SharedReplayRecordingManager.Write.cs @@ -26,22 +26,30 @@ internal abstract partial class SharedReplayRecordingManager // and even then not for much longer than a couple hundred ms at most. private readonly List _finalizingWriteTasks = new(); - private void WriteYaml(RecordingState state, ResPath path, YamlDocument data) + private void WriteYaml( + RecordingState state, + ResPath path, + YamlDocument data, + CompressionLevel level = CompressionLevel.Optimal) { var memStream = new MemoryStream(); using var writer = new StreamWriter(memStream); var yamlStream = new YamlStream { data }; yamlStream.Save(new YamlMappingFix(new Emitter(writer)), false); writer.Flush(); - WriteBytes(state, path, memStream.AsMemory()); + WriteBytes(state, path, memStream.AsMemory(), level); } - private void WriteSerializer(RecordingState state, ResPath path, T obj) + private void WriteSerializer( + RecordingState state, + ResPath path, + T obj, + CompressionLevel level = CompressionLevel.Optimal) { var memStream = new MemoryStream(); _serializer.SerializeDirect(memStream, obj); - WriteBytes(state, path, memStream.AsMemory()); + WriteBytes(state, path, memStream.AsMemory(), level); } private void WritePooledBytes( diff --git a/Robust.Shared/Replays/SharedReplayRecordingManager.cs b/Robust.Shared/Replays/SharedReplayRecordingManager.cs index 656e9ff83..40cb32de0 100644 --- a/Robust.Shared/Replays/SharedReplayRecordingManager.cs +++ b/Robust.Shared/Replays/SharedReplayRecordingManager.cs @@ -375,6 +375,11 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM private void WriteFinalMetadata(RecordingState recState) { var yamlMetadata = new MappingDataNode(); + + // TODO REPLAYS + // Why are these separate events? + // I assume it was for backwards compatibility / avoiding breaking changes? + // But eventually RecordingStopped2 will probably be renamed and there'll just be more breaking changes. RecordingStopped?.Invoke(yamlMetadata); RecordingStopped2?.Invoke(new ReplayRecordingStopped { @@ -552,6 +557,12 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM manager.WriteBytes(state, path, bytes, compressionLevel); } + void IReplayFileWriter.WriteYaml(ResPath path, YamlDocument document, CompressionLevel compressionLevel) + { + CheckDisposed(); + manager.WriteYaml(state, path, document, compressionLevel); + } + private void CheckDisposed() { if (state.Done) diff --git a/Robust.Shared/RichText/FormattedString.cs b/Robust.Shared/RichText/FormattedString.cs new file mode 100644 index 000000000..fd929fc1d --- /dev/null +++ b/Robust.Shared/RichText/FormattedString.cs @@ -0,0 +1,146 @@ +using System; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Robust.Shared.RichText; + +/// +/// Contains a simple string of text formatted with markup tags. +/// +/// +/// +/// This type differs from by storing purely a markup string, +/// rather than a full parsed object model. +/// This makes it significantly more lightweight than , +/// and suitable for places where markup only has to be passed around, rather than modified or interpreted. +/// +/// +public struct FormattedString : IEquatable, ISelfSerialize +{ + // NOTE: This type has a custom network type serializer. + + /// + /// Represents an empty () string. + /// + public static readonly FormattedString Empty = new(""); + + /// + /// The contained markup text. + /// + /// + /// This must always be strict valid markup, i.e. parseable by . + /// + public readonly string Markup; + + [Obsolete("Do not construct FormattedString directly")] + public FormattedString() + { + throw new NotSupportedException("Do not construct FormattedString directly"); + } + + /// + /// Internal constructor, does not validate markup is strictly valid. + /// + /// + private FormattedString(string markup) + { + Markup = markup; + } + + /// + /// Create a from a strict markup string. + /// + /// + /// The provided markup string must be strict valid markup, + /// i.e. parseable by . + /// + /// + /// Thrown of is not strict valid markup. + /// + /// + public static FormattedString FromMarkup(string markup) + { + if (!FormattedMessage.ValidMarkup(markup)) + throw new ArgumentException("Invalid markup string"); + + return new FormattedString(markup); + } + + /// + /// Create a from a permissive markup string. + /// + /// + /// The provided markup string does not need to be strict valid markup, + /// but it will be normalized to be strict if it's not. + /// + /// + public static FormattedString FromMarkupPermissive(string markup) + { + // We round trip here to ensure the contents are valid. + var permissive = FormattedMessage.FromMarkupPermissive(markup); + return (FormattedString)permissive; + } + + /// + /// Create a from plaintext (escaping it if necessary). + /// + /// + /// This is equivalent to + /// + public static FormattedString FromPlainText(string plainText) + { + return new FormattedString(FormattedMessage.EscapeText(plainText)); + } + + public static explicit operator FormattedString(FormattedMessage message) + { + // Assumed to be valid markup returned by ToMarkup(). + return new FormattedString(message.ToMarkup()); + } + + public static explicit operator FormattedMessage(FormattedString str) + { + // This should never throw. + return FormattedMessage.FromMarkupOrThrow(str.Markup); + } + + public static explicit operator string(FormattedString str) + { + return str.Markup; + } + + public readonly bool Equals(FormattedString other) + { + return other.Markup == Markup; + } + + public readonly override bool Equals(object? obj) + { + return obj is FormattedString other && Equals(other); + } + + public readonly override int GetHashCode() + { + return Markup.GetHashCode(); + } + + public static bool operator ==(FormattedString left, FormattedString right) + { + return left.Equals(right); + } + + public static bool operator !=(FormattedString left, FormattedString right) + { + return !left.Equals(right); + } + + void ISelfSerialize.Deserialize(string value) + { + this = FromMarkup(value); + } + + readonly string ISelfSerialize.Serialize() + { + return Markup; + } +} diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index c30d5e050..424cc81c6 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -16,7 +16,7 @@ - + diff --git a/Robust.Shared/Sandboxing/SandboxHelper.cs b/Robust.Shared/Sandboxing/SandboxHelper.cs index d30aa104b..8906586b7 100644 --- a/Robust.Shared/Sandboxing/SandboxHelper.cs +++ b/Robust.Shared/Sandboxing/SandboxHelper.cs @@ -4,15 +4,16 @@ using Robust.Shared.IoC; namespace Robust.Shared.Sandboxing { + [NotContentImplementable] public interface ISandboxHelper { /// /// Effectively equivalent to but safe for content use. /// /// - /// Thrown if is not defined in content. + /// Thrown if is not defined in content. /// - /// + /// object CreateInstance(Type type); } diff --git a/Robust.Shared/Serialization/IRobustSerializer.cs b/Robust.Shared/Serialization/IRobustSerializer.cs index 0a603ef1d..ed1039f51 100644 --- a/Robust.Shared/Serialization/IRobustSerializer.cs +++ b/Robust.Shared/Serialization/IRobustSerializer.cs @@ -6,6 +6,7 @@ using Robust.Shared.Network; namespace Robust.Shared.Serialization { + [NotContentImplementable] public interface IRobustSerializer { void Initialize(); diff --git a/Robust.Shared/Serialization/ISelfSerialize.cs b/Robust.Shared/Serialization/ISelfSerialize.cs index 38d7ffa13..670cdbf98 100644 --- a/Robust.Shared/Serialization/ISelfSerialize.cs +++ b/Robust.Shared/Serialization/ISelfSerialize.cs @@ -1,9 +1,27 @@ +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + namespace Robust.Shared.Serialization { + /// + /// Allows a type with an argument-free constructor (i.e. new()) to provide its own serializer and + /// deserializer in place from a string, without deferring to an . + /// + /// + /// This is much more limited than a full serializer, and only allows working with a scalar, but may be + /// convenient. + /// public interface ISelfSerialize { + /// + /// Deserialize the type from a given string value, after having already constructed one with new(). + /// + /// The scalar to deserialize from. void Deserialize(string value); + /// + /// Serialize the type to a yaml scalar (i.e. string). + /// + /// The serialized representation of the data. string Serialize(); } } diff --git a/Robust.Shared/Serialization/ISerializationHooks.cs b/Robust.Shared/Serialization/ISerializationHooks.cs index 9cca8c35f..88f0223a3 100644 --- a/Robust.Shared/Serialization/ISerializationHooks.cs +++ b/Robust.Shared/Serialization/ISerializationHooks.cs @@ -1,13 +1,13 @@ namespace Robust.Shared.Serialization; /// -/// Provides a method that gets executed after deserialization is complete and a method that gets executed before serialization +/// Provides a method that gets executed after deserialization is complete. /// [RequiresExplicitImplementation] public interface ISerializationHooks { /// - /// Gets executed after deserialization is complete + /// Gets executed after deserialization is complete /// void AfterDeserialization() {} } diff --git a/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs index 04cd13e1f..6e499c88f 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/AlwaysPushInheritanceAttribute.cs @@ -2,10 +2,33 @@ using System; namespace Robust.Shared.Serialization.Manager.Attributes { - // TODO Serialization: find a way to constrain this to DataFields only & make exclusive w/ NeverPush /// - /// Adds the parent DataDefinition field to this field. + /// When inheriting a field from a parent, always merge the two fields when such behavior exists. + /// This is unlike the normal behavior where the child's field will always overwrite the parent's. + ///
+ /// Merging is done at a YAML level by merging mappings and sequences recursively. ///
+ /// + /// + /// - id: Parent + /// myField: [Foo, Bar] + ///
+ /// - id: Child + /// parents: [Parent] + /// myField: [Baz, Qux] + ///
+ /// Which, when deserialized and assuming myField is marked with AlwaysPushInheritance, will result in data that + /// looks like this: + /// + /// - id: Child + /// myField: [Foo, Bar, Baz, Qux] + /// + /// compared to the default behavior: + /// + /// - id: Child + /// myField: [Baz, Qux] + /// + ///
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class AlwaysPushInheritanceAttribute : Attribute { diff --git a/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs index cd99ce666..c4ee44466 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/CopyByRefAttribute.cs @@ -7,49 +7,49 @@ namespace Robust.Shared.Serialization.Manager.Attributes; /// and . /// This means that the source instance is returned directly. /// This attribute is not inherited. -/// -/// Note that when calling any of the generic and -/// methods, this attribute will only be respected -/// if the generic parameter passed to the copying methods has this attribute. -/// For example, if a copy method is called with a generic parameter T that is not annotated with this attribute, -/// but the actual type of the source parameter is annotated with this attribute, it will not be copied by ref. -/// Conversely, if the generic parameter T is annotated with this attribute, but the actual type of the source -/// is an inheritor which is not annotated with this attribute, it will still be copied by ref. -/// If the generic parameter T is a type derived from another that is annotated with the attribute, -/// but it itself is not annotated with this attribute, source will not be copied by ref as this attribute -/// is not inherited. -/// -/// public class A {} -/// -/// [CopyByRef] -/// public class B : A {} -/// -/// public class C : B {} -/// -/// public class Copier(ISerializationManager manager) -/// { -/// var a = new A(); -/// var b = new B(); -/// var c = new C(); -/// -/// // false, not copied by ref -/// manager.CreateCopy(a) == a -/// -/// // false, not copied by ref -/// manager.CreateCopy<A>(b) == b -/// -/// // true, copied by ref -/// manager.CreateCopy(b) == b -/// -/// // false, not copied by ref -/// manager.CreateCopy(c) == c -/// -/// // true, copied by ref -/// manager.CreateCopy<B>(c) == c -/// } -/// -/// ///
+/// +/// Note that when calling any of the generic and +/// methods, this attribute will only be respected +/// if the generic parameter passed to the copying methods has this attribute. +/// For example, if a copy method is called with a generic parameter T that is not annotated with this attribute, +/// but the actual type of the source parameter is annotated with this attribute, it will not be copied by ref. +/// Conversely, if the generic parameter T is annotated with this attribute, but the actual type of the source +/// is an inheritor which is not annotated with this attribute, it will still be copied by ref. +/// If the generic parameter T is a type derived from another that is annotated with the attribute, +/// but it itself is not annotated with this attribute, source will not be copied by ref as this attribute +/// is not inherited. +/// +/// public class A {} +///
+/// [CopyByRef] +/// public class B : A {} +///
+/// public class C : B {} +///
+/// public class Copier(ISerializationManager manager) +/// { +/// var a = new A(); +/// var b = new B(); +/// var c = new C(); +///
+/// // false, not copied by ref +/// manager.CreateCopy(a) == a +///
+/// // false, not copied by ref +/// manager.CreateCopy<A>(b) == b +///
+/// // true, copied by ref +/// manager.CreateCopy(b) == b +///
+/// // false, not copied by ref +/// manager.CreateCopy(c) == c +///
+/// // true, copied by ref +/// manager.CreateCopy<B>(c) == c +/// } +///
+///
[AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct | diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataDefinitionAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataDefinitionAttribute.cs index 2df38bc55..fe2d8c0c8 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/DataDefinitionAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/DataDefinitionAttribute.cs @@ -3,6 +3,13 @@ using JetBrains.Annotations; namespace Robust.Shared.Serialization.Manager.Attributes; +/// +/// Marks this type as being data-serializable. +/// +/// +/// +/// +/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] [MeansDataDefinition] [MeansImplicitUse] diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs index fcde5823d..c2702a523 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/DataFieldAttribute.cs @@ -1,10 +1,20 @@ using System; #if !ROBUST_ANALYZERS_TEST +using Robust.Shared.ViewVariables; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; using JetBrains.Annotations; #endif namespace Robust.Shared.Serialization.Manager.Attributes { + /// + /// Marks a field or property as being serializable/deserializable, also implying + /// with ReadWrite permissions. + /// + /// + /// + /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] #if !ROBUST_ANALYZERS_TEST [MeansImplicitAssignment] @@ -20,12 +30,18 @@ namespace Robust.Shared.Serialization.Manager.Attributes public string? Tag { get; internal set; } /// - /// Whether or not this field being mapped is required for the component to function. + /// Whether this field being mapped is required for the object to successfully deserialize. /// This will not guarantee that the field is mapped when the program is run, /// it is meant to be used as metadata information. /// public readonly bool Required; + /// See . + /// See . + /// See . + /// See . + /// See . + /// See . public DataFieldAttribute(string? tag = null, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false, Type? customTypeSerializer = null) : base(readOnly, priority, serverOnly, customTypeSerializer) { Tag = tag; @@ -40,11 +56,60 @@ namespace Robust.Shared.Serialization.Manager.Attributes public abstract class DataFieldBaseAttribute : Attribute { + /// + /// Defines an order datafields should be deserialized in. You rarely if ever need this functionality, and + /// it has no effect on serialization. + /// + /// + /// + /// [DataDefinition] + /// public class TestClass + /// { + /// // This field is decoded first, as it has the highest priority. + /// [DataField(priority: 3)] + /// public FooData First = new(); + ///
+ /// // This field has the default priority of 0, and is in the middle. + /// [DataField] + /// public bool Middle = false; + ///
+ /// // This field has the lowest priority, so it's dead last. + /// [DataField(priority: -999)] + /// public int DeadLast = 0; + /// } + ///
+ ///
public readonly int Priority; + + /// + /// A specific implementation to use for parsing this type. + /// This allows you to provide custom yaml parsing logic for a field, an example of this in regular use is + /// . + /// public readonly Type? CustomTypeSerializer; + + /// + /// Marks the datafield as only ever being read/deserialized from YAML, it will never be + /// written/saved. + /// + /// This is useful for data that is only ever used during, say, entity setup, and shouldn't be kept in live + /// entities nor saved for them. + /// public readonly bool ReadOnly; + + /// + /// Marks the datafield as server only, indicating to client code that it should not attempt to read or + /// write this field because it may not understand the contained data. + /// + /// This is useful for working with types that only exist on the server in otherwise shared data like a + /// shared prototype. + /// public readonly bool ServerOnly; + /// See . + /// See . + /// See . + /// See . protected DataFieldBaseAttribute(bool readOnly = false, int priority = 1, bool serverOnly = false, Type? customTypeSerializer = null) { ReadOnly = readOnly; diff --git a/Robust.Shared/Serialization/Manager/Attributes/DataRecordAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/DataRecordAttribute.cs index e9357bef1..e8a8cc64b 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/DataRecordAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/DataRecordAttribute.cs @@ -4,9 +4,22 @@ using JetBrains.Annotations; namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// Makes all properties in a record data fields with camel case naming. -/// +/// Marks this type as being data-serializable and automatically marks all properties as data fields. /// +/// +/// +/// +/// [DataRecord] +/// public sealed record MyRecord(int Foo, bool Bar); +/// +/// which has the serialized yaml equivalent of: +/// +/// foo: 0 +/// bar: false +/// +/// +/// +/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] [MeansDataDefinition] [MeansDataRecord] diff --git a/Robust.Shared/Serialization/Manager/Attributes/Docs.xml b/Robust.Shared/Serialization/Manager/Attributes/Docs.xml new file mode 100644 index 000000000..8069e1346 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Attributes/Docs.xml @@ -0,0 +1,56 @@ + + + + + + Data-serializable types must be . They can be serialized/deserialized with . + Properties to be serialized should be annotated with , though this is automatic for . + + + This also allows this type to be used from !type:name annotations in YAML if its name is unique. + Fields not marked with or its + relatives are never serialized. + + + Has no relation to , which is only used with + in RobustToolbox games. + + + + + + Also implies , you don't + need to specify it yourself. + + + + + Also implies , you don't + need to specify it yourself. + + + + + Starting with a definition in C#: + + [DataDefinition] // Mark our type as being serializable by ISerializationManager. + public partial class MyData + { + // Mark this field as being a data field, it'll be named "enabled" implicitly as we + // didn't specify any names. + // If a field is not required, it is best practice to specify a default value. + [DataField] + public bool Enabled = true; +
+ [DataField(required: true)] + public int Counter; + } +
+ This definition describes a YAML schema, which when serialized could look like this: + + enabled: false + counter: 3 + +
+
+
diff --git a/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs index 482fa97c1..f1e906557 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataDefinitionForInheritorsAttribute.cs @@ -2,6 +2,30 @@ using System; namespace Robust.Shared.Serialization.Manager.Attributes { + /// + /// Marks all classes or interfaces that inherit from the one with this attribute with + /// , without requiring this be done manually. + /// Cannot be reversed by inheritors! + /// + /// + /// + /// [ImplicitDataDefinitionForInheritors] + /// public abstract class BaseClass + /// { + /// [DataField] + /// public bool Enabled; + /// } + ///
+ /// // Not only do we not need to mark this as a data definition, + /// // we inherit our fields from our parent class as normal and can add our own fields. + /// public sealed class MyClass : BaseClass + /// { + /// [DataField] + /// public int Counter; + /// } + ///
+ ///
+ /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class ImplicitDataDefinitionForInheritorsAttribute : Attribute { diff --git a/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataRecordAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataRecordAttribute.cs index f5ad94f45..e8fb1359a 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataRecordAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/ImplicitDataRecordAttribute.cs @@ -3,9 +3,11 @@ namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// Makes any inheritors data records. -/// +/// Marks all classes or interfaces that inherit from the one with this attribute with +/// , without requiring this be done manually. +/// Cannot be reversed by inheritors! /// +/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class ImplicitDataRecordAttribute : Attribute { diff --git a/Robust.Shared/Serialization/Manager/Attributes/IncludeDataFieldAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/IncludeDataFieldAttribute.cs index de0846619..844dc3d61 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/IncludeDataFieldAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/IncludeDataFieldAttribute.cs @@ -4,23 +4,42 @@ using JetBrains.Annotations; namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// Inlines the datafield instead of putting it into its own node. +/// Marks a field or property as being serializable/deserializable, including all of the fields from its type into +/// the current data definition. Does not create a named field of its own, and should be used sparingly for +/// conciseness and readability only. /// /// -/// mapping: -/// data1: 0 -/// data2: 0 -/// Becomes -/// data1: 0 -/// data2: 0 +/// This should never be used in a way where the included/collapsed fields conflict in name with other fields! /// +/// +/// +/// otherField: 42 +/// myField: +/// subfield1: foo +/// subfield2: bar +/// +/// becomes +/// +/// otherField: 42 +/// subfield1: foo +/// subfield2: bar +/// +/// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] [MeansImplicitAssignment] [MeansImplicitUse(ImplicitUseKindFlags.Assign)] public sealed class IncludeDataFieldAttribute : DataFieldBaseAttribute { - public IncludeDataFieldAttribute(bool readOnly = false, int priority = 1, bool serverOnly = false, - Type? customTypeSerializer = null) : base(readOnly, priority, serverOnly, customTypeSerializer) + /// See . + /// See . + /// See . + /// See . + public IncludeDataFieldAttribute( + bool readOnly = false, + int priority = 1, + bool serverOnly = false, + Type? customTypeSerializer = null + ) : base(readOnly, priority, serverOnly, customTypeSerializer) { } diff --git a/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinitionAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinitionAttribute.cs index f73d772ab..cf728161a 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinitionAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/MeansDataDefinitionAttribute.cs @@ -3,6 +3,11 @@ using JetBrains.Annotations; namespace Robust.Shared.Serialization.Manager.Attributes { + /// + /// Marks an attribute class as implying . + /// + /// + /// [BaseTypeRequired(typeof(Attribute))] [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class MeansDataDefinitionAttribute : Attribute diff --git a/Robust.Shared/Serialization/Manager/Attributes/MeansDataRecordAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/MeansDataRecordAttribute.cs index 922838c0c..68b8c3c00 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/MeansDataRecordAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/MeansDataRecordAttribute.cs @@ -4,8 +4,9 @@ using JetBrains.Annotations; namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// +/// Marks an attribute class as implying . /// +/// [BaseTypeRequired(typeof(Attribute))] [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class MeansDataRecordAttribute : Attribute diff --git a/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs index 7c714865e..303a29e51 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/NeverPushInheritanceAttribute.cs @@ -1,8 +1,15 @@ using System; +using Robust.Shared.Prototypes; namespace Robust.Shared.Serialization.Manager.Attributes { - // TODO Serialization: find a way to constrain this to DataField only & make exclusive w/ AlwaysPush + /// + /// When added to a DataField, this makes it so that the value of the field + /// is never given to a child when inheriting. For example with prototypes, this means the value of the + /// field will not be given to a child if it inherits from a parent with the field set. This is useful for + /// things like where you do not want abstract-ness to pass on to the + /// child. + /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class NeverPushInheritanceAttribute : Attribute { diff --git a/Robust.Shared/Serialization/Manager/Attributes/NotYamlSerializableAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/NotYamlSerializableAttribute.cs index 414e954b9..ed536ee65 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/NotYamlSerializableAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/NotYamlSerializableAttribute.cs @@ -3,7 +3,8 @@ using System; namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// Used to denote that a type is not serializable to yaml, and should not be used as a data-field. +/// Used to denote that a type is not serializable to yaml, and should not be used as a data-field. +/// Types marked with this will cause an error early in serialization init if used as a field. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] internal sealed class NotYamlSerializableAttribute : Attribute; diff --git a/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs index 14f144cf4..fb01cd2ed 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/TypeSerializerAttribute.cs @@ -1,10 +1,13 @@ using System; using JetBrains.Annotations; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.Shared.Serialization.Manager.Attributes { /// - /// Registers a as a default serializer for its type + /// Registers a as a default serializer for its type. + /// This should only be used for serializers that should always be used unconditionally. If you're making a + /// custom serializer, do not apply this attribute. /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] [MeansImplicitUse] diff --git a/Robust.Shared/Serialization/Manager/Attributes/ValidatePrototypeIdAttribute.cs b/Robust.Shared/Serialization/Manager/Attributes/ValidatePrototypeIdAttribute.cs index 1ff17a62d..6fd07cafb 100644 --- a/Robust.Shared/Serialization/Manager/Attributes/ValidatePrototypeIdAttribute.cs +++ b/Robust.Shared/Serialization/Manager/Attributes/ValidatePrototypeIdAttribute.cs @@ -4,9 +4,9 @@ using Robust.Shared.Prototypes; namespace Robust.Shared.Serialization.Manager.Attributes; /// -/// This attribute should be used on static string or string collection fields to validate that they correspond to -/// valid YAML prototype ids. This attribute is not required for static and -/// fields, as they automatically get validated. +/// This attribute should be used on static string or string collection fields to validate that they correspond to +/// valid YAML prototype ids. This attribute is not required for static and +/// fields, as they automatically get validated. /// [Obsolete("Use a static readonly ProtoId instead")] [AttributeUsage(AttributeTargets.Field)] diff --git a/Robust.Shared/Serialization/Manager/ISerializationManager.cs b/Robust.Shared/Serialization/Manager/ISerializationManager.cs index 0b7ba2efe..4abe1d444 100644 --- a/Robust.Shared/Serialization/Manager/ISerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/ISerializationManager.cs @@ -9,6 +9,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.Shared.Serialization.Manager { + [NotContentImplementable] public interface ISerializationManager { public delegate T InstantiationDelegate(); diff --git a/Robust.Shared/Serialization/Markdown/DataNodeParser.cs b/Robust.Shared/Serialization/Markdown/DataNodeParser.cs index d7c0460d8..83595daac 100644 --- a/Robust.Shared/Serialization/Markdown/DataNodeParser.cs +++ b/Robust.Shared/Serialization/Markdown/DataNodeParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using Robust.Shared.Collections; using Robust.Shared.Serialization.Markdown.Mapping; @@ -17,22 +18,31 @@ public static class DataNodeParser { public static IEnumerable ParseYamlStream(TextReader reader) { - return ParseYamlStream(new Parser(reader)); + return ParseYamlStream(reader, internStrings: false); } - internal static IEnumerable ParseYamlStream(Parser parser) + internal static IEnumerable ParseYamlStream(TextReader reader, bool internStrings) { + return ParseYamlStream(new Parser(reader), internStrings); + } + + internal static IEnumerable ParseYamlStream(Parser parser, bool internStrings = false) + { + var state = new ParserState(internStrings); + parser.Consume(); while (!parser.TryConsume(out _)) { - yield return ParseDocument(parser); + yield return ParseDocument(parser, state); } + + // System.Console.WriteLine(state.TotalStringsSaved); } - private static DataNodeDocument ParseDocument(Parser parser) + private static DataNodeDocument ParseDocument(Parser parser, ParserState parserState) { - var state = new DocumentState(); + var state = new DocumentState(parserState); parser.Consume(); @@ -78,7 +88,11 @@ public static class DataNodeParser private static ValueDataNode ParseValue(Parser parser, DocumentState state) { var ev = parser.Consume(); - var node = new ValueDataNode(ev){Tag = ConvertTag(ev.Tag)}; + var node = new ValueDataNode(ev) + { + Tag = ConvertTag(ev.Tag, state.ParserState), + Value = state.ParserState.InternString(ev.Value) + }; NodeParsed(node, ev, false, state); @@ -100,7 +114,7 @@ public static class DataNodeParser var ev = parser.Consume(); var node = new SequenceDataNode(); - node.Tag = ConvertTag(ev.Tag); + node.Tag = ConvertTag(ev.Tag, state.ParserState); node.Start = ev.Start; var unresolvedAlias = false; @@ -127,14 +141,14 @@ public static class DataNodeParser var ev = parser.Consume(); var node = new MappingDataNode(); - node.Tag = ConvertTag(ev.Tag); + node.Tag = ConvertTag(ev.Tag, state.ParserState); var unresolvedAlias = false; MappingEnd mapEnd; while (!parser.TryConsume(out mapEnd)) { - var key = ParseKey(parser); + var key = state.ParserState.InternString(ParseKey(parser)); var value = Parse(parser, state); node.Add(key, value); @@ -218,13 +232,14 @@ public static class DataNodeParser return node; } - private static string ConvertTag(TagName tag) + private static string ConvertTag(TagName tag, ParserState state) { - return (tag.IsNonSpecific || tag.IsEmpty) ? null : tag.Value; + return (tag.IsNonSpecific || tag.IsEmpty) ? null : state.InternString(tag.Value); } - private sealed class DocumentState + private sealed class DocumentState(ParserState parserState) { + public readonly ParserState ParserState = parserState; public readonly Dictionary Anchors = new(); public ValueList UnresolvedAliasOwners; } @@ -256,6 +271,37 @@ public static class DataNodeParser throw new NotSupportedException(); } } + +#nullable enable + + private sealed class ParserState(bool internStrings) + { + public readonly HashSet? StringInternIndex = internStrings ? [] : null; + //public int TotalStringsSaved = 0; + + [return: NotNullIfNotNull(nameof(str))] + public string? InternString(string? str) + { + if (StringInternIndex == null) + return str; + + if (str == null) + return null; + + // Use a basic string interning system to avoid releasing a bunch of equivalent strings. + // This avoids having thousands of identical strings for stuff like "type" in prototypes stored in memory. + if (StringInternIndex.TryGetValue(str, out var indexedString)) + { + // if (!ReferenceEquals(str, indexedString)) + // TotalStringsSaved += 1; + + return indexedString; + } + + StringInternIndex.Add(str); + return str; + } + } } public sealed class DataParseException : Exception diff --git a/Robust.Shared/Serialization/NetBitArraySerializer.cs b/Robust.Shared/Serialization/NetBitArraySerializer.cs new file mode 100644 index 000000000..9997c67fc --- /dev/null +++ b/Robust.Shared/Serialization/NetBitArraySerializer.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Serialization; +using JetBrains.Annotations; +using NetSerializer; + +namespace Robust.Shared.Serialization; + +/// +/// Custom serializer implementation for . +/// +/// +/// +/// This type is necessary as, since .NET 10, the internal layout of was changed. +/// The type now (internally) implements for backwards compatibility with existing +/// BinaryFormatter code, but NetSerializer does not support . +/// +/// +/// This code is designed to be backportable & network compatible with the previous behavior on .NET 9. +/// +/// +internal sealed class NetBitArraySerializer : IDynamicTypeSerializer +{ + // NOTE: MUST be a IDynamicTypeSerializer for compatibility! + // Can be changed in the future. + + // For reference, the layout of BitArray before .NET 10 was: + // private int[] m_array; + // private int m_length; + // private int _version; + // NetSerializer serialized these in the following order (sorted by name): + // _version, m_array, m_length + + public bool Handles(Type type) + { + return type == typeof(BitArray); + } + + public IEnumerable GetSubtypes(Type type) + { + return [typeof(int[]), typeof(int)]; + } + + public void GenerateWriterMethod(Serializer serializer, Type type, ILGenerator il) + { + var method = typeof(NetBitArraySerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!; + + // arg0: Serializer, arg1: Stream, arg2: value + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + + il.EmitCall(OpCodes.Call, method, null); + + il.Emit(OpCodes.Ret); + } + + public void GenerateReaderMethod(Serializer serializer, Type type, ILGenerator il) + { + var method = typeof(NetBitArraySerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!; + + // arg0: Serializer, arg1: stream, arg2: out value + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + + il.EmitCall(OpCodes.Call, method, null); + + il.Emit(OpCodes.Ret); + } + + [UsedImplicitly] + private static void Write(Serializer serializer, Stream stream, BitArray value) + { + var intCount = (31 + value.Length) >> 5; + var ints = new int[intCount]; + value.CopyTo(ints, 0); + + serializer.SerializeDirect(stream, 0); // _version + serializer.SerializeDirect(stream, ints); // m_array + serializer.SerializeDirect(stream, value.Length); // m_length + } + + [UsedImplicitly] + private static void Read(Serializer serializer, Stream stream, out BitArray value) + { + serializer.DeserializeDirect(stream, out _); // _version + serializer.DeserializeDirect(stream, out var array); // m_array + serializer.DeserializeDirect(stream, out var length); // m_length + + value = new BitArray(array) + { + Length = length + }; + } +} diff --git a/Robust.Shared/Serialization/NetFormattedStringSerializer.cs b/Robust.Shared/Serialization/NetFormattedStringSerializer.cs new file mode 100644 index 000000000..24a45b63e --- /dev/null +++ b/Robust.Shared/Serialization/NetFormattedStringSerializer.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using JetBrains.Annotations; +using NetSerializer; +using Robust.Shared.RichText; + +namespace Robust.Shared.Serialization; + +/// +/// Special network serializer for to make sure validation runs for network values. +/// +internal sealed class NetFormattedStringSerializer : IStaticTypeSerializer +{ + public bool Handles(Type type) + { + return type == typeof(FormattedString); + } + + public IEnumerable GetSubtypes(Type type) + { + return [typeof(string)]; + } + + public MethodInfo GetStaticWriter(Type type) + { + return typeof(NetFormattedStringSerializer).GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic)!; + } + + public MethodInfo GetStaticReader(Type type) + { + return typeof(NetFormattedStringSerializer).GetMethod("Read", BindingFlags.Static | BindingFlags.NonPublic)!; + } + + [UsedImplicitly] + private static void Write(Stream stream, FormattedString value) + { + Primitives.WritePrimitive(stream, value.Markup); + } + + [UsedImplicitly] + private static void Read(Stream stream, out FormattedString value) + { + Primitives.ReadPrimitive(stream, out string markup); + + // Must be valid formed strict markup, do not trust the client! + value = FormattedString.FromMarkup(markup); + } +} diff --git a/Robust.Shared/Serialization/RobustMappedStringSerializer.cs b/Robust.Shared/Serialization/RobustMappedStringSerializer.cs index 14a73c8fc..19d9b6510 100644 --- a/Robust.Shared/Serialization/RobustMappedStringSerializer.cs +++ b/Robust.Shared/Serialization/RobustMappedStringSerializer.cs @@ -295,7 +295,7 @@ namespace Robust.Shared.Serialization _stringMapHash = _serverHash; - LogSzr.Debug($"Locked in at {_dict.StringCount} mapped strings."); + LogSzr.Debug($"Locked in at {_dict.StringCount} mapped strings ({ByteHelpers.FormatBytes(msg.Package!.Length)})."); packageStream.Position = 0; if (EnableCaching) @@ -439,7 +439,7 @@ namespace Robust.Shared.Serialization _stringMapHash = msgMapStr.Hash!; LogSzr.Debug($"Read {added} strings from cache {hashStr}."); - LogSzr.Debug($"Locked in at {_dict.StringCount} mapped strings."); + LogSzr.Debug($"Locked in at {_dict.StringCount} mapped strings ({ByteHelpers.FormatBytes(file.Length)})."); // ok we're good now var channel = msgMapStr.MsgChannel; OnClientCompleteHandshake(_net, channel); diff --git a/Robust.Shared/Serialization/RobustSerializer.cs b/Robust.Shared/Serialization/RobustSerializer.cs index 893096dc7..ba8cf9af5 100644 --- a/Robust.Shared/Serialization/RobustSerializer.cs +++ b/Robust.Shared/Serialization/RobustSerializer.cs @@ -89,7 +89,9 @@ namespace Robust.Shared.Serialization CustomTypeSerializers = new[] { MappedStringSerializer.TypeSerializer, - new NetMathSerializer() + new NetMathSerializer(), + new NetBitArraySerializer(), + new NetFormattedStringSerializer() } }; _serializer = new Serializer(types, settings); diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs index cc82c0138..66533f6cb 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs @@ -9,7 +9,8 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; /// -/// Simple string serializer that just validates that strings correspond to valid component names +/// Simple string serializer that just validates that strings correspond to valid component names. +/// This will not fail when it encounters explicitly ignored components. /// public sealed class ComponentNameSerializer : ITypeSerializer { @@ -17,7 +18,7 @@ public sealed class ComponentNameSerializer : ITypeSerializer(); - if (!factory.TryGetRegistration(node.Value, out _)) + if (!factory.TryGetRegistration(node.Value, out _) && factory.GetComponentAvailability(node.Value) != ComponentAvailability.Ignore) return new ErrorNode(node, $"Unknown component kind: {node.Value}"); return new ValidatedValueNode(node); diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ConstantSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ConstantSerializer.cs index ae495f48e..4cc2564e8 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ConstantSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ConstantSerializer.cs @@ -8,6 +8,22 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom { + /// + /// Serializes or deserializes an integer from a set of known constants specified by an enum. + /// This is very niche in utility, for integer fields where yaml should only ever be using named + /// shorthands. + /// + /// + /// + /// public enum MyConstants { + /// Foo = 1, + /// Bar = 2, + /// Life = 42, + /// } + /// + /// Using this serializer, an integer field can then be deserialized from, say, "Life" and correctly + /// be set to the value 42. + /// public sealed class ConstantSerializer : ITypeSerializer { public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/FlagSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/FlagSerializer.cs index a1dd23f86..01f0a1bfb 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/FlagSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/FlagSerializer.cs @@ -9,6 +9,24 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces; namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom { + /// + /// Serializes or deserializes marked enums, allowing you to specify a list of + /// flags instead of writing magic numbers. + /// + /// + /// + /// [Flags] + /// public enum TestFlags { + /// Foo = 1, + /// Bar = 2, + /// Baz = 4, + /// } + /// + /// which, using this serializer, can be deserialized from + /// + /// [Foo, Baz] + /// + /// public sealed class FlagSerializer : ITypeSerializer, ITypeReader, ITypeCopyCreator { public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomArraySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomArraySerializer.cs new file mode 100644 index 000000000..15e2f4911 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomArraySerializer.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +/// +/// This is a variant of the normal array serializer that uses a custom type serializer to handle the values. +/// +public sealed class CustomArraySerializer : ITypeSerializer + where TCustomSerializer : ITypeSerializer +{ + T[] ITypeReader.Read( + ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context, + ISerializationManager.InstantiationDelegate? instanceProvider) + { + var list = new T[node.Count]; + var i = 0; + foreach (var dataNode in node) + { + list[i++] = serializationManager.Read((ValueDataNode)dataNode, hookCtx, context); + } + + return list; + } + + ValidationNode ITypeValidator.Validate( + ISerializationManager seri, + SequenceDataNode node, + IDependencyCollection deps, + ISerializationContext? ctx) + { + var list = new List(node.Count); + foreach (var elem in node) + { + list.Add(seri.ValidateNode((ValueDataNode)elem, ctx)); + } + return new ValidatedSequenceNode(list); + } + + public DataNode Write( + ISerializationManager seri, + T[] value, + IDependencyCollection deps, + bool alwaysWrite = false, + ISerializationContext? ctx = null) + { + var sequence = new SequenceDataNode(); + foreach (var elem in value) + { + sequence.Add(seri.WriteValue(elem, alwaysWrite, ctx)); + } + return sequence; + } +} + diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomListSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomListSerializer.cs new file mode 100644 index 000000000..fb14578d1 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Generic/CustomListSerializer.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +/// +/// This is a variation of the that uses a custom type serializer to handle the values. +/// +public sealed class CustomListSerializer + : ITypeSerializer, SequenceDataNode> + where TCustomSerializer : ITypeSerializer +{ + List ITypeReader, SequenceDataNode>.Read( + ISerializationManager seri, + SequenceDataNode node, + IDependencyCollection deps, + SerializationHookContext hookCtx, + ISerializationContext? ctx, + ISerializationManager.InstantiationDelegate>? instanceProvider) + { + var list = instanceProvider != null ? instanceProvider() : new(node.Count); + foreach (var dataNode in node) + { + var value = seri.Read((ValueDataNode)dataNode, hookCtx, ctx); + list.Add(value); + } + + return list; + } + + ValidationNode ITypeValidator, SequenceDataNode>.Validate( + ISerializationManager seri, + SequenceDataNode node, + IDependencyCollection deps, + ISerializationContext? ctx) + { + var list = new List(node.Count); + foreach (var elem in node) + { + list.Add(seri.ValidateNode((ValueDataNode)elem, ctx)); + } + return new ValidatedSequenceNode(list); + } + + public DataNode Write( + ISerializationManager seri, + List value, + IDependencyCollection deps, + bool alwaysWrite = false, + ISerializationContext? ctx = null) + { + var sequence = new SequenceDataNode(); + foreach (var elem in value) + { + sequence.Add(seri.WriteValue(elem, alwaysWrite, ctx)); + } + return sequence; + } +} + diff --git a/Robust.Shared/Threading/IParallelRobustJob.cs b/Robust.Shared/Threading/IParallelRobustJob.cs index 572448b72..6284eb077 100644 --- a/Robust.Shared/Threading/IParallelRobustJob.cs +++ b/Robust.Shared/Threading/IParallelRobustJob.cs @@ -1,16 +1,54 @@ namespace Robust.Shared.Threading; /// -/// Runs the job with the specified batch size per thread; Execute is still called per index. +/// Represents a generic parallel job that processes a range of indices. /// -public interface IParallelRobustJob +public interface IParallelRangeRobustJob { /// /// Minimum amount of batches required to engage in parallelism. + /// If the total number of batches is less than this, the job will run serially. /// int MinimumBatchParallel => 2; + /// + /// The amount of elements to process in each batch. + /// int BatchSize => 1; + /// + /// Processes a range of indices from startIndex to endIndex. + /// + /// The starting index of the range. + /// The ending index of the range. + void ExecuteRange(int startIndex, int endIndex); +} + +/// +/// Represents a parallel job that processes individual indices. +/// +public interface IParallelRobustJob : IParallelRangeRobustJob +{ + /// + /// Default implementation that executes the job for each index in the specified range. + /// + void IParallelRangeRobustJob.ExecuteRange(int startIndex, int endIndex) + { + for (var i = startIndex; i < endIndex; i++) + { + Execute(i); + } + } + + /// + /// Executes the job for the specified index. + /// + /// The index to process. void Execute(int index); } + +/// +/// Represents a parallel job that processes a bulk range of indices. +/// Good for jobs that can operate on ranges more efficiently (SIMD) than individual indices. +/// +public interface IParallelBulkRobustJob : IParallelRangeRobustJob; diff --git a/Robust.Shared/Threading/ParallelManager.cs b/Robust.Shared/Threading/ParallelManager.cs index c8388edda..fa69039de 100644 --- a/Robust.Shared/Threading/ParallelManager.cs +++ b/Robust.Shared/Threading/ParallelManager.cs @@ -7,6 +7,7 @@ using Robust.Shared.Log; namespace Robust.Shared.Threading; +[NotContentImplementable] public interface IParallelManager { event Action ParallelCountChanged; @@ -40,6 +41,28 @@ public interface IParallelManager /// Takes in a parallel job and runs it without blocking. ///
WaitHandle Process(IParallelRobustJob jobs, int amount); + + /// + /// Takes in a bulk parallel job and runs it the specified amount. + /// + /// The bulk parallel job to process. + /// The total number of elements to process. + void ProcessNow(IParallelBulkRobustJob jobs, int amount); + + /// + /// Processes a bulk robust job sequentially if desired. + /// + /// The bulk parallel job to process. + /// The total number of elements to process. + void ProcessSerialNow(IParallelBulkRobustJob jobs, int amount); + + /// + /// Takes in a bulk parallel job and runs it without blocking. + /// + /// The bulk parallel job to process. + /// The total number of elements to process. + /// A wait handle that signals when the job is complete. + WaitHandle Process(IParallelBulkRobustJob jobs, int amount); } internal interface IParallelManagerInternal : IParallelManager @@ -66,8 +89,8 @@ internal sealed class ParallelManager : IParallelManagerInternal private readonly ObjectPool _jobPool = new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024); - private readonly ObjectPool _parallelPool = - new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024); + private readonly ObjectPool _parallelPool = + new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024); /// /// Used internally for Parallel jobs, for external callers it gets garbage collected. @@ -95,7 +118,11 @@ internal sealed class ParallelManager : IParallelManagerInternal return robustJob; } - private InternalParallelJob GetParallelJob(IParallelRobustJob job, int start, int end, ParallelTracker tracker) + private InternalParallelRangeJob GetParallelJob( + IParallelRangeRobustJob job, + int start, + int end, + ParallelTracker tracker) { var internalJob = _parallelPool.Get(); internalJob.Set(_sawmill, job, start, end, tracker, _parallelPool); @@ -130,8 +157,25 @@ internal sealed class ParallelManager : IParallelManagerInternal job.Execute(); } - /// - public void ProcessNow(IParallelRobustJob job, int amount) + public void ProcessNow(IParallelRobustJob jobs, int amount) => + ProcessNow((IParallelRangeRobustJob) jobs, amount); + + public void ProcessNow(IParallelBulkRobustJob jobs, int amount) => + ProcessNow((IParallelRangeRobustJob) jobs, amount); + + public void ProcessSerialNow(IParallelRobustJob jobs, int amount) => + ProcessSerialNow((IParallelRangeRobustJob) jobs, amount); + + public void ProcessSerialNow(IParallelBulkRobustJob jobs, int amount) => + ProcessSerialNow((IParallelRangeRobustJob) jobs, amount); + + public WaitHandle Process(IParallelRobustJob jobs, int amount) => + Process((IParallelRangeRobustJob) jobs, amount); + + public WaitHandle Process(IParallelBulkRobustJob jobs, int amount) => + Process((IParallelRangeRobustJob) jobs, amount); + + public void ProcessNow(IParallelRangeRobustJob job, int amount) { var batches = amount / (float) job.BatchSize; @@ -147,17 +191,15 @@ internal sealed class ParallelManager : IParallelManagerInternal _trackerPool.Return(tracker); } - /// - public void ProcessSerialNow(IParallelRobustJob jobs, int amount) + public void ProcessSerialNow(IParallelRangeRobustJob jobs, int amount) { - for (var i = 0; i < amount; i++) - { - jobs.Execute(i); - } + if (amount <= 0) + return; + + jobs.ExecuteRange(0, amount); } - /// - public WaitHandle Process(IParallelRobustJob job, int amount) + public WaitHandle Process(IParallelRangeRobustJob job, int amount) { var tracker = InternalProcess(job, amount); return tracker.Event.WaitHandle; @@ -167,7 +209,7 @@ internal sealed class ParallelManager : IParallelManagerInternal /// Runs a parallel job internally. Used so we can pool the tracker task for ProcessParallelNow /// and not rely on external callers to return it where they don't want to wait. /// - private ParallelTracker InternalProcess(IParallelRobustJob job, int amount) + private ParallelTracker InternalProcess(IParallelRangeRobustJob job, int amount) { var batches = (int) MathF.Ceiling(amount / (float) job.BatchSize); var batchSize = job.BatchSize; @@ -236,27 +278,33 @@ internal sealed class ParallelManager : IParallelManagerInternal } /// - /// Runs an and handles cleanup. + /// Runs a for a specified range and handles cleanup. + /// This is so jobs that process per-element () + /// and jobs that process in bulk () can both use it. /// - private sealed class InternalParallelJob : IRobustJob, IThreadPoolWorkItem + private sealed class InternalParallelRangeJob : IRobustJob, IThreadPoolWorkItem { - private IParallelRobustJob _robust = default!; + private IParallelRangeRobustJob _robust = default!; private int _start; private int _end; private ISawmill _sawmill = default!; private ParallelTracker _tracker = default!; - private ObjectPool _parentPool = default!; + private ObjectPool _parentPool = default!; - public void Set(ISawmill sawmill, IParallelRobustJob robust, int start, int end, ParallelTracker tracker, ObjectPool parentPool) + public void Set( + ISawmill sawmill, + IParallelRangeRobustJob robust, + int start, + int end, + ParallelTracker tracker, + ObjectPool parentPool) { _sawmill = sawmill; - _robust = robust; _start = start; _end = end; - _tracker = tracker; _parentPool = parentPool; } @@ -265,10 +313,7 @@ internal sealed class ParallelManager : IParallelManagerInternal { try { - for (var i = _start; i < _end; i++) - { - _robust.Execute(i); - } + _robust.ExecuteRange(_start, _end); } catch (Exception exc) { @@ -276,7 +321,8 @@ internal sealed class ParallelManager : IParallelManagerInternal } finally { - // Set the event and return it to the pool for re-use. + // Task is done, so tell the tracker that it has one less task to process. + // And of course return the job to the pool. _tracker.Set(); _parentPool.Return(this); } diff --git a/Robust.Shared/Timing/FrameEventArgs.cs b/Robust.Shared/Timing/FrameEventArgs.cs index ea0bd5005..c314451db 100644 --- a/Robust.Shared/Timing/FrameEventArgs.cs +++ b/Robust.Shared/Timing/FrameEventArgs.cs @@ -1,4 +1,6 @@ -namespace Robust.Shared.Timing +using System; + +namespace Robust.Shared.Timing { /// /// Arguments of the GameLoop frame event. @@ -8,6 +10,10 @@ /// /// Seconds passed since this event was last called. /// + /// + /// Acceptable and simple to use for basic timing code, but accumulators/etc should prefer to use + /// s and to avoid loss of precision. + /// public float DeltaSeconds { get; } /// diff --git a/Robust.Shared/Timing/GameLoop.cs b/Robust.Shared/Timing/GameLoop.cs index 81131c534..64030fb6d 100644 --- a/Robust.Shared/Timing/GameLoop.cs +++ b/Robust.Shared/Timing/GameLoop.cs @@ -219,7 +219,7 @@ namespace Robust.Shared.Timing if (_timing.Paused) continue; - _timing.TickRemainder = accumulator; + _timing.TickRemainder = accumulator / _timing.TimeScale; countTicksRan += 1; // update the simulation @@ -282,7 +282,7 @@ namespace Robust.Shared.Timing // if not paused, save how close to the next tick we are so interpolation works if (!_timing.Paused) - _timing.TickRemainder = accumulator; + _timing.TickRemainder = accumulator / _timing.TimeScale; _timing.InSimulation = false; diff --git a/Robust.Shared/Timing/GameTick.cs b/Robust.Shared/Timing/GameTick.cs index 80411658c..e30d66c4b 100644 --- a/Robust.Shared/Timing/GameTick.cs +++ b/Robust.Shared/Timing/GameTick.cs @@ -4,8 +4,25 @@ using Robust.Shared.Serialization; namespace Robust.Shared.Timing { /// - /// Wraps a game tick value. + /// Represents a tick at some point in time over the game's runtime. + /// The actual span of time a tick is depends on the . /// + /// + /// + /// While the game does use ticks for some timing, they are always an arbitrary time step. If you need to + /// measure exact passage of time, you should use s instead in your reference frame + /// (client, server, etc.) from . + /// + /// + /// Ticks are appropriate for thinking purely relative to previous game ticks, for example tracking the last + /// time modification occurred on a component for networking purposes. + /// + /// + /// The game can theoretically run out of ticks. At the default tickrate, this is after approximately 4.5 years. + /// It is recommended to reboot the game before that happens. + /// + /// + /// [Serializable, NetSerializable] public readonly struct GameTick : IEquatable, IComparable { diff --git a/Robust.Shared/Timing/GameTiming.cs b/Robust.Shared/Timing/GameTiming.cs index 22fef70e3..6a451db62 100644 --- a/Robust.Shared/Timing/GameTiming.cs +++ b/Robust.Shared/Timing/GameTiming.cs @@ -137,6 +137,8 @@ namespace Robust.Shared.Timing set => SetTickRateAt(value, CurTick); } + public float TimeScale { get; set; } = 1; + /// /// The length of a tick at the current TickRate. 1/TickRate. /// @@ -156,13 +158,15 @@ namespace Robust.Shared.Timing } } + public TimeSpan TickRemainderRealtime => TickRemainder * TimeScale; + public TimeSpan CalcAdjustedTickPeriod() { // ranges from -1 to 1, with 0 being 'default' var ratio = MathHelper.Clamp(TickTimingAdjustment, -0.99f, 0.99f); // Final period ranges from near 0 (runs very fast to catch up) or 2 * tick period (runs at half speed). - return TickPeriod * (1-ratio); + return TickPeriod * (1-ratio) * TimeScale; } /// @@ -217,7 +221,7 @@ namespace Robust.Shared.Timing } /// - /// Resets the simulation time. + /// Resets the simulation time. /// public void ResetSimTime() { @@ -302,5 +306,10 @@ namespace Robust.Shared.Timing var variance = devSquared / (count - 1); return TimeSpan.FromTicks((long)Math.Sqrt(variance)); } + + internal static bool IsTimescaleValid(float scale) + { + return scale > 0 && float.IsNormal(scale) && float.IsFinite(scale); + } } } diff --git a/Robust.Shared/Timing/IGameTiming.cs b/Robust.Shared/Timing/IGameTiming.cs index e744a697b..a20b30c19 100644 --- a/Robust.Shared/Timing/IGameTiming.cs +++ b/Robust.Shared/Timing/IGameTiming.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; namespace Robust.Shared.Timing @@ -7,16 +8,22 @@ namespace Robust.Shared.Timing /// /// This holds main loop timing information and helper functions. /// + [NotContentImplementable] public interface IGameTiming { /// - /// Is program execution inside of the simulation, or rendering? + /// Is program execution inside the simulation, or outside of it (rendering, input handling, etc.) /// bool InSimulation { get; set; } /// /// Is the simulation currently paused? /// + /// + /// When true, system update loops are not ran and relative time (like ) does not + /// advance. This is useful for fully idling a game server and can be automatically managed by + /// . + /// bool Paused { get; set; } /// @@ -26,7 +33,8 @@ namespace Robust.Shared.Timing TimeSpan CurTime { get; } /// - /// The current real uptime of the simulation. Use this for UI and out of game timing. + /// The current real uptime of the simulation. Use this for UI and out of game timing, it is not affected + /// by pausing, timescale, or lag. /// TimeSpan RealTime { get; } @@ -91,8 +99,19 @@ namespace Robust.Shared.Timing /// /// The target ticks/second of the simulation. /// + /// + /// This is specified in simulation time, not real time. + /// ushort TickRate { get; set; } + /// + /// The scale of simulation time to real time. + /// + /// + /// A scale of 2 means the game should go "twice as slow" + /// + float TimeScale { get; set; } + /// /// The baseline time value that CurTime is calculated relatively to. /// @@ -101,6 +120,9 @@ namespace Robust.Shared.Timing /// /// The length of a tick at the current TickRate. 1/TickRate. /// + /// + /// This is in simulation time, not necessarily real time. + /// TimeSpan TickPeriod { get; } /// @@ -108,6 +130,18 @@ namespace Robust.Shared.Timing /// TimeSpan TickRemainder { get; set; } + /// + /// in real time. + /// + TimeSpan TickRemainderRealtime { get; } + + /// + /// Calculate the amount of real time to wait between ticks. + /// + /// + /// This is adjusted for various "out of simulation" + /// factors such as and . + /// TimeSpan CalcAdjustedTickPeriod(); /// @@ -142,34 +176,44 @@ namespace Robust.Shared.Timing void StartFrame(); /// - /// Is this the first time CurTick has been predicted? + /// Is this the first time CurTick has been predicted? /// bool IsFirstTimePredicted { get; } /// - /// True if CurTick is ahead of LastRealTick, and is false. + /// True if CurTick is ahead of LastRealTick, and is false. /// + /// + /// This means the client is currently running ahead of the server, to fill in the gaps for the player and + /// reduce latency while waiting for the next game state to arrive. + /// bool InPrediction { get; } /// - /// If true, the game is currently in the process of applying a game server-state. + /// If true, the game is currently in the process of applying a game server-state. /// bool ApplyingState { get; } string TickStamp => $"{CurTick}, predFirst: {IsFirstTimePredicted}, tickRem: {TickRemainder.TotalSeconds}, sim: {InSimulation}"; /// - /// Statically-accessible version of . + /// Statically-accessible version of . /// /// - /// This is intended as a debugging aid, and should not be used in regular committed code. + /// This is intended as a debugging aid, and should not be used in regular committed code. /// static string TickStampStatic => IoCManager.Resolve().TickStamp; /// - /// Resets the simulation time. This should be called on round restarts. + /// Resets the simulation time completely. While functional, no mainstream RobustToolbox game currently uses + /// this outside of client synchronization with the server and it may have quirks on existing titles. /// + /// + /// To avoid potential desynchronization where some entities think they have changes from the far future, + /// this should be accompanied by a full ECS reset using . + /// void ResetSimTime(); + /// void ResetSimTime((TimeSpan, GameTick) timeBase); void SetTickRateAt(ushort tickRate, GameTick atTick); diff --git a/Robust.Shared/Timing/IStopwatch.cs b/Robust.Shared/Timing/IStopwatch.cs index cc5bb0625..9c99a4c8a 100644 --- a/Robust.Shared/Timing/IStopwatch.cs +++ b/Robust.Shared/Timing/IStopwatch.cs @@ -4,8 +4,11 @@ namespace Robust.Shared.Timing { /// /// Provides a set of methods and properties that you can use to accurately - /// measure elapsed time. + /// measure elapsed time.
+ ///
+ /// This is legacy and of low utility (it's for mocking), prefer using . ///
+ /// public interface IStopwatch { /// diff --git a/Robust.Shared/Timing/ITimerManager.cs b/Robust.Shared/Timing/ITimerManager.cs index 99a5bcd82..46c603a2c 100644 --- a/Robust.Shared/Timing/ITimerManager.cs +++ b/Robust.Shared/Timing/ITimerManager.cs @@ -2,8 +2,20 @@ namespace Robust.Shared.Timing { + /// + /// Manages -based timing, allowing you to register new timers with optional cancellation. + /// + [NotContentImplementable] public interface ITimerManager { + /// + /// Registers a timer with the manager, which will be executed on the main thread when its duration has + /// elapsed. + /// + /// + /// Due to the granularity of the game simulation, the wait time for timers is will (effectively) round to + /// the nearest multiple of a tick, as they can only be processed on tick. + /// void AddTimer(Timer timer, CancellationToken cancellationToken = default); void UpdateTimers(FrameEventArgs frameEventArgs); diff --git a/Robust.Shared/Timing/RStopwatch.cs b/Robust.Shared/Timing/RStopwatch.cs index 8d21ae320..86d31a107 100644 --- a/Robust.Shared/Timing/RStopwatch.cs +++ b/Robust.Shared/Timing/RStopwatch.cs @@ -19,6 +19,9 @@ public struct RStopwatch private static readonly double TicksToTimeTicks = (double)TimeSpan.TicksPerSecond / SStopwatch.Frequency; + /// + /// Creates and immediately starts a new stopwatch. + /// public static RStopwatch StartNew() { RStopwatch watch = new(); @@ -26,6 +29,12 @@ public struct RStopwatch return watch; } + /// + /// Starts a stopwatch if it wasn't already. + /// + /// + /// Starting the same stopwatch twice does nothing. + /// public void Start() { if (IsRunning) @@ -36,12 +45,21 @@ public struct RStopwatch IsRunning = true; } + /// + /// Restarts the stopwatch, ensuring it is running regardless of the state it was in before. + /// public void Restart() { IsRunning = true; _curTicks = SStopwatch.GetTimestamp(); } + /// + /// Stops the stopwatch, freezing its count if it is running. + /// + /// + /// Does nothing if the stopwatch wasn't already running. + /// public void Stop() { if (!IsRunning) @@ -51,12 +69,23 @@ public struct RStopwatch IsRunning = false; } + /// + /// Completely resets the stopwatch to an unstarted state with no elapsed time. + /// Strictly equivalent to default. + /// public void Reset() { this = default; } + /// + /// The amount of elapsed time in + /// public readonly long ElapsedTicks => IsRunning ? SStopwatch.GetTimestamp() - _curTicks : _curTicks; + + /// + /// The amount of elapsed time, in real time. + /// public readonly TimeSpan Elapsed => new(ElapsedTimeTicks()); private readonly long ElapsedTimeTicks() diff --git a/Robust.Shared/Timing/Stopwatch.cs b/Robust.Shared/Timing/Stopwatch.cs index 0e97d22e0..9735311a5 100644 --- a/Robust.Shared/Timing/Stopwatch.cs +++ b/Robust.Shared/Timing/Stopwatch.cs @@ -2,10 +2,7 @@ namespace Robust.Shared.Timing { - /// - /// Provides a set of methods and properties that you can use to accurately - /// measure elapsed time. - /// + /// public sealed class Stopwatch : IStopwatch { private readonly System.Diagnostics.Stopwatch _stopwatch; @@ -18,23 +15,16 @@ namespace Robust.Shared.Timing _stopwatch = new System.Diagnostics.Stopwatch(); } - /// - /// Gets the total elapsed time measured by the current instance. - /// + /// public TimeSpan Elapsed => _stopwatch.Elapsed; - /// - /// Starts, or resumes, measuring elapsed time for an interval. - /// + /// public void Start() { _stopwatch.Start(); } - /// - /// Stops time interval measurement, resets the elapsed time to zero, - /// and starts measuring elapsed time. - /// + /// public void Restart() { _stopwatch.Restart(); diff --git a/Robust.Shared/Timing/Timer.cs b/Robust.Shared/Timing/Timer.cs index 07867b329..371ff9679 100644 --- a/Robust.Shared/Timing/Timer.cs +++ b/Robust.Shared/Timing/Timer.cs @@ -6,6 +6,13 @@ using Robust.Shared.IoC; namespace Robust.Shared.Timing { + /// + /// Non-serializable, but async friendly timers. + /// + /// + /// Using these in Space Station 14 is discouraged, it has its own idioms that are all serialization friendly. + /// + /// public sealed class Timer { /// diff --git a/Robust.Shared/Upload/IGamePrototypeLoadManager.cs b/Robust.Shared/Upload/IGamePrototypeLoadManager.cs index 0c1e56590..75570819b 100644 --- a/Robust.Shared/Upload/IGamePrototypeLoadManager.cs +++ b/Robust.Shared/Upload/IGamePrototypeLoadManager.cs @@ -3,6 +3,7 @@ using Robust.Shared.Serialization; namespace Robust.Shared.Upload; +[NotContentImplementable] public interface IGamePrototypeLoadManager { public void Initialize(); diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index 8e963e225..9a00810d9 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -6,6 +6,7 @@ using System.Text; using JetBrains.Annotations; using Nett.Parser; using Robust.Shared.Maths; +using Robust.Shared.RichText; using Robust.Shared.Serialization; namespace Robust.Shared.Utility; @@ -14,6 +15,7 @@ namespace Robust.Shared.Utility; /// Represents a formatted message in the form of a list of "tags". /// Does not do any concrete formatting, simply useful as an API surface. /// +/// [PublicAPI] [Serializable, NetSerializable] public sealed partial class FormattedMessage : IEquatable, IReadOnlyList diff --git a/Robust.Shared/Utility/MarkupNode.cs b/Robust.Shared/Utility/MarkupNode.cs index 9cf7b6706..660dac186 100644 --- a/Robust.Shared/Utility/MarkupNode.cs +++ b/Robust.Shared/Utility/MarkupNode.cs @@ -35,7 +35,7 @@ public sealed class MarkupNode : IComparable, IEquatable public override string ToString() { if(Name == null) - return Value.StringValue ?? ""; + return FormattedMessage.EscapeText(Value.StringValue ?? ""); var attributesString = ""; foreach (var (k, v) in Attributes) diff --git a/Robust.Shared/Utility/QuadTree.cs b/Robust.Shared/Utility/QuadTree.cs index af2eb53e6..1c7737d22 100644 --- a/Robust.Shared/Utility/QuadTree.cs +++ b/Robust.Shared/Utility/QuadTree.cs @@ -521,6 +521,7 @@ namespace Robust.Shared.Utility SE = 3 } + [NotContentImplementable] public interface IQuadObject { Box2 Bounds { get; } diff --git a/Robust.Shared/Utility/RuntimeInformationPrinter.cs b/Robust.Shared/Utility/RuntimeInformationPrinter.cs index 1cb4dd311..15f3ad11e 100644 --- a/Robust.Shared/Utility/RuntimeInformationPrinter.cs +++ b/Robust.Shared/Utility/RuntimeInformationPrinter.cs @@ -17,6 +17,7 @@ internal static class RuntimeInformationPrinter $".NET Runtime: {RuntimeInformation.FrameworkDescription} {RuntimeInformation.RuntimeIdentifier}", $"Server GC: {GCSettings.IsServerGC}", $"Processor: {Environment.ProcessorCount}x {SystemInformation.GetProcessorModel()}", + $"Available Memory: {ByteHelpers.FormatBytes(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}", $"Architecture: {RuntimeInformation.ProcessArchitecture}", $"Robust Version: {version}", $"Compile Options: {string.Join(';', GetCompileOptions())}", diff --git a/Robust.Shared/Utility/SystemInformation.cs b/Robust.Shared/Utility/SystemInformation.cs index 00ca56e16..38445a39a 100644 --- a/Robust.Shared/Utility/SystemInformation.cs +++ b/Robust.Shared/Utility/SystemInformation.cs @@ -181,6 +181,18 @@ internal static class SystemInformation if (Avx512Vbmi.VL.IsSupported) options.Add(nameof(Avx512Vbmi) + ".VL"); + if (Avx10v1.IsSupported) + options.Add(nameof(Avx10v1)); + + if (Avx10v1.V512.IsSupported) + options.Add(nameof(Avx10v1) + ".V512"); + + if (Avx10v2.IsSupported) + options.Add(nameof(Avx10v2)); + + if (Avx10v2.V512.IsSupported) + options.Add(nameof(Avx10v2) + ".V512"); + if (Bmi1.IsSupported) options.Add(nameof(Bmi1)); diff --git a/Robust.Shared/ViewVariables/IViewVariablesManager.cs b/Robust.Shared/ViewVariables/IViewVariablesManager.cs index f2883f854..944d6cb2e 100644 --- a/Robust.Shared/ViewVariables/IViewVariablesManager.cs +++ b/Robust.Shared/ViewVariables/IViewVariablesManager.cs @@ -5,6 +5,7 @@ using Robust.Shared.Player; namespace Robust.Shared.ViewVariables; +[NotContentImplementable] public interface IViewVariablesManager { /// diff --git a/Robust.UnitTesting/AssemblyInfo.cs b/Robust.UnitTesting/AssemblyInfo.cs index fc728beff..5ca3b49f6 100644 --- a/Robust.UnitTesting/AssemblyInfo.cs +++ b/Robust.UnitTesting/AssemblyInfo.cs @@ -3,5 +3,7 @@ using NUnit.Framework; // So it can use RobustServerSimulation. [assembly: InternalsVisibleTo("Robust.Benchmarks")] +[assembly: InternalsVisibleTo("Robust.Server.IntegrationTests")] +[assembly: InternalsVisibleTo("Robust.Shared.IntegrationTests")] [assembly: Parallelizable(ParallelScope.Fixtures)] diff --git a/Robust.UnitTesting/Helpers.cs b/Robust.UnitTesting/Helpers.cs index 99412c92a..1b3b79f5e 100644 --- a/Robust.UnitTesting/Helpers.cs +++ b/Robust.UnitTesting/Helpers.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Text; using Robust.Shared.ContentPack; -using Robust.Shared.Utility; namespace Robust.UnitTesting { diff --git a/Robust.UnitTesting/Robust.UnitTesting.csproj b/Robust.UnitTesting/Robust.UnitTesting.csproj index c14f58901..81d0d4fe5 100644 --- a/Robust.UnitTesting/Robust.UnitTesting.csproj +++ b/Robust.UnitTesting/Robust.UnitTesting.csproj @@ -2,15 +2,13 @@ false - false - ../bin/UnitTesting true + + - - @@ -19,16 +17,15 @@ + + + + - - - Robust.UnitTesting.Shared.ContentPack.ZipTest.zip - - diff --git a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs index 614cb3812..e830ae152 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.NetManager.cs @@ -10,7 +10,6 @@ using Lidgren.Network; using Robust.Shared.Asynchronous; using Robust.Shared.IoC; using Robust.Shared.Network; -using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Serialization; using Robust.Shared.Timing; diff --git a/Robust.UnitTesting/RobustIntegrationTest.TestPair.cs b/Robust.UnitTesting/RobustIntegrationTest.TestPair.cs index 38c680c27..a2bb7f552 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.TestPair.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.TestPair.cs @@ -1,6 +1,4 @@ using System.Threading.Tasks; -using Robust.Client; -using Robust.Server; using Robust.Shared.Log; using Robust.UnitTesting.Pool; diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 5db67f698..48be51495 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -36,8 +36,8 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization; +using Robust.Shared.Testing; using Robust.Shared.Timing; -using Robust.UnitTesting.Pool; using ServerProgram = Robust.Server.Program; namespace Robust.UnitTesting @@ -83,6 +83,9 @@ namespace Robust.UnitTesting /// protected virtual ServerIntegrationInstance StartServer(ServerIntegrationOptions? options = null) { + options ??= new ServerIntegrationOptions(); + options.TestAssembly = GetType().Assembly; + ServerIntegrationInstance instance; if (ShouldPool(options)) @@ -132,6 +135,9 @@ namespace Robust.UnitTesting /// protected virtual ClientIntegrationInstance StartClient(ClientIntegrationOptions? options = null) { + options ??= new ClientIntegrationOptions(); + options.TestAssembly = GetType().Assembly; + ClientIntegrationInstance instance; if (ShouldPool(options)) @@ -717,8 +723,8 @@ namespace Robust.UnitTesting //ServerProgram.SetupLogging(); ServerProgram.InitReflectionManager(deps); - if (Options?.LoadTestAssembly != false) - deps.Resolve().LoadAssemblies(typeof(RobustIntegrationTest).Assembly); + if (Options?.LoadTestAssembly != false && Options?.TestAssembly != null) + deps.Resolve().LoadAssemblies(Options.TestAssembly); var server = DependencyCollection.Resolve(); @@ -747,6 +753,7 @@ namespace Robust.UnitTesting var cfg = deps.Resolve(); cfg.LoadCVarsFromAssembly(typeof(RobustIntegrationTest).Assembly); + cfg.LoadCVarsFromAssembly(typeof(RTCVars).Assembly); if (Options != null) { @@ -977,8 +984,8 @@ namespace Robust.UnitTesting GameController.RegisterReflection(deps); - if (Options?.LoadTestAssembly != false) - deps.Resolve().LoadAssemblies(typeof(RobustIntegrationTest).Assembly); + if (Options?.LoadTestAssembly != false && Options?.TestAssembly != null) + deps.Resolve().LoadAssemblies(Options.TestAssembly); var client = DependencyCollection.Resolve(); @@ -1007,6 +1014,7 @@ namespace Robust.UnitTesting var cfg = deps.Resolve(); cfg.LoadCVarsFromAssembly(typeof(RobustIntegrationTest).Assembly); + cfg.LoadCVarsFromAssembly(typeof(RTCVars).Assembly); if (Options != null) { @@ -1214,6 +1222,7 @@ namespace Robust.UnitTesting public Assembly[]? ContentAssemblies { get; set; } public bool LoadTestAssembly { get; set; } = true; + public Assembly? TestAssembly { get; set; } /// /// String containing extra prototypes to load. Contents of the string are treated like a yaml file in the diff --git a/Robust.UnitTesting/RobustIntegrationTestDummy.cs b/Robust.UnitTesting/RobustIntegrationTestDummy.cs new file mode 100644 index 000000000..78e2aceb4 --- /dev/null +++ b/Robust.UnitTesting/RobustIntegrationTestDummy.cs @@ -0,0 +1,13 @@ +using NUnit.Framework; + +namespace Robust.UnitTesting; + +[TestFixture] +internal sealed class RobustIntegrationTestDummy : RobustIntegrationTest +{ + [Test] + public void Foo() + { + // Nada. + } +} diff --git a/Robust.UnitTesting/RobustUnitTest.IoC.cs b/Robust.UnitTesting/RobustUnitTest.IoC.cs index 7d9d20ca7..e69d60c2d 100644 --- a/Robust.UnitTesting/RobustUnitTest.IoC.cs +++ b/Robust.UnitTesting/RobustUnitTest.IoC.cs @@ -3,6 +3,7 @@ using Robust.Client; using Robust.Server; using Robust.Shared.ContentPack; using Robust.Shared.IoC; +using Robust.Shared.Testing; namespace Robust.UnitTesting { diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index fdccbcc57..71e63806a 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -14,18 +14,16 @@ using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.ContentPack; using Robust.Shared.EntitySerialization.Components; -using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Controllers; -using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Reflection; +using Robust.Shared.Testing; using Robust.Shared.Threading; using Robust.Shared.Utility; using AppearanceSystem = Robust.Client.GameObjects.AppearanceSystem; @@ -112,7 +110,7 @@ namespace Robust.UnitTesting configurationManager.LoadCVarsFromAssembly(assembly); } - configurationManager.LoadCVarsFromAssembly(typeof(RobustUnitTest).Assembly); + configurationManager.LoadCVarsFromAssembly(typeof(RTCVars).Assembly); var systems = deps.Resolve(); // Required systems diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs deleted file mode 100644 index 1a76bc500..000000000 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System.Collections.Generic; -using Moq; -using NUnit.Framework; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Reflection; -using Robust.Shared.Serialization.Manager; -using Robust.UnitTesting.Shared.Reflection; - -namespace Robust.UnitTesting.Shared.GameObjects -{ - public sealed partial class EntityEventBusTests - { - [Test] - public void SubscribeCompEvent() - { - var compFactory = new ComponentFactory(new DynamicTypeFactory(), new ReflectionManagerTest(), new SerializationManager(), new LogManager()); - - // Arrange - var entUid = new EntityUid(7); - var compInstance = new MetaDataComponent(); - - var entManMock = new Mock(); - var reflectMock = new Mock(); - - compFactory.RegisterClass(); - entManMock.Setup(m => m.ComponentFactory).Returns(compFactory); - - IComponent? outIComponent = compInstance; - entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out outIComponent)) - .Returns(true); - - entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())) - .Returns(compInstance); - - entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())) - .Returns(compInstance); - - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); - - // Subscribe - int calledCount = 0; - bus.SubscribeLocalEvent(HandleTestEvent); - bus.LockSubscriptions(); - - // add a component to the system - bus.OnEntityAdded(entUid); - - var reg = compFactory.GetRegistration(CompIdx.Index()); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); - - // Raise - var evntArgs = new TestEvent(5); - bus.RaiseLocalEvent(entUid, evntArgs, true); - - // Assert - Assert.That(calledCount, Is.EqualTo(1)); - void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) - { - calledCount++; - Assert.That(uid, Is.EqualTo(entUid)); - Assert.That(component, Is.EqualTo(compInstance)); - Assert.That(args.TestNumber, Is.EqualTo(5)); - } - - } - - [Test] - public void UnsubscribeCompEvent() - { - // Arrange - var entUid = new EntityUid(7); - var compInstance = new MetaDataComponent(); - - var entManMock = new Mock(); - - var compRegistration = new ComponentRegistration( - "MetaData", - typeof(MetaDataComponent), - CompIdx.Index()); - - var compFacMock = new Mock(); - var reflectMock = new Mock(); - - compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); - compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); - compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); - entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); - - IComponent? outIComponent = compInstance; - entManMock.Setup(m => m.TryGetComponent(entUid, typeof(MetaDataComponent), out outIComponent)) - .Returns(true); - - entManMock.Setup(m => m.GetComponent(entUid, typeof(MetaDataComponent))) - .Returns(compInstance); - - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); - - // Subscribe - int calledCount = 0; - bus.SubscribeLocalEvent(HandleTestEvent); - bus.UnsubscribeLocalEvent(); - bus.LockSubscriptions(); - - // add a component to the system - bus.OnEntityAdded(entUid); - - var reg = compFacMock.Object.GetRegistration(CompIdx.Index()); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); - - // Raise - var evntArgs = new TestEvent(5); - bus.RaiseLocalEvent(entUid, evntArgs, true); - - // Assert - Assert.That(calledCount, Is.EqualTo(0)); - void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args) - { - calledCount++; - } - - } - - [Test] - public void SubscribeCompLifeEvent() - { - // Arrange - var entUid = new EntityUid(7); - var compInstance = new MetaDataComponent(); - - var entManMock = new Mock(); - -#pragma warning disable CS0618 // Type or member is obsolete - compInstance.Owner = entUid; -#pragma warning restore CS0618 // Type or member is obsolete - - var compRegistration = new ComponentRegistration( - "MetaData", - typeof(MetaDataComponent), - CompIdx.Index()); - - var compFacMock = new Mock(); - var reflectMock = new Mock(); - - compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); - compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); - compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); - entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); - - IComponent? outIComponent = compInstance; - entManMock.Setup(m => m.TryGetComponent(entUid, typeof(MetaDataComponent), out outIComponent)) - .Returns(true); - - entManMock.Setup(m => m.GetComponent(entUid, typeof(MetaDataComponent))) - .Returns(compInstance); - - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); - - // Subscribe - int calledCount = 0; - bus.SubscribeLocalEvent(HandleTestEvent); - bus.LockSubscriptions(); - - // add a component to the system - entManMock.Raise(m => m.EntityAdded += null, entUid); - - var reg = compFacMock.Object.GetRegistration(); - entManMock.Raise(m => m.ComponentAdded += null, new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); - - // Raise - ((IEventBus)bus).RaiseComponentEvent(entUid, compInstance, new ComponentInit()); - - // Assert - Assert.That(calledCount, Is.EqualTo(1)); - void HandleTestEvent(EntityUid uid, MetaDataComponent component, ComponentInit args) - { - calledCount++; - Assert.That(uid, Is.EqualTo(entUid)); - Assert.That(component, Is.EqualTo(compInstance)); - } - } - - [Test] - public void CompEventOrdered() - { - // Arrange - var entUid = new EntityUid(7); - - var entManMock = new Mock(); - var compFacMock = new Mock(); - var reflectMock = new Mock(); - - List allRefTypes = new(); - void Setup(out T instance) where T : IComponent, new() - { - IComponent? inst = instance = new T(); - var reg = new ComponentRegistration( - typeof(T).Name, - typeof(T), - CompIdx.Index()); - - compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); - compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); - entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); - entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); - entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); - allRefTypes.Add(reg); - } - - Setup(out var instA); - Setup(out var instB); - Setup(out var instC); - - compFacMock.Setup(m => m.GetAllRegistrations()).Returns(allRefTypes.ToArray()); - - entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); - - // Subscribe - var a = false; - var b = false; - var c = false; - - void HandlerA(EntityUid uid, Component comp, TestEvent ev) - { - Assert.That(b, Is.False, "A should run before B"); - Assert.That(c, Is.False, "A should run before C"); - - a = true; - } - - void HandlerB(EntityUid uid, Component comp, TestEvent ev) - { - Assert.That(c, Is.True, "B should run after C"); - b = true; - } - - void HandlerC(EntityUid uid, Component comp, TestEvent ev) => c = true; - - bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent), before: new []{typeof(OrderBComponent), typeof(OrderCComponent)}); - bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent), after: new []{typeof(OrderCComponent)}); - bus.SubscribeLocalEvent(HandlerC, typeof(OrderCComponent)); - bus.LockSubscriptions(); - - // add a component to the system - bus.OnEntityAdded(entUid); - - var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); - var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); - var regC = compFacMock.Object.GetRegistration(CompIdx.Index()); - - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), regC)); - - // Raise - var evntArgs = new TestEvent(5); - bus.RaiseLocalEvent(entUid, evntArgs, true); - - // Assert - Assert.That(a, Is.True, "A did not fire"); - Assert.That(b, Is.True, "B did not fire"); - Assert.That(c, Is.True, "C did not fire"); - } - - [Test] - public void CompEventLoop() - { - var entUid = new EntityUid(7); - - var entManMock = new Mock(); - var compFacMock = new Mock(); - var reflectMock = new Mock(); - - List allRefTypes = new(); - void Setup(out T instance) where T : IComponent, new() - { - IComponent? inst = instance = new T(); - var reg = new ComponentRegistration( - typeof(T).Name, - typeof(T), - CompIdx.Index()); - - compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); - compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); - entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); - entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); - entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); - allRefTypes.Add(reg); - } - - Setup(out var instA); - Setup(out var instB); - - compFacMock.Setup(m => m.GetAllRegistrations()).Returns(allRefTypes.ToArray()); - - entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); - var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); - bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); - - var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); - var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); - - var handlerACount = 0; - void HandlerA(EntityUid uid, Component comp, TestEvent ev) - { - Assert.That(handlerACount, Is.EqualTo(0)); - handlerACount++; - - // add and then remove component B - bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instB, entUid), false, default!, CompIdx.Index())); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); - } - - var handlerBCount = 0; - void HandlerB(EntityUid uid, Component comp, TestEvent ev) - { - Assert.That(handlerBCount, Is.EqualTo(0)); - handlerBCount++; - - // add and then remove component A - bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instA, entUid), false, default!, CompIdx.Index())); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); - } - - bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent)); - bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent)); - bus.LockSubscriptions(); - - // add a component to the system - bus.OnEntityAdded(entUid); - - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); - bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); - - // Event subscriptions currently use a linked list. - // Currently expect event subscriptions to be raised in order: handlerB -> handlerA - // If a component gets removed and added again, it gets moved back to the front of the linked list. - // I.e., adding and then removing compA changes the linked list order: handlerA -> handlerB - // - // This could in principle cause the event raising code to enter an infinite loop. - // Adding and removing a comp in an event handler may seem silly but: - // - it doesn't have to be the same component if you had a chain of three or more components - // - some event handlers raise other events and can lead to convoluted chains of interactions that might inadvertently trigger something like this. - - // Raise - bus.RaiseLocalEvent(entUid, new TestEvent(0)); - - // Assert - Assert.That(handlerACount, Is.LessThanOrEqualTo(1)); - Assert.That(handlerBCount, Is.LessThanOrEqualTo(1)); - Assert.That(handlerACount+handlerBCount, Is.GreaterThan(0)); - } - - private sealed partial class DummyComponent : Component - { - } - - private sealed partial class OrderAComponent : Component - { - } - - private sealed partial class OrderBComponent : Component - { - } - - private sealed partial class OrderCComponent : Component - { - } - - private sealed class TestEvent : EntityEventArgs - { - public int TestNumber { get; } - - public TestEvent(int testNumber) - { - TestNumber = testNumber; - } - } - } -} diff --git a/Robust.UnitTesting/Shared/Physics/Shapes/Polygon_Test.cs b/Robust.UnitTesting/Shared/Physics/Shapes/Polygon_Test.cs deleted file mode 100644 index c1c5ac36a..000000000 --- a/Robust.UnitTesting/Shared/Physics/Shapes/Polygon_Test.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Numerics; -using NUnit.Framework; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Shapes; - -namespace Robust.UnitTesting.Shared.Physics; - -[TestFixture] -public sealed class Polygon_Test -{ - /// - /// Check that Slim and normal Polygon are equals - /// - [Test] - public void TestSlim() - { - var slim = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); - - var poly = new Polygon(Box2.UnitCentered.Translated(Vector2.One)); - - Assert.That(slim.Equals(poly)); - } - - [Test] - public void TestAABB() - { - var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); - - Assert.That(shape.ComputeAABB(Transform.Empty, 0), Is.EqualTo(Box2.UnitCentered.Translated(Vector2.One))); - } - - [Test] - public void TestBox2() - { - var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One)); - Assert.That(shape._vertices.AsSpan.ToArray(), Is.EqualTo(new Vector2[] - { - new Vector2(0.5f, 0.5f), - new Vector2(1.5f, 0.5f), - new Vector2(1.5f, 1.5f), - new Vector2(0.5f, 1.5f), - })); - } - - [Test] - public void TestBox2Rotated() - { - var shape = new SlimPolygon(new Box2Rotated(Box2.UnitCentered, Angle.FromDegrees(90))); - - Assert.That(shape._vertices.AsSpan.ToArray(), Is.EqualTo(new Vector2[] - { - new Vector2(0.5f, -0.5f), - new Vector2(0.5f, 0.5f), - new Vector2(-0.5f, 0.5f), - new Vector2(-0.5f, -0.5f), - })); - } -} diff --git a/Robust.UnitTesting/Shared/Serialization/SerializationTest.cs b/Robust.UnitTesting/Shared/Serialization/SerializationTest.cs index dedb3f5cb..bb9f21f38 100644 --- a/Robust.UnitTesting/Shared/Serialization/SerializationTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/SerializationTest.cs @@ -14,10 +14,14 @@ namespace Robust.UnitTesting.Shared.Serialization protected virtual Assembly[] Assemblies => Array.Empty(); + protected override Assembly[] GetContentAssemblies() + { + return Assemblies; + } + [OneTimeSetUp] public void OneTimeSetup() { - Reflection.LoadAssemblies(Assemblies); Serialization.Initialize(); } } diff --git a/Robust.UnitTesting/Shared/Utility/PrettyPrint_Test.cs b/Robust.UnitTesting/Shared/Utility/PrettyPrint_Test.cs deleted file mode 100644 index 2aa70c8e0..000000000 --- a/Robust.UnitTesting/Shared/Utility/PrettyPrint_Test.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NUnit.Framework; -using Robust.Shared.Utility; - -namespace Robust.Shared.TestPrettyPrint -{ - public sealed class Foo - { - override public string ToString() { return "ACustomFooRep"; } - } - - public sealed class Bar {} -} - -namespace Robust.UnitTesting.Shared.Utility -{ - - - [TestFixture] - [Parallelizable(ParallelScope.Fixtures | ParallelScope.All)] - [TestOf(typeof(PrettyPrint))] - public sealed class PrettyPrint_Test - { - private static IEnumerable<(object val, string expectedRep, string expectedTypeRep)> TestCases { get; } = new (object, string, string)[] - { - (new Robust.Shared.TestPrettyPrint.Foo(), "ACustomFooRep", "R.Sh.TestPrettyPrint.Foo"), - (new Robust.Shared.TestPrettyPrint.Bar(), "R.Sh.TestPrettyPrint.Bar", ""), - }; - - [Test] - public void Test([ValueSource(nameof(TestCases))] (object value, string expectedRep, string expectedTypeRep) data) - { - Assert.That(PrettyPrint.PrintUserFacingWithType(data.value, out var typeRep), Is.EqualTo(data.expectedRep)); - Assert.That(typeRep, Is.EqualTo(data.expectedTypeRep)); - } - } -} diff --git a/Robust.UnitTesting/Shared/Utility/TypeAbbreviations_Test.cs b/Robust.UnitTesting/Shared/Utility/TypeAbbreviations_Test.cs deleted file mode 100644 index 8e3feff25..000000000 --- a/Robust.UnitTesting/Shared/Utility/TypeAbbreviations_Test.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using NUnit.Framework; -using Robust.Shared.Utility; - -namespace Robust.Shared.TestTypeAbbreviation -{ - public sealed class Foo {} - - public sealed class Bar {} -} - -namespace Robust.UnitTesting.Shared.Utility -{ - [TestFixture] - [Parallelizable(ParallelScope.Fixtures | ParallelScope.All)] - [TestOf(typeof(TypeAbbreviation))] - public sealed class TypeAbbreviations_Test - { - private static IEnumerable<(string name, string expected)> NameTestCases { get; } = new[] - { - ("Robust.Shared.GameObjects.Foo", "R.Sh.GO.Foo"), - ("Robust.Client.GameObjects.Foo", "R.C.GO.Foo"), - ("Content.Client.GameObjects.Foo", "C.C.GO.Foo"), - ("Robust.Shared.Maths.Vector2", "R.Sh.M.Vector2"), - ("System.Collections.Generic.List", "S.C.G.List"), - ("System.Math", "S.Math"), - }; - - [Test] - public void Test([ValueSource(nameof(NameTestCases))] (string name, string expected) data) - { - Assert.That(TypeAbbreviation.Abbreviate(data.name), Is.EqualTo(data.expected)); - } - - - private static IEnumerable<(Type type, string expected)> TypeTestCases { get; } = new[] - { - ( typeof(Robust.Shared.TestTypeAbbreviation.Foo) - , "R.Sh.TestTypeAbbreviation.Foo`1[R.Sh.TestTypeAbbreviation.Bar]" - ), - (typeof(Robust.Shared.TestTypeAbbreviation.Bar), "R.Sh.TestTypeAbbreviation.Bar"), - }; - - [Test] - public void Test([ValueSource(nameof(TypeTestCases))] (Type type, string expected) data) - { - Assert.That(TypeAbbreviation.Abbreviate(data.type), Is.EqualTo(data.expected)); - } - } -} diff --git a/Robust.Xaml/Robust.Xaml.csproj b/Robust.Xaml/Robust.Xaml.csproj index ccc7d2cab..ff779e6ab 100644 --- a/Robust.Xaml/Robust.Xaml.csproj +++ b/Robust.Xaml/Robust.Xaml.csproj @@ -6,12 +6,14 @@ - - + + + + diff --git a/RobustToolbox.sln b/RobustToolbox.sln deleted file mode 100644 index 10a4a8181..000000000 --- a/RobustToolbox.sln +++ /dev/null @@ -1,341 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29503.13 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lidgren.Network", "Lidgren.Network\Lidgren.Network.csproj", "{59250BAF-0000-0000-0000-000000000000}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Server", "Robust.Server\Robust.Server.csproj", "{B04AAE71-0000-0000-0000-000000000000}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Shared", "Robust.Shared\Robust.Shared.csproj", "{0529F740-0000-0000-0000-000000000000}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.UnitTesting", "Robust.UnitTesting\Robust.UnitTesting.csproj", "{F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Client", "Robust.Client\Robust.Client.csproj", "{83429BD6-6358-4B18-BE51-401DF8EA2673}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Shared.Maths", "Robust.Shared.Maths\Robust.Shared.Maths.csproj", "{93F23A82-00C5-4572-964E-E7C9457726D4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenToolkit.GraphicsLibraryFramework", "OpenToolkit.GraphicsLibraryFramework\OpenToolkit.GraphicsLibraryFramework.csproj", "{52710F44-4240-4140-9B33-F36EBD9D3FC2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Shared.Scripting", "Robust.Shared.Scripting\Robust.Shared.Scripting.csproj", "{5AFDBB5C-CF01-4542-95F5-9ED195075313}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NetSerializer", "NetSerializer", "{9143C8DD-A989-4089-9149-C50D12189FE4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSerializer", "NetSerializer\NetSerializer\NetSerializer.csproj", "{ECBCE1D8-05C2-4881-9446-197C4C8E1C14}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Robust.LoaderApi", "Robust.LoaderApi", "{805C8FD2-0C32-4DA8-BC4B-143BA5D48FF4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.LoaderApi", "Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj", "{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Client.Injectors", "Robust.Client.Injectors\Robust.Client.Injectors.csproj", "{EEF2C805-5E03-41EA-A916-49C1DD15EF41}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Client.NameGenerator", "Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj", "{EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XamlX", "XamlX", "{1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamlX", "XamlX\src\XamlX\XamlX.csproj", "{D73768A2-BFCD-4916-8F52-4034C28F345C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamlX.IL.Cecil", "XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj", "{1CDC9C4F-668E-47A3-8A44-216E95644BEB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamlX.Runtime", "XamlX\src\XamlX.Runtime\XamlX.Runtime.csproj", "{B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Analyzers", "Robust.Analyzers\Robust.Analyzers.csproj", "{3173712A-9E75-4685-B657-9AF9B7D54EFB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "Avalonia.Base\Avalonia.Base.csproj", "{C60905B4-072F-4376-BCEC-C91186644127}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Analyzers.Tests", "Robust.Analyzers.Tests\Robust.Analyzers.Tests.csproj", "{A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Packaging", "Robust.Packaging\Robust.Packaging.csproj", "{ED52DDC3-90CB-441D-921E-E8A5AFC136FE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Client.WebView", "Robust.Client.WebView\Robust.Client.WebView.csproj", "{7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cefglue", "cefglue", "{2D787B3C-65EB-43AA-BC6F-289A34ADFDDD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CefGlue", "cefglue\CefGlue\CefGlue.csproj", "{6BC71226-BA9C-4CD6-9838-03AC076F9518}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Xaml", "Robust.Xaml\Robust.Xaml.csproj", "{EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Serialization.Generator", "Robust.Serialization.Generator\Robust.Serialization.Generator.csproj", "{26001B63-7C22-43F7-9690-D6F5D6B9711C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Roslyn", "Roslyn", "{50CEB810-1E9A-4C14-9EDE-DABCC15ECE73}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Shared.CompNetworkGenerator", "Robust.Shared.CompNetworkGenerator\Robust.Shared.CompNetworkGenerator.csproj", "{6AD8DA1A-C807-47B7-A251-97BE0AB381DB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Tools|Any CPU = Tools|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {59250BAF-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Debug|x64.ActiveCfg = Debug|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Debug|x64.Build.0 = Debug|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Release|x64.ActiveCfg = Release|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Release|x64.Build.0 = Release|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {59250BAF-0000-0000-0000-000000000000}.Tools|Any CPU.Build.0 = Tools|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Debug|x64.ActiveCfg = Debug|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Debug|x64.Build.0 = Debug|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Release|x64.ActiveCfg = Release|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Release|x64.Build.0 = Release|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {B04AAE71-0000-0000-0000-000000000000}.Tools|Any CPU.Build.0 = Tools|Any CPU - {0529F740-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0529F740-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0529F740-0000-0000-0000-000000000000}.Debug|x64.ActiveCfg = Debug|Any CPU - {0529F740-0000-0000-0000-000000000000}.Debug|x64.Build.0 = Debug|Any CPU - {0529F740-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0529F740-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU - {0529F740-0000-0000-0000-000000000000}.Release|x64.ActiveCfg = Release|Any CPU - {0529F740-0000-0000-0000-000000000000}.Release|x64.Build.0 = Release|Any CPU - {0529F740-0000-0000-0000-000000000000}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {0529F740-0000-0000-0000-000000000000}.Tools|Any CPU.Build.0 = Tools|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Debug|x64.ActiveCfg = Debug|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Debug|x64.Build.0 = Debug|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Release|Any CPU.Build.0 = Release|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Release|x64.ActiveCfg = Release|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Release|x64.Build.0 = Release|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {F0ADA779-40B8-4F7E-BA6C-CDB19F3065D9}.Tools|Any CPU.Build.0 = Tools|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Debug|x64.ActiveCfg = Debug|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Debug|x64.Build.0 = Debug|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Release|Any CPU.Build.0 = Release|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Release|x64.ActiveCfg = Release|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Release|x64.Build.0 = Release|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {83429BD6-6358-4B18-BE51-401DF8EA2673}.Tools|Any CPU.Build.0 = Tools|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Debug|x64.ActiveCfg = Debug|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Debug|x64.Build.0 = Debug|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Release|Any CPU.Build.0 = Release|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Release|x64.ActiveCfg = Release|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Release|x64.Build.0 = Release|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {93F23A82-00C5-4572-964E-E7C9457726D4}.Tools|Any CPU.Build.0 = Tools|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Debug|x64.ActiveCfg = Debug|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Debug|x64.Build.0 = Debug|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Release|Any CPU.Build.0 = Release|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Release|x64.ActiveCfg = Release|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Release|x64.Build.0 = Release|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {52710F44-4240-4140-9B33-F36EBD9D3FC2}.Tools|Any CPU.Build.0 = Tools|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Debug|x64.ActiveCfg = Debug|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Debug|x64.Build.0 = Debug|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Release|Any CPU.Build.0 = Release|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Release|x64.ActiveCfg = Release|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Release|x64.Build.0 = Release|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {5AFDBB5C-CF01-4542-95F5-9ED195075313}.Tools|Any CPU.Build.0 = Tools|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Debug|x64.ActiveCfg = Debug|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Debug|x64.Build.0 = Debug|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Release|Any CPU.Build.0 = Release|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Release|x64.ActiveCfg = Release|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Release|x64.Build.0 = Release|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14}.Tools|Any CPU.Build.0 = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Debug|x64.ActiveCfg = Debug|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Debug|x64.Build.0 = Debug|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|Any CPU.Build.0 = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.ActiveCfg = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.Build.0 = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Tools|Any CPU.Build.0 = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|x64.Build.0 = Debug|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|Any CPU.Build.0 = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|x64.ActiveCfg = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Release|x64.Build.0 = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Tools|Any CPU.Build.0 = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|x64.ActiveCfg = Debug|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Debug|x64.Build.0 = Debug|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|Any CPU.Build.0 = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|x64.ActiveCfg = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Release|x64.Build.0 = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}.Tools|Any CPU.Build.0 = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|x64.ActiveCfg = Debug|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Debug|x64.Build.0 = Debug|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|Any CPU.Build.0 = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|x64.ActiveCfg = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Release|x64.Build.0 = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {D73768A2-BFCD-4916-8F52-4034C28F345C}.Tools|Any CPU.Build.0 = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|x64.ActiveCfg = Debug|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Debug|x64.Build.0 = Debug|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|Any CPU.Build.0 = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|x64.ActiveCfg = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Release|x64.Build.0 = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {1CDC9C4F-668E-47A3-8A44-216E95644BEB}.Tools|Any CPU.Build.0 = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|x64.ActiveCfg = Debug|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Debug|x64.Build.0 = Debug|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|Any CPU.Build.0 = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|x64.ActiveCfg = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Release|x64.Build.0 = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1}.Tools|Any CPU.Build.0 = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Debug|x64.ActiveCfg = Debug|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Debug|x64.Build.0 = Debug|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Release|Any CPU.Build.0 = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Release|x64.ActiveCfg = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Release|x64.Build.0 = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {3173712A-9E75-4685-B657-9AF9B7D54EFB}.Tools|Any CPU.Build.0 = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Debug|x64.ActiveCfg = Debug|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Debug|x64.Build.0 = Debug|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Release|Any CPU.Build.0 = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Release|x64.ActiveCfg = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Release|x64.Build.0 = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {C60905B4-072F-4376-BCEC-C91186644127}.Tools|Any CPU.Build.0 = Release|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Debug|x64.ActiveCfg = Debug|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Debug|x64.Build.0 = Debug|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Release|Any CPU.Build.0 = Release|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Release|x64.ActiveCfg = Release|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Release|x64.Build.0 = Release|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A}.Tools|Any CPU.Build.0 = Tools|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Debug|x64.ActiveCfg = Debug|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Debug|x64.Build.0 = Debug|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Release|Any CPU.Build.0 = Release|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Release|x64.ActiveCfg = Release|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Release|x64.Build.0 = Release|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {ED52DDC3-90CB-441D-921E-E8A5AFC136FE}.Tools|Any CPU.Build.0 = Tools|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Debug|x64.ActiveCfg = Debug|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Debug|x64.Build.0 = Debug|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Release|Any CPU.Build.0 = Release|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Release|x64.ActiveCfg = Release|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Release|x64.Build.0 = Release|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {7FE77D04-21FC-4BD4-9097-ADC90BC71D5E}.Tools|Any CPU.Build.0 = Tools|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Debug|x64.ActiveCfg = Debug|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Debug|x64.Build.0 = Debug|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|Any CPU.Build.0 = Release|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|x64.ActiveCfg = Release|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Release|x64.Build.0 = Release|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {6BC71226-BA9C-4CD6-9838-03AC076F9518}.Tools|Any CPU.Build.0 = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|x64.ActiveCfg = Debug|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Debug|x64.Build.0 = Debug|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|Any CPU.Build.0 = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|x64.ActiveCfg = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Release|x64.Build.0 = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {EC7BA4C0-A02F-40E8-B4FC-9A96D91BD1EC}.Tools|Any CPU.Build.0 = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Debug|x64.ActiveCfg = Debug|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Debug|x64.Build.0 = Debug|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Release|Any CPU.Build.0 = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Release|x64.ActiveCfg = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Release|x64.Build.0 = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {26001B63-7C22-43F7-9690-D6F5D6B9711C}.Tools|Any CPU.Build.0 = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Debug|x64.ActiveCfg = Debug|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Debug|x64.Build.0 = Debug|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Release|Any CPU.Build.0 = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Release|x64.ActiveCfg = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Release|x64.Build.0 = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Tools|Any CPU.ActiveCfg = Release|Any CPU - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB}.Tools|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {ECBCE1D8-05C2-4881-9446-197C4C8E1C14} = {9143C8DD-A989-4089-9149-C50D12189FE4} - {4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15} = {805C8FD2-0C32-4DA8-BC4B-143BA5D48FF4} - {D73768A2-BFCD-4916-8F52-4034C28F345C} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} - {1CDC9C4F-668E-47A3-8A44-216E95644BEB} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} - {B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0} - {6BC71226-BA9C-4CD6-9838-03AC076F9518} = {2D787B3C-65EB-43AA-BC6F-289A34ADFDDD} - {3173712A-9E75-4685-B657-9AF9B7D54EFB} = {50CEB810-1E9A-4C14-9EDE-DABCC15ECE73} - {A773F7D4-8EF5-4144-A0DF-3B393B4C1B3A} = {50CEB810-1E9A-4C14-9EDE-DABCC15ECE73} - {26001B63-7C22-43F7-9690-D6F5D6B9711C} = {50CEB810-1E9A-4C14-9EDE-DABCC15ECE73} - {EFB7A05D-71D0-47D1-B7B4-35D4FF661F13} = {50CEB810-1E9A-4C14-9EDE-DABCC15ECE73} - {6AD8DA1A-C807-47B7-A251-97BE0AB381DB} = {50CEB810-1E9A-4C14-9EDE-DABCC15ECE73} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {57757344-0FF4-4842-8A68-141CAA18A35D} - EndGlobalSection - GlobalSection(Performance) = preSolution - HasPerformanceSessions = true - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - Policies = $0 - $0.DotNetNamingPolicy = $1 - $0.TextStylePolicy = $9 - EndGlobalSection -EndGlobal diff --git a/RobustToolbox.slnx b/RobustToolbox.slnx new file mode 100644 index 000000000..c76780511 --- /dev/null +++ b/RobustToolbox.slnx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/Robust.SolutionGen/Program.cs b/Tools/Robust.SolutionGen/Program.cs new file mode 100644 index 000000000..89e2f7f46 --- /dev/null +++ b/Tools/Robust.SolutionGen/Program.cs @@ -0,0 +1,181 @@ +using System.CommandLine; +using System.Diagnostics.CodeAnalysis; +using System.Xml; + +var targetOption = new Option("--solution", "-s") +{ + Description = + "Game solution file to update. If not provided, a .slnx file in the current directory is located automatically." +}.AcceptExistingOnly(); + +var outputOption = new Option("--output", "-o") +{ + Description = "If provided, output to a new file instead of updating in-place." +}.AcceptLegalFilePathsOnly(); + +var robustOptions = new Option("--robust") +{ + DefaultValueFactory = _ => new FileInfo("RobustToolbox"), + Description = "Path to RobustToolbox" +}.AcceptExistingOnly(); + +var updateCommand = new Command("update"); +updateCommand.Description = "Update your game's solution file to be compatible with this RT version."; +updateCommand.Add(targetOption); +updateCommand.Add(robustOptions); +updateCommand.Add(outputOption); +updateCommand.SetAction(CmdUpdate); + +var rootCommand = new RootCommand("Robust.SolutionGen") { updateCommand }; +return rootCommand.Parse(args).Invoke(); + +void CmdUpdate(ParseResult result) +{ + var rtSlnx = Path.Combine(result.GetRequiredValue(robustOptions).FullName, "RobustToolbox.slnx"); + var source = GetSolutionTarget(result); + var target = result.GetValue(outputOption) ?? source; + + var rtDocument = new XmlDocument(); + rtDocument.Load(rtSlnx); + var rtRoot = rtDocument.DocumentElement; + if (rtRoot is not { Name: "Solution" }) + Bail("Invalid RT solution file: does not start with Solution element"); + + var sourceDocument = new XmlDocument(); + sourceDocument.Load(source.FullName); + + var root = sourceDocument.DocumentElement; + if (root is not { Name: "Solution" }) + Bail("Invalid solution file: does not start with Solution element"); + + var features = GetFeatures(root); + + RemoveRobustEntries(root); + MergeRobustSolution(sourceDocument, root, rtRoot, features); + + sourceDocument.Save(target.FullName); +} + +void RemoveRobustEntries(XmlElement root) +{ + var toRemove = new List(); + foreach (XmlElement folder in root.GetElementsByTagName("Folder")) + { + var nameAttr = folder.GetAttribute("Name"); + if (nameAttr.StartsWith("/RobustToolbox/")) + toRemove.Add(folder); + } + + toRemove.ForEach(el => root.RemoveChild(el)); +} + +void MergeRobustSolution(XmlDocument targetDocument, XmlElement targetElement, XmlElement rtElement, string[] features) +{ + var rootFolder = targetDocument.CreateElement("Folder"); + var nameAttr = targetDocument.CreateAttribute("Name"); + nameAttr.Value = "/RobustToolbox/"; + rootFolder.Attributes.Append(nameAttr); + foreach (var elem in rtElement.ChildNodes) + { + if (elem is XmlElement { Name: "Folder" } folder) + { + var folderClone = MapFolder(targetDocument, folder, features); + if (folderClone != null) + targetElement.AppendChild(folderClone); + } + else if (elem is XmlElement { Name: "Project" } project) + { + var mappedProject = MapProject(targetDocument, project, features); + if (mappedProject != null) + rootFolder.AppendChild(mappedProject); + } + } + + var solutionFile = targetDocument.CreateElement("File"); + solutionFile.SetAttribute("Path", "RobustToolbox/RobustToolbox.slnx"); + rootFolder.AppendChild(solutionFile); + + targetElement.AppendChild(rootFolder); +} + +bool IsFeatureEnabled(XmlElement rtElement, string[] features) +{ + var config = rtElement.SelectSingleNode("Properties[@Name=\"RobustToolbox\"]/Property[@Name=\"Feature\"]/@Value"); + if (config is not { Value: { } value }) + return true; + + return features.Contains(value); +} + +XmlElement? MapFolder(XmlDocument targetDocument, XmlElement rtElement, string[] features) +{ + var clone = (XmlElement)targetDocument.ImportNode(rtElement, false); + clone.SetAttribute("Name", "/RobustToolbox" + clone.GetAttribute("Name")); + + foreach (var elem in rtElement.ChildNodes) + { + if (elem is XmlElement { Name: "Project" } project) + { + var mappedProject = MapProject(targetDocument, project, features); + if (mappedProject != null) + clone.AppendChild(mappedProject); + } + else if (elem is XmlElement { Name: "File" } file) + { + clone.AppendChild(MapFile(targetDocument, file)); + } + } + + return clone.HasChildNodes ? clone : null; +} + +XmlElement? MapProject(XmlDocument targetDocument, XmlElement rtElement, string[] features) +{ + if (!IsFeatureEnabled(rtElement, features)) + return null; + + var clone = (XmlElement)targetDocument.ImportNode(rtElement, true); + clone.SetAttribute("Path", "RobustToolbox/" + clone.GetAttribute("Path")); + return clone; +} + +XmlElement MapFile(XmlDocument targetDocument, XmlElement rtElement) +{ + var clone = (XmlElement)targetDocument.ImportNode(rtElement, true); + clone.SetAttribute("Path", "RobustToolbox/" + clone.GetAttribute("Path")); + return clone; +} + +FileInfo GetSolutionTarget(ParseResult result) +{ + var targetGiven = result.GetValue(targetOption); + + if (targetGiven != null) + return targetGiven; + + var root = Environment.CurrentDirectory; + var candidates = Directory.GetFiles(root, "*.slnx"); + if (candidates.Length > 1) + Bail("There are multiple .slnx files in this directory, please specify the path directory with --solution."); + + if (candidates.Length == 0) + Bail("There are no .slnx files in this directory, please specify the path directory with --solution"); + + return new FileInfo(Path.Combine(root, candidates[0])); +} + +string[] GetFeatures(XmlElement root) +{ + var config = root.SelectSingleNode("Properties[@Name=\"RobustToolbox\"]/Property[@Name=\"Features\"]/@Value"); + if (config is not { Value: { } value }) + return []; + + return value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); +} + +[DoesNotReturn] +void Bail(string message) +{ + Console.WriteLine(message); + Environment.Exit(1); +} diff --git a/Tools/Robust.SolutionGen/Robust.SolutionGen.csproj b/Tools/Robust.SolutionGen/Robust.SolutionGen.csproj new file mode 100644 index 000000000..21b3a0cbc --- /dev/null +++ b/Tools/Robust.SolutionGen/Robust.SolutionGen.csproj @@ -0,0 +1,14 @@ + + + + + Exe + enable + true + + + + + + + diff --git a/Tools/package_client_build.py b/Tools/package_client_build.py index 861231346..290db3e43 100755 --- a/Tools/package_client_build.py +++ b/Tools/package_client_build.py @@ -98,7 +98,7 @@ IGNORED_FILES_MACOS = { "libsodium.dylib", "libopenal.1.dylib", "libSDL3.0.dylib", - "libfluidsynth.3.dylib" + "libfluidsynth.3.dylib", "libzstd.1.dylib" } @@ -112,7 +112,7 @@ IGNORED_FILES_LINUX = { "libsodium.so", "libopenal.so.1", "libSDL3.so.0", - "libfluidsynth.so.3" + "libfluidsynth.so.3", "libzstd.so.1" } @@ -220,7 +220,7 @@ def build_linux_like(rid: str, target_os: TargetOS, skip_build: bool) -> None: client_zip = zipfile.ZipFile( p("release", "Robust.Client_%s.zip" % rid), "w", - compression=zipfile.ZIP_DEFLATED) + compression=zipfile.ZIP_DEFLATED, strict_timestamps=False) copy_dir_into_zip(p("bin", "Client", rid, "publish"), "", client_zip, IGNORED_FILES_LINUX) copy_resources("Resources", client_zip) diff --git a/Tools/package_webview.py b/Tools/package_webview.py index 7c996bb96..f5699e78a 100644 --- a/Tools/package_webview.py +++ b/Tools/package_webview.py @@ -33,7 +33,7 @@ PLATFORM_LINUX = "linux-x64" PLATFORM_LINUX_ARM64 = "linux-arm64" PLATFORM_MACOS = "osx-x64" -TARGET_FRAMEWORK = "net9.0" +TARGET_FRAMEWORK = "net10.0" def main() -> None: parser = argparse.ArgumentParser( diff --git a/global.json b/global.json index cdbb589ed..512142d2b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "10.0.100", "rollForward": "latestFeature" } }